面向对象基本原则的逆向解析

作为一个java出身的程序员,面向对象这个概念可谓是耳濡目染.三大特征和五大原则也理当倒背如流,但理论归理论,该如何与实践相结合呢?

拿java的设计模式举例,说来惭愧,至今我也没能完整背出传说中的23种,甚至有些都没有用过.然而我们真的需要在写代码前,像挑选工具一样,先选中一种设计模式,然后去套用吗?

程序员的思想应该是天马行空,不受拘束的.

一段优美的代码,一个优秀的框架,绝不是因为其使用了何种设计模式,而是因为其是当时环境下最合适的方案,换句话来说,软件开发没有教条,使用方说好才是真的好.

设计模式如此,那设计原则呢?虽然是一个更为宽泛的概念,死记硬背也废不了多少脑细胞.但就如同前面所说,带着这样的桎梏写代码,终究是落了下乘.理想中的状态,应该是在完全理解后,信手拈来,融会贯通,如武侠小说一样,”手中无剑,心中有剑”的地步.

那么如何能做到完全理解?在我看来,最熟悉事物的必然是其创作者,如果藉由开发过程中的感悟,整理出一套类似的基本原则,必能大大提升对其理解程度.

说了这么多,下面进入正文,从软件开发的源头开始,探究基本原则的诞生.

以下为作者在多年代码阅读,架构设计中的体会,可能存在理解上的谬误.请自行去芜存菁.


起源

每一个软件空间都如同一个小宇宙,初始一片空白.

造物主写下第一个Class.这个类开始有了自己成员变量,自己的方法,换句话说,在这个小宇宙里,有了自己的职责.

既然有”类”这个概念,那么其包含的内容应该是高度内聚的,是你的任务,就需要你独立完成,不然”要你何用”?

单一职责原则:

一个类应该只负责一件事情.

扩张

一个类显然是不够的,成熟的软件往往由成千上万个类”合作”完成其功能.而类与类之间的关系,根据UML中的定义,分为以下六种:

  • 继承
  • 实现
  • 依赖
  • 关联
  • 聚合
  • 组合

实际上,个人认为从更宽泛的角度而言,可以分为两类关系:

  • 传承/纵向关系:继承/实现
  • 合作/横向关系:依赖/关联/聚合/组合

纵向:实现和继承,分别对应interface和class,在java中都可以用instanceof来判断,下级相对于上级有非常明确的目的性.

横向:四种关系只对应了关系的强与弱.本质上是多个不同用途的类,为了某个整体目的,融合在一起.

那么在不同方向上增加类时,应该如何设计呢?

纵向发展

当我们需要修改类的某个方法实现时应该怎么做?

直接修改源码?

这会造成当前类难以维护,可回退性是技术方案实施过程中的一个重要指标,况且你怎么知道将来不会改回来?

正常做法是,对当前类进行扩展,对需要修改的方法进行扩展实现,然后替换到具体的使用场景中.

开闭原则

改实现请扩展,别改我源码.

替换,就意味着除了需求涉及部分,其他功能应该和替换前保持完全一致,那么就产生了另一个要求:

用于替换的子类,必须可以完全替代父类,不能让使用方有任何执行上的调整.

从语法上来说,java天生的继承和实现机制已经足以保证子类和实现类可以做到编译时完全替换,但是运行时呢?

举个例子:

类A有方法a和方法b两个方法,其中方法a使用到了方法b的执行结果.类B继承了类A,重写了 b方法,返回了null,此时使用类B替换类A,从语法上来说没有问题,但是执行时会出现意料之外的结果.

在实际使用中,这种情况并不少见,往往因为代码规范或者业务逻辑,产生多个关联性极强的方法,子类在重写时却并不知道其关联度,进而导致在替换父类时出现意外.

替换的方式给维护带来了便利,但同时也带来了潜在的不稳定性.

所幸的是,java提供了抽象方法,将留给子类重写的方法设置为抽象方法,由子类自行管理:

  • 可重写方法用抽象方法代替.
  • 不可重写方法设置为private或者final.

里氏替换原则

所有非抽象方法的重写其实都是不合理的.

横向发展

依赖/关联/聚合/组合,这几类只是关联关系的不同强弱等级.

既然有关联,就有耦合,前面也说过,使用替换的方式,可以快速的进行功能实现上的修改.此处替换的方式,实际上就是基于抽象编程,而非基于实现编程.

依赖反转原则

依赖接口而不是具体实现类

与此同时,接口的设计便成为一个值得考虑的问题.

是将关联方法设计到一个大接口中,还是拆成多个不同接口?

先就大接口方式举一个例子:接口I有a,b,c,d,e方法.类A只依赖了接口I的a,b,c方法,B是A中接口I的实现类.类C依赖了接口I的d,e方法,D是C中接口I的实现类.此时B需要实现不需要的方法d和e,D则需要实现不需要的方法a,b,c.这是一个非常浪费的设计.

显然根据其类别,设计成不同的接口,做精细的控制是更好的选择.接口应该是一类方法的集合,类与类之间的依赖应该建立在最小的接口上,庞大而臃肿的接口应该进行拆分.

接口隔离原则

接口应该按照功能进行拆分

至此,五大原则已经跃然纸上.知其然,更要知其所以然.回头再看看设计模式,无非都是在具体的使用场景下,根据基本原则设计出来的通用方案.只要心中谨记基本原则,不论何时何地,都可以创作出自己的设计模式.

尾声

关于”五大原则”,”六大原则”,”七大原则”:

相对于上述五大原则,多出来的两个分别是:

迪米特法则

一个对象应当对其他对象有尽可能少的了解

组合复用原则

尽量使用对象组合与聚合,而不是继承

在我看来,不管是五还是六还是七.都只是对设计原则的不同理解:

  • 迪米特法则,是依赖反转原则应用后的必然结果.
  • 组合复用原则,则是在横向发展与纵向发展中做出的取舍:组合相对于继承,可以随意拆分,具备更高的灵活度,降低了代码层次,从目的上来说,与接口隔离原则有着异曲同工之妙.

软件设计发展至今日,已经有了很多前人的优秀经验,若想站在巨人的肩膀上,直接飞上去是不行的,那一定得是一步步爬上去.

谨以此篇文章记录编码感悟.