Introduction

总结一下个人编程以来积累的对程序架构设计的领悟和心得。

代码常常会出现各种各样的“坏味道”。虽然存在设计缺陷的代码在功能上可能是完全正确的(能完成正确的业务、能通过正确性测试),但这样的代码,工程性差,对于日后的维护、扩展会造成很大的阻碍,也会增加出问题的概率。尤其是在团队的协作开发中,设计不良带来的问题更加突出,会大幅加大开发者的沟通消耗和产生 bug 的可能性,也会给测试、迭代带来更大压力。因此了解什么样的工程设计是好的、有哪些经典的良好设计、如何对不良设计进行重构,对每个软件开发者来说都是必须要掌握好的基本功。

此外,许多设计原则也需要结合实际情况。例如“使用策略模式代替 switch 语句”,当 switch 语句非常简洁且功能简单时,创建额外的数个类往往对工程性的破坏更大(牺牲可读性、简洁性换取轻微的可扩展性提升,实际是没有必要的)。“使用策略模式代替 switch 语句”往往是针对选择软件多个核心功能时的情况。此时,该 switch 语句的每个分支内的逻辑很复杂,且 switch 的分支数量很可能会在将来发生变化。为了提升可维护性(以实现“对修改封闭、对扩展开放”)进行重构,恰当地进行抽象,编写额外代码提取出多个类是值得的。因此对于设计原则我们也不能粗暴地“一刀切”,而需要结合具体情况具体分析。如果为了遵守某个原则会大幅降低可读性或简洁性,请放心打破这个原则。当然,这对个人的经验积累提出了更高的要求。这也是我撰写这篇博客的原因。

由于实例实在太多,限于篇幅,这里主要作抽象性的总结。

 

Experiences

Decoupling(解耦)

  • 耦合:许多设计不良的现象都可以用“强耦合”来表述。例如两个类发生强耦合(尤其是直接访问彼此成员),一旦将来实现细节改变,所有的访问处需要逐一修改。这样的过程是极易出错的。为了消除强耦合,需要精准地把握两个类间进行耦合的度,从而决定是融合还是进一步抽象。

  • 解耦合、消除依赖:对于两个业务逻辑独立的类,一个类(依赖类)持有另一个类(被依赖类)的实例引用是强的耦合。若为java,解决方案:在类之间使用Interface通信(亦即:细节应该依赖于抽象)。依赖类持有被依赖类的接口引用是弱耦合的,但要注意接口中函数要尽量简洁,否则不但会造成代码冗余,还会让接口失去松耦合的性质。

     

Encapsulation(封装)

  • 高内聚:高内聚是封装的追求。当然,太追求内聚往往会导致内部逻辑过于复杂,大幅降低可读性,因此也要把握好内聚的度。纯粹的内聚会让面向对象变成面向过程,这显然是不可取的。

  • 接口的封装性:一个类如果需要与另外多个类交互,鼓励其实现多个接口。不要把一个类实现的多个接口合并,否则那些需要通过接口和这个类交互的类都会依赖于一些多余的抽象,这是违背ISP原则的。每个接口应该是高内聚的、单纯的、小而精悍的,否则接口会失去封装的作用。

  • 类的封装性:成员的getter setter也是越少越好,多了不但削弱类的封装性,还使类结构更加臃肿。如果一个类提供了太多的getter setter,而且内部还有很多复杂逻辑,说明其内聚性不足。当然对于一些只有getter setter的类(纯粹表示数据的类),例如 javabean,给每个成员提供 getter setter 是可以接受的。

  • 函数的封装性:函数参数越少越好,否则会大幅降低函数的封装性。注意不是单指个数越少越好,而是指参数实际上包含的信息量越少越好。尤其在 python 中,可变参数对封装性和可读性的破坏很大(相信经常使用 matplotlib 库的大家会感受到函数接口提示不足的缺陷)。对于一个满足了高内聚性的函数来讲,它是不应该要知道这么多信息的。多余的参数、个数不定的参数还会降低函数的可读性。

  • 继承与封装:不要滥用继承,继承必然会带来封装性和可读性的破坏。如果破坏是值得的(如:不继承会导致大量代码冗余、大幅降低可维护性,或这种继承非常顺理成章,即是is-a的关系),才可以使用继承。

     

Simplicity(简洁性)

  • 类的简洁性

    • 成员变量越少越好。①不是所有的构造器参数都要做成成员变量。因为这样会降低类的可读性,以及拖慢程序效率。②尤其是慎用标志位变量!尽量用逻辑函数代替标志位变量。使用标志位变量既增加了程序的复杂程度(处理不好修改标记位的时机,尤其是涉及并发的时候),还会降低可读性。
    • 访问权限大的函数越少越好。这也是高内聚的要求。
  • 命名的一致性(简洁性):逻辑含义上的同一个东西,命名要一致。不能这个函数的参数叫num_of_foo,那个函数的参数又叫foo_count,传参时的变量又是cnt_foo。否则也是会严重影响可读性。

  • 注释:代码的注释适量即可。首先没有注释显然是不现实的,代码并不是普通语言,不能要求读者对整个系统都了如指掌。注释也不是越多越好。需要很多注释来辅助阅读,说明代码的可读性差,或许是实现很复杂(奇技淫巧很多),或许是不能做到自解释(命名欠佳、逻辑混乱、没有利用代码对称性或者照应等)。良好的注释应该是简要解释这段代码的核心目的和功能,或是一些艰深、难以理解的小细节。将代码逐行直译为人类语言的注释是没有必要的。

     

Conclusion

OO(乃至整个代码架构)的追求是:

  • 强有力的封装(单元内部高内聚,单元之间低耦合)
  • 对修改封闭,对扩展开放(例如使用诸如策略模式/行为树等的控制类)
  • 依赖倒置(模块不应该相互依赖,应共同依赖于抽象。因语言特性等原因做不到时也要尽力避免双向依赖)
  • 良好的可读性(命名规范、风格规范、代码自解释)
  • 良好的可维护性、可扩展性(以高内聚、低耦合、低代码冗余为基础,典例模式如MVP、MVVM)
  • 良好的可移植性(必要时有意识地考虑跨平台问题)

     

发表评论

关闭菜单