为什么我不再用 .NET 框架
.NET平台很棒。真的很棒。直到它不再那么棒。我为什么不再用.NET?简单来说,它限制了我们选择的能力(对我来说很重要),转移了我们的注意力,使得我们向内认知它的安全性,替代了帮助我们认知外面广阔世界的所有可能性。
[系好安全带:这个文章的长度几乎成了一本书…]
优点
首先让我开始说说.NET做得对的许多事吧,尽管这其中的大多数并不来自.NET本身,但却是由.NET社区而来。
C#
C#令人惊叹。我认为它是一个令人惊叹的编程语言。从强大的C语言背景而来,我彻底地喜欢其语法,流和这门语言的所带来的感觉。当然有我可能改变的事,但总体来说它是一门扎实的语言。并且基于开发人员使用的编程语言如此巨额的百分比和Windows操作系统的优越性,它是一门众所周知的语言。
ReSharper
我也很喜欢Resharper。在JetBrains工作的开发者们都是奇迹般的人。如果没有ReSharper和一些相关的工具,我可能并不会如此喜欢C#。
BDD and MSpec
我也很喜欢简称为机器规格(mspec)的BDD风格的框架。它是一个令人惊叹的测试框架,真正支持在测试中使用正确的语言测试本身。在使用mspec之前,我的测试真是一团糟并且很碍我的事。
另外,当我们创建GoConvey—基于Golang的BDD测试框架的时候,Mspec对于我的组织来说是一个巨大的灵感和激励。
多语言运行时
我认为多语言的CLR(公共语言运行时)的观念真得使得JVM的世界思考着。我不知道任何非Java的JVM语言在CLR之前,但随着“公共语言运行时”的到来,我的理解是这使得使用JVM的人们向前进并且最终创造了如Scala和Clojure这样伟大的JVM编程语言。如果我错了请纠正我。再者,CLR使得Sun公司的人们坐下来并关注它,因为Java有一点陈旧并且随着Java 8的到来,仅仅现在才在多个方面追赶着。竞争是一件非常好的事。
NuGet
另一个显著的例子是NuGet。这个包在Windows中作为一个整体特别是在Windows的开发中,它的管理轶事是糟透的。NuGet解决了很多问题,他们也通过从Python和Ruby借用了很多东西去做了很多正确的事。有改进的余地吗?当然。但比起其他一些选择在这儿或那儿的包升级来说,我还没有感到使用NuGet有这许多痛楚。
Mono
对于Mono的开发者们,我不能不说太棒了。他们所创造的太惊奇了。没有任何官方支持和不顾潜在的悬在他们头上的法律问题,他们向前推进并创造了一个居然能替代官方运行时的实现。我已经有一些运行在产品中应用程序,在Mono下运行了几乎一年而没有任何问题。它的产品准备好了吗?这可能取决于你的应用程序(见下文“Mono”)。
CQRS 和事件溯源
可以认为,关于.NET最好事之一是,它是CQRS的诞生地并有相关的技术:事件溯源。就算这样,CQRS+ES本身并没有什么很新的东西。正如Greg Young将会告诉你的,这是由一堆40年历史原料为我们重新打包并更名的。对于大型代码库我有些非常严重的问题,当我5年前使用CQRS+ES的时候,它完全释放了我的域。CQRS+ES现在是命名模式的并且其成长是显而易见的。这可能是因为.NET已经能够和其他的开发平台交互共享的原因。除了这个之外,大多数的创新是从外部来的。
缺点
优点先放在一边,让我们看看什么出错了和我为什么不再用.NET框架。关于我最近开发平台的迁移,最能激励我的事是我可以利用许多最好的部分而丢下不好的部分(如下文所说)。
Windows
正如前文所述,当面对基于网络的服务器软件时,Windows并不是一个好的选手。在我看来,Windows的另一个真正的大问题是传统的Windows开发者是通常仅仅擅长于Windows,当他们离开安乐窝之后就会很快迷失,这对于Linux开发者来说却不是问题。计算远不止是Windows。开发者仅仅能操作单一的操作系统的一个问题是它不可避免得导致Windows的激增。换句话说,Windows生了Windows。没办法打破这个循环。
另一方面,*NIX的开发者通常熟悉多操作系统(Linux,Unix,OSX,Windows等等),一个操作系统的内部工作原理,不同的分布(基于Debian和基于Fedora),窗口管理器,桌面管理器,文件系统,包管理,编译,重新编译,重新打包,命令行“fu”等等。
我的一个心病是文件系统。NTFS并不是系统唯一的文件系统,对于任何给予的任务它几乎都不是最好的选择。ZFS,BTRFS,ReiserFs,ext*等等,有一些很酷的特性。我也很喜欢为了各种高速/透明的磁盘操作,能从BASH创建回路设备或者创建RAM设备。这在Windows中不会发生—如果没有第三方软件的话。
在AWS云服务中,启动一个Windows机器要花掉足足10多分钟。我大约15-20秒就能启动一个简单的Linux机器。当涉及到云计算规模,它能够迅速扩展是很重要的,因为当扩展很重要时,10-15分钟就像是永恒的。
Visual Studio
在我这另一根刺,当属Visual Studio。我需要一个大大超出预期的 IDE 去做任何开发,这个想法困扰着我。它只是如Windows一样庞大的资源猪。我有一个内核i7 3770K 3.5GHZ的台式机,以16GB的内存和最大4512GB的固态硬盘去编译。它差不多刷爆了Windows体验指数,但Windows+VS仍然很慢。(是的,ReSharper使得它更慢了,但是ReSharper对这来说是值得的。)
现在我在MacBook Pro上开发,它比起我的强大的台式机来说只有更少的CPU马力,但运行明显更快,在一个短小的学习曲线之后,UX(用户体验)变得无限美好了。事实上,我甚至不再用鼠标了—我的双手一直在键盘或触控板上,我可以用手势操作我的电脑并让它回应—不像在Windows。
关于VS很酷的一个事是调试器。它的查看和使用,令人难以置信得方便。每隔一段时间会在监视窗口报告错误的值,导致花费更多时间去调试。同时,这也是很大的负面,因为CLR默认的,多线程的世界使得我一开始就需要一个调试器。没有调试器是一个解脱的体验,因为它迫使你以另一种方式编程。
VS同样也有创建“csproj”和“sln”文件的坏毛病。我恨这些。当然,C#必须知道编译什么和何时编译。我理解这点。在Golang中,引用在代码中使用了很重要的语句。如果它不是.NET中用到的工程文件,我可能使用简单的文本编辑器编码C#,并且对这门语言更流畅。使用git rebase操作时,这些文件也有导致合并冲突。
别让我开始说换行符的差异。我不能相信直到今天我们还在处理这样的事。如果VS解决方案文件以Linux行结束符结束,通过双击它并不能载入该解决方案,因为VS解决方案文件分析器读不出它来。
源代码管理
幸运的是,我早就跳出了微软阵营的源代码管理(版本控制系统VSS)。我早在2000年初,在VSS无数次丢失了我的提交之后,就使用了Subversion(译者注:Subversion是开源的版本控制系统)。之后git(译者注:git是开源的版本控制系统,内容管理系统等)出现了,我又迷上了它。不幸的是,没有Windows的接口—对我来说是典型的遭遇。最终有人创建了一个接口,我就用了那个并且没有回头。Git是一把非常锋利的刀,但当你正确运用它的时候,它是一个强大而高效的工具。我曾经在一个小工程中用过TFS(译者注:Team Foundation Server,工作流协作引擎),它是一个怪物—和所有来自Redmond(译者注:美国微软总部)的产品一样。它感染了我的项目文件并且污染了我的源代码目录。真可恶。不,还是谢谢你。给了我任意一天用命令行git…或者可能是SourceTree,如果你需要从GUI得到一点关爱。
Mono
是的,这是第二次提及Mono。正如Mono本身如此惊艳一样。在.NET的世界,它仍然二等公民。无论什么时候我尝试在Mono上运行任何重要的东西,我通常都在和漏洞作斗争。幸运的是,对下载代码,查找问题,发送请求和在Linux上编译代码我没有感到不舒服。但是这件事我都记不清做了多少遍了。
是的,CLR是个巨大的怪物,并且对一个非官方的应用在不同的操作系统都有相同的行为,简直是个类似于分开红海的奇迹。但事实是,我不得不花费如此多的时间来填补漏洞以使我的代码能够正确运行,实在是很难为其辩护。
Mono的特定区域也慢。也许它不是在慢在过载,但对我来说Web服务器是关键所在。并且它非常慢,最后,慢到了最底下—即使是微不足道的东西。我想好消息是它只能从这儿得到更好的。我也应该提及Mono的开发者可能忘了Linux,比起我可能知道的还多,所以我不能太挑剔。
IIS
也许IIS在尝试着为太多的应用程序做太多的事情。它从作为一个web服务器变为像J2EE应用程序容器一样的应用程序宿主。它也站在慢速这一边。我猜如果我需要更高的性能,我应该编写我自己的web服务器,但我真的很想只关注我应用程序的代码。可能利用Windows事件服务器将是好的,但nginx(译者注:一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP代理服务器)和其他服务器只是不喜欢在Windows中生产。
虚拟的以JVM为基础的实现,例如Netty(译者注:JBOSS提供的一个java开源框架),很容易处理每秒650K+/的请求量。IIS在运行一个简单的CLR应用程序“Hello,World!”,处理大约每秒50K的请求量时就会壅塞。(有趣的题外话,参考基准开发者通过TCP套接字创建了一个简单的C#的web服务器,它能处理大约每秒120K的请求量。)
狭隘的心理
前些年有个运动叫做ALT.NET。该运动是全部是关于寻找我们自身之外的更广阔的开发社区以作为一个整体,并汇聚不同的部分。有趣的是,那是StructureMap、Autofac、NuGet、ASP.NET MVC和许多其它工具的灵感来源。在传统的.NET的圈子里,这个运动受到了很多的不屑和鄙视。我把这看作是,作为一个整体的社区普遍的狭隘心理和怠惰的一个极大的例证。(的确,它们中的一些可能会消失,进而以包括Redis,MongoBD还有其它的不同的技术而出现。)
有这么多很棒的方案在那里。假定微软已注定是唯一正确之路的想法是荒谬的。如果是这样的话,我们就都还在使用Visual Studio的设计工具去拖放按钮和链接元素到一个WebForm的界面上,我们会设定了该按钮并且依赖ViewState以帮助我们与可怕的HTTP所带来的恐惧隔开。我从我的一个部署的代码库中最后一个WebForm中摆脱的那一天,是个光荣的值得庆贺的日子。
谁又曾想过“网络控制”是个好主意?很显然我考虑过因为我喝了Kool-Aid(译者注:卡夫公司出品的饮料,这里意指明知是注定的或有危险的仍然去做,有负面涵义)并且完全接受它。它狠咬了我。见过2MB的ViewState吗?
[注:当我写这篇文章的时候,原来的标题,“为什么我不再用.NET”,意味着整个.NET生态系统。标题感觉有点短于是我更新为“为什么我不再用.NET框架”。我想.NET作为一个生态系统,包括了所有的工具,工程,平台,组织还有很多开发者。这就是为什么有些更广泛的.NET社区的元素在我的这篇文章中受到抨击原因。]
性能杀手
C,Java和C#中典型的多线程范例都强烈推荐使用锁和互斥。对于锁来说有个隐藏的开销:它们慢得难以忍受。使用Disruptor(JVM中的无锁的环形缓存[译者注:实际上就是拥有一个序号指向下一个可用元素的数组]),你可以很容易得每秒处理20M以上的事件。在.NET中使用规定的“最佳实践”等任何超过每秒十几次的传输,都被认为是体面又好的性能表现,在这一点上来说你仅仅需要更大/更好/更多的硬件设备。事实上,我见过第三方客户端库(Rabbit,Couch,Mongo等等)中锁语句遍布整个代码。即使在我的代码中没有任何的并发,默认的和首选的方法都用了锁。
无锁的、事件驱动的方法允许你大幅降低硬件和资金支出。大部分应用程序可以轻易地运行在两台机器上,第二台机器仅仅在冗余和失效备援时是必须的,以防因为硬件相关的问题导致第一台机器不可用的时候起作用。
这个问题的另一个方面是调用网络和磁盘子系统的传统方式:同步,阻塞代码。如果你需要多个并发的HTTP请求,你需要更多的线程。大多数人不知道的是,为维持线程多出的1-2MB和上下文切换线程的需求,使得CPU内核消耗所有的时间颠簸在上下文切换上而不是做真正的工作。所以现在我们得到了在一个应用程序中数百或数千的线程,占用了RAM,并造成CPU停滞不前。还有个更好的方式。
Netty/NIO (JVM),Erlang,Node,Gevent (Python)和Go都支持使用事件驱动的子系统操作(选择/epoll[译者注:Linux内核中的一种可扩展IO事件处理机制]/kqueue[译者注:FreeBSD的可扩展的事件通知接口])。这就意味着当等待数据包被tx/rx跨网络的时候,CPU可以自由地去做其它,重要的工作。因为JVM的成熟,Netty可以认为是做这项工作最快的,但我喜欢Go用Goroutines操作这个的方式—它简单,优雅,很容易推理,没有像意大利面条一样的回调。
SQL Server
作为一名.NET开发者,当你开始一个新的工程时,有一些事是你通常会去做的:
- 创建一个新的solution
- 将其部署到Team Foundation Server(译者注:Microsoft 应用程序生命周期管理 (ALM) 解决方案的核心协作平台)
- IIS中建立相应的网站入口
- 创建一个新的SQL Server数据库
- 在solution中关联Entity Framework(通常是2010年之后创建的工程)
- 开始设计你的数据库和ActiveRecord实体
在大多数情况下这不是编写代码的正确方式。当然它可能在某些情况下有效,但是作为一个“默认的架构”它并不是你想要的。为什么在我们甚至还没理解问题领域之前已经做了任何技术上的选择?这简直是本末倒置了。
微软的生态系统鼓励每个人使用SQL Server。在Visual Studio中和SQL Service进行交互或者使用SQL Management Studio(和它的前身,SQL查询分析器)是如此令人难以置信的容易。这种以数据库为中心的重点,是钦定的或唯一正确的方式的一部分。它使你更加迷恋微软。厂商锁定始终对厂商来说是好的。
为什么我们要如此开发?为什么我们不更多地考虑应用程序的行为而不是它如何存储的?现在我所有的项目都使用基于JSON的键/值存储。有了这种功能,我可以选择任何我想要的存储引擎,包括SQL Server,Oracle,PostgreSQL,MySQL,Cassandra, CouchDB, CouchBase, Dynamo, SimpleDB, S3, Riak, BerkeleyDB, Firebird, Hypertable, RavenDB, Redis, Tokyo Cabinet/Tyrant, Azure Blobs,文件系统中的明文JSON文件等等等等。突然之间,我们能够开始根据其优点而不是仅仅对其熟悉来选择存储引擎了。
题外话:在AWS RDS的云上运行过SQL Server吗?别这么做。当然它会工作,但是一些例如复制这样最简单的事是不存在的。文章充斥着对SQL Server不能在AWS RDS上工作的引用。
结论
也许我在软件开发中学到的两件最重要的教训是:
- 边界和封装的重要性(以多种形式)
- 付出代价以得到正确的模型和抽象
许多年前我恨“模型”这个词。每个人都会把它到处扔,它是一个如此过载的术语,很难理解它的含义和它为什么这么重要。就这点来说,我仅仅会说模型是对你想要封装的现实的一个有限的表示。也许最简单的例子就是地球仪的墨卡托投影了。这很确切得说明了一件事:导航。如果你在其他的事情上使用它,它并不毫无价值。如果你不专注于付出代价去使模型正确,去封装商业现实,那么没有任何技术能够拯救你。
我对.NET最大的抱怨是,“唯一正确的方式”引导你远离理想的模型并把你推向关注实现细节和技术缺陷的方向。这样的关注导致技术实施渗血并且感染模型,最终导致它腐烂变质,因为它不能适应不断变化的商业需求。当这发生的时候,开发者挣扎着并蹬踢着,如同吸毒者一样,他们从一个新技术转向另一个,以期望下一个强大的技术能够治愈他们的病痛。
技术本身并不是灵丹妙药,相反地,它是关于取舍和选择。只有正确地理解了商业行为并把它们封装进结构良好的,易于理解的模型中,以帮助保持技术堆栈在属于它的地方—作为一个实现细节。
这就是我为什么不再用.NET 框架,因为它不断地重申自己(的主张),不断地想要比它的本身更多的:一个实现细节。