代码与代码之外

Code and Beyond

Go对比C++:它们自身的设计符合面向对象原则吗?

阅读本文需要深入学习过面向对象设计,最好研究过设计模式并有多种非函数式语言的学习和使用经验,最好用过 Go 和 C++。

我本人资质普通,写 Go 和 C++ 的时间差不多都是两到三年,目前对 C++ 的态度是:爱过,但不再回头。

面向对象是当前被多数同行认可的开发方法,整体来讲我认为它利大于弊,是目前最能保障开发质量的方法。但以我多年的观察来看,其实还有挺多同行了解不深,也有很多过度设计的走火入魔者,导致面向对象方法并没有被有效的利用,非常可惜。

关于面向对象概念的定义,其实学术研究和工业实现有一些不同:学术上更强调对象/类、消息通讯、继承,工业实现如 C++ 更强调类、封装、继承、多态、泛型等。

从每种语言最外层的高级特性来看,它们似乎都非常不同。但往往学过 C/C++ 后再学习其它语言如 JAVA/C#/Python/Go 就非常容易了,这是因为各语言除去高级特性后的核心特性都是非常相似的结构化程序设计方法。高级特性仅仅体现不同的语言对不同应用场景的侧重,如指针方便内存操作,但在企业应用场景就必须弱化!当我们分析一个语言的设计时,八成是针对它的核心特性,二成是针对其高级特性。

到现在为止讨论设计模式似乎也并不过时,不管是 GoF 的 23 种设计模式还是牛人自创的 24、25、26…… 它们都是围绕所谓的 SOLID 原则来实现的。SOLID 这个词本身意思是“坚固,可靠的”,在设计模式语境里代表五大面向对象原则,除此之外还有两个面向对象的原则,它们七个分别是:单一职责原则、开放封闭原则、里氏代换原则、接口分离原则、依赖倒置原则、组合复用原则、迪米特法则。

下面我们用这七条原则来衡量编程语言本身的设计是不是足够的面向对象!

单一职责原则(S,Single Responsibility):以 0 来说,在 C/C++ 中它可以表示数字 / 逻辑非 / 空指针 / 纯虚函数修饰符 等等(或许还有我不知道的,加个“等等”表示一下谦虚),实实在在的承担了过多的职责。可能是因为 C 那个年代流行这么干吧,Go 把这三个分解为 0 / false / nil 是更先进更清晰的风格。

开放封闭原则(O,Open/Closed):如果要改动一个 C++ 程序,往往会出现牵一发动全身的事,如果设计中使用了泛型/继承/多重继承等高级特性会更悲惨。C++ 完全依靠程序员的个人设计能力和对 C++ 细节的掌握,但多年经验告诉我:往往两者都靠不住。与此相比,易学易用的 Go 采用组合方式的继承和非浸入式接口的设计使其扩展极为方便,修改也更安全。优秀的非浸入式接口设计可以使程序功能大变样的时候,代码复用率保持在非常高的水平。

里氏代换原则(L,Liskov Substitution):快速的在 C++ 中用某一个子类代替其父类需要一定的胆量,如果这个子类结构中包含了虚函数表指针和虚基类指针那更需要英雄气概!如果想要更有把握一点,你必须按继承方式和顺序上溯很远才行,但在 Go 代码中这样的事不值一提。

接口分离原则(I,Interface Segregation):Go 的非浸入式接口在其它语言上没见过!

依赖倒置原则(D,Dependency Inversion):以迭代器为例,C++ 相比 C 实在是非常大的进步,然而 C++ 的迭代器依赖它的高级特性和库实现,而 for range 是 Go 的核心功能由编译器实现。

组合/复用原则(Composite/Aggregate Reuse):Go 中的结构和接口都是用的组合式继承,跟其它语言的继承机制比有明显优势,但理论上会占用更多廉价的内存。

迪米特法则(Law of Demeter/Least Knowledge Principle):C++ 中的友元设计明显违反此原则。另外在 C++ 中,你没有明显用的特性可能也间接在用,导致出现异常的时候你必须追踪更多代码和学习更多东西。使用 Go 写代码会降低程序员的心智成本,但是某些时候翻一下库源码也是必要的。

C++ 因为年代关系,设计时杂揉了太多东西,明显不够克制。大家学 C++ 其实大多数特性是用不上或用不好的,但你必须去学习,因为你集成的别人的库里可能会这样用,不懂你就掉坑里了。各种特性过多的耦合,也影响了代码的工程化,用 C++ 实现高内聚、低耦合、正交化设计的目标需要付出更多努力。

倒是少了很多复杂机制的 Go 本身的设计反而更面向对象,因为它本身的设计更符合 SOLID 原则!


2016-11-02 深圳

今天公司创始人兼 CTO 离职,今年的旅游团建方案也出来了,大家对旅游更感兴趣!