大家看到这个标题肯定会欢呼雀跃了,以为功能少的语言就容易学。其实完全不是这样的。功能少的语言如果还适用范围广,那所有的概念必定是正交的,最后就会变得跟数学一样。数学的概念很正交吧,正交的东西都特别抽象,一点都不直观的。不信?出门转左看Haskell,还有抽象代数。因此删减语言的功能是需要高超的技巧的,这跟大家想的,还有跟go那帮人想的,可以断定完全不一样。
首先,我们要知道到底为什么需要删减功能。在这里我们首先要达成一个共识——人都是很贱的。一方面在发表言论的时候光面堂皇的表示,要以需求变更和可维护性位中心;另一方面自己写代码的时候又总是不惜“后来的维护者所支付的代价代价”进行偷懒。有些时候,人就是被语言惯坏的,所以需要对功能进行删减的同时,又不降低语言的表达能力,从而让做不好的事情变得更难(完全不让别人做不好的事情是不可能的),这样大家才会倾向于写出结构好的程序。
于是,语法糖到底是不是需要被删减的对象呢?显然不是。一个好的语言,采用的概念是正交的。尽管正交的概念可以在拼接处我们需要的概念的时候保持可维护性和解耦,但是往往这么做起来却不是那么舒服的,所以需要语法糖。那如果不是语法糖,到底需要删减什么呢?
这一集我们就来讨论面向对象的语言的事情,看看有什么是可以去掉的。
在面向对象刚刚流行起来的时候,大家就在讨论什么搭积木编程啊、is-a、has-a这些概念啊、面向接口编程啊、为什么硬件的互相插就这么容易软件就不行呢,然后就开始搞什么COM啊、SOA啊这些的确让插变得更容易,但是部署起来又很麻烦的东西。到底是什么原因造成OO没有想象中那么好用呢?
之所以会想起这个问题,其实是因为最近在我们研究院的工位上出现了一个相机的三脚架,这个太三脚架用来固定一个手机干点邪恶的事情,于是大家就围绕这个事情展开了讨论——譬如说为什么手机和三脚架是正交的,中间只要一个前凸后凹的用来插的小铁块就可以搞定,而软件就不行呢?
于是我就在想,这不就是跟所谓的面向接口编程一样,只要你全部东西都用接口,那软件组合起来就很简单了吗。这样就算刚好对不上,只要写个adaptor,就可以搞定了。其实这种做法我们现在还是很常见的。举个例子,有些时候我们需要Visual C++ 2013这款全球最碉堡的C++ IDE来开发世界上最好的复杂的软件,不过自带的那个cl.exe实在是算不上最好的。那怎么办,为了用一款更好的编译器,放弃这个IDE吗?显然不是。正确的解决方法是,买intel的icc,然后换掉cl.exe,然后一切照旧。
其实那个面向接口编程就有点这个意思。有些时候一个系统大部分是你所需要的,别人又不能满足,但是刚好这个系统的一个重要部分你手上又有更好的零件可以代替。那你是选择更好的零件,还是选择大部分你需要的周边工具呢?为什么就非得二选一呢?如果大家都是面向接口编程,那你只需要跟cl.exe换成icc一样,写个adaptor就可以上了。
好了,那接口是什么?其实这并没有什么深奥的理解,接口指的就是java和C#里面的那个interface,是很直白的。不知道为什么后来传着传着这条建议就跟一些封装偶合在一起,然后各种非面向对象语言就把自己的某些部分曲解为interface,成功地把“面向接口编程”变成了一句废话。
不过在说interface之前,有一个更简单但是可以类比的例子,就是函数和lambda expression了。如果一个语言同时存在函数和lambda expression,那么其实有一个是多余的——也就是函数了。一个函数总是可以被定义为初始化的时候给了一个lambda expression的只读变量。这里并不存在什么性能问题,因为这种典型的写法,编译器往往可以识别出来,最终把它优化成一个函数。当我们把一个函数名字当成表达式用,获得一个函数指针的时候,其实这个类型跟lambda expression并没有任何区别。一个函数就只有这两种用法,因此实际上把函数去掉,留下lambda expression,整个语言根本没有发生变化。于是函数在这种情况下就属于可以删减的功能。
那class和interface呢?跟上面的讨论类似,我主张class也是属于可以删减的功能之一,而且删减了的话,程序员会因为人类的本性而写出更好的代码。把class删掉其实并没有什么区别,我能想到的唯一的区别也就是class本身从此再也不是一个类型,而是一个函数了。这有关系吗?完全没有,你用interface就行了。
class和interface的典型区别就是,interface所有的函数都是virtual的,而且没有局部变量。class并不是所有的函数都是virtual的——java的函数默认virtual但是可以改,C++和C#则默认不virtual但是可以改。就算你把所有的class的函数都改成virtual,那你也会因此留下一些状态变量。这有什么问题呢?假设C++编译器是一个接口,而Visual C++和周边的工具则是依赖于这个class所创造出来的东西。如果你想把cl.exe替换成icc,实际上只要new一个新的icc就可以了。而如果C++编译器是一个class的话,你就不能替换了——就算class所有的函数都是virtual的,你也不可能给出一个规格相同而实现不同的icc——因为你已经被class所声明的构造函数、析构函数以及写好的一些状态变量(成员变量)所绑架了!
那我们可以想到的一个迫使大家都写出倾向于比以前更可以组合的程序,要怎么改造语言才可以呢?其实很简单,只需要不把class的名字看成一个类型,而把他看成一个函数就可以了。class本身有多个构造函数,其实也就是这个意思。这样的话,所有原本要用到class的东西,我们就会去定义一个接口了。而且这个接口往往会是最小化的,因为完全没有必要去声明一些用不到的函数。
于是跟去掉函数而留下匿名函数(也就是lambda expression)类似,我们也可以去掉class而留下匿名class的。Java有匿名class,所以我们完全不会感到这个概念有多么的陌生。于是我们可以来检查一下,这样会不会让我们丧失什么表达方法。
首先,是关于类的继承。我们有四种方法来使用类的继承。
1、类似于C#的Control继承出Button。这完全是接口规格的继承。我们继承出一个Button,不是为了让他去实现一个Control,而是因为Button比Control多出了一些新东西,而且直接体现在成员函数上面。因此在这个框架下,我们需要做的是IControl继承出IButton。
2、类似于C#的TextReader继承出StreamReader。StreamReader并不是为了给TextReader添加新功能,而是为了给TextReader指定一个来源——Stream。因此这更类似于接口和实现的区别。因此在这个框架下,我们需要的是用CreateStreamReader函数来创建一个ITextReader。
3、类似于C#的XmlNode继承出XmlElement。这纯粹是数据的继承关系。我们之所以这么做,不是因为class的用法是设计来这么用的,而是因为C++、Java或者C#并没有别的办法可以让我们来表达这些东西。在C里面我们可以用一个union加上一个enum来做,而且大家基本上都会这么做,所以我们可以看到这实际上是为了拿到一个tag来让我们知道如何解释那篇内存。但是C语言的这种做法只有大脑永远保持清醒的人可以使用,而且我们可以看到在函数式语言里面,Haskell、F#和Scala都有自己的一种独有的强类型的union。因此在这个框架下,我们需要做的是让struct可以继承,并且提供一个Nullable<T>(C#也可以写成T?)的类型——等价于指向struct的引用——来让我们表达“这里是一个关于数据的union:XmlNode,他只可能是XmlElement、XmlText、XmlCData等有限几种可能”。这完全不关class的事情。
4、在Base里面留几个纯虚函数,让Derived继承自Base并且填补他们充当回调使用——卧槽都知道是回调了为什么还要用class?设计模式帮我们准备好了Template Method Pattern,我们完全可以把这几个回调写在一个interface里面,让Base的构造函数接受这个interface,效果完全没有区别。
因此我们可以看到,干掉class留下匿名class,根本不会对语言的表达能力产生影响。而且这让我们可以把所有需要的依赖都从class转成interface。interface是很好adapt的。还是用Visual C++来举例子。我们知道cl.exe和icc都可以装,那gcc呢?cl.exe和icc是兼容的,而gcc完全是另一套。我们只需要简单地adapt一下(尽管有可能不那么简单,但总比完全不能做强多了),就可以让VC++使用gcc了。class和interface的关系也是类似的。如果class A依赖于class B,那这个依赖是绑死的。尽管class A我们很欣赏,但是由于class B实现得太傻比从而导致我们必须放弃class A这种事情简直是不能接受的。如果class A依赖于interface IB,就算他的缺省实现CreateB()函数我们不喜欢,我们可以自己实现一个CreateMyB(),从而吧我们自己的IB实现给class A,这样我们又可以提供更好的B的同时不需要放弃我们很需要的A了。
不过其实每次CreateA(CreateMyB())这种事情来得到一个IA的实现也是很蠢得,优点化神奇为腐朽的意思。不过这里就是IoC——Inverse of Control出场的地方了。这完全是另一个话题,而且Java和C#的一些类库(包括我的GacUI)已经深入的研究了IoC、正确使用了它并且发挥得淋漓尽致。这就是另一个话题了。如何用好interface,跟class是否必须是类型,没什么关系。
但是这样做还有一个小问题。假设我们在写一个UI库,定义了IControl并且有一个函数返回了一个IControl的实现,那我们在开发IButton和他的实现的时候,要如何利用IControl的实现呢?本质上来说,其实我们只需要创造一个IControl的实现x,然后把IButton里面所有原本属于IControl的函数都重定向到这个x上面去,就等价于继承了。不过这个写起来就很痛苦了,因此我们需要一个语法糖来解决它,传说中的Mixin就可以上场了。不知道Mixin?这种东西跟prototype很接近但是实际上他不是prototype,所以类似的想法经常在javascript和ruby等动态语言里面出现。相信大家也不会陌生。
上面基本上论证了把class换成匿名class的可能性(完全可能),和他对语言表达能力的影响(毫无影响),以及他对系统设计的好处(更容易通过人类的人性的弱点来引导我们写出比现在更加容易解耦的系统)。尽管这不是银弹,但显然比现在的做法要强多了。最重要的是,因为class不是一个类型,所以你没办法从IX强转成XImpl了,于是我们只能够设计出不需要知道到底谁实现了IX的算法,可靠性迅速提高。如果IY继承自IX的话,那IX可以强转成IY就类似于COM的QueryInterface一样,从“查看到底是谁实现的”升华到了“查看这个IX是否具有IY所描述的功能”,不仅B格提高了,而且会让你整个软件的质量都得到提高。
因此把class换成匿名class,让原本正确使用OO的人更容易避免无意识的偷懒,让原本不能正确使用OO的人迅速掌握如何正确使用OO,封死了一大堆因为偷懒而破坏质量的后门,具有相当的社会意义(哈哈哈哈哈哈哈哈)。
我之所以写这篇文章是为了告诉大家,通过删减语言的功能来让语言变得更好完全是可能的。但这并不意味着你能通过你自己的口味、偷懒的习惯、B格、因为智商低而学不会等各种奇怪的理由来衡量一个语言的功能是否应该被删除。只有冗余的东西在他带来危害的时候,我们应该果断删除它(譬如在有interface前提下的class)。而且通常我们为了避免正交的概念所本质上所不可避免的增加理解难度所带来的问题,我们还需要相应的往语言里面加入语法糖或者新的结构(匿名class、强类型union等)。让语言变得更简单从来不是我们的目标,让语言变得更好用才是。而且一个语言不容易学会的话,我们有各种方法可以解决——譬如说增加常见情况下可以解决问题的语法糖、免费分享知识、通过努力提高自己的智商(虽然有一部分人会因此感到绝望不过反正社会上有那么多职业何必非得跟死程死磕)等等有效做法。