SOLID 是缺乏灵魂的,难以记忆更难以灵活运用。 Robert Martin 在 Agile Software Development, Principles, Patterns, and Practices 中对 SOLID 的描述过于松散,原则之间缺乏逻辑的一致性和一贯性,缺少理性的分析,导致夸夸其谈者甚多。为此我们大多局限在经验的泥潭中无法自拔,在面对新的场景时却束手无策。
本文认为在面向对象的软件设计中,抽象是一种实在,而对应的多种实现则是抽象的一个个现象。基于实在的抽象和现象的实现,本文尝试构造一个逻辑上连贯且完备的面向对象设计原则模型,并基于此分析已有原则和设计模式的完备性。抽象、封装、继承和多态是面向对象设计的四大基石,其中抽象以接口的形式展现,封装以类的形式体现。
抽象是一种实在,是复杂的事物或虚拟概念在软件中的一个映射。它抓住部分侧重点并有意识地忽略无关紧要的细节,是面向对象设计中接口和类的本质性规定,可以说抽象就是接口。
实现是抽象的现象,表现为一个个具体的类,实现遵从抽象的本质性限定,
认识模型
面向对象设计原则模型
目前已知的有六个面向对象设计原则:接口隔离原则、依赖倒置原则、里氏替换原则、单一职责原则、开闭原则和最少知识法则。其中接口隔离原则、依赖倒置原则、里氏替换原则,属于抽象的范畴;单一职责原则、开闭原则、最少知识法则,属于对具体现象的约束。
抽象和现象都遵守上面的模型,即一个研究的目标都需要一个本质的界定、一个清晰的边界和与外交交互的法则。其中本质决定研究目标的存在性,边界限定存在的形式限定,交互限定目标之间的关联准则。
抽象原则
实现原则
面向对象的六大设计原则,三个接口设计原则和三个对象设计原则。
三个接口设计原则为:接口隔离原则(存在准则)、依赖倒置原则+最小依赖原则(调用准则)、里氏替换原则(抽象不变性准则)。
三个对象设计原则为:单一职责原则(存在准则)、开闭原则(变更准则)、迪米特原则(最少知识原则,关联准则)。
模版方法 VS 方法覆盖
最小依赖原则
下面我们从接口和类两个概念出发,在剖析六大设计原则的完备性基础上,提出了最小依赖原则。
接口设计原则
在接口设计原则中,接口是我们考察的基本概念。在考察接口概念定义的基础上,剖析其四个原则的合理性和完备性。
什么是接口?接口是抽象行为能力的定义,
接口隔离原则
接口尽可能的细
依赖倒置原则
实体或接口对外要依赖抽象,依赖接口
里氏替换原则
继承确保语义不变,接口语义和属性语义(KVO)
最小依赖原则(新原则)
注释:接口是对能力的抽象 抽象隔离(存在)-依赖抽象(使用方式)-抽象语义不变(运行结果)
对象设计原则
单一职责原则
一个实体只能有一个维度的变因
开闭原则
类是一个坚实的实体,可对其使用装饰模式,但不能直接修改它。
迪米特法则(最少知识法则)
两个实体不直接通信,通过一个中间的人,即用一个皮条客来协助两个实体之间的通信,比较著名的 MVC 模式就遵守了该原则。
注释:类是对特定业下公有接口和自定义接口的实现,变因-如何变-如何通信
小结
读一读黑格尔的小逻辑,把握原则背后的思想,更灵活运用这些设计原则,功夫在诗外。思维僵化、知识面太窄、缺乏批判精神,便无法对已有元素做出合理性的考察。良好的技术敏感性是局部设计中能够兼顾整体架构的关键。
设计模式
软件模式分为:设计模式、架构模式、分析模式、过程模式等。在软件生存期的每一 个阶段,都存在着一些被认同的模式。设计模式主要分为三类,创建模式、结构模式和行为模式:
- 创建模式,解决类实例化的复杂性,解决存在性;
- 结构模式,封装类的各个组合组件在类实例化过程中的复杂性,反应类的自身结构特性;
- 行为模式,解决两个或是多个对象之间的交互复杂性。
在做分析的时候,存在性、结构性、交互性,是我们对所有的软件实体在考察过程中必须使用的形而上方法论。下面我们分别从存在性、结构性、交互性三个概念出发,考察创建模式、结构模式和行为模式所包含的具体设计模式,及其在形而上下的完备性。
创建模式
行为模式(交互性12)
行为模式可化分为四个类别,行为封装(点),行为委托(线),分配行为(图),调用职责(调用)。
行为封装(在对象内部解决复杂性,使对象体现出多态的特性):策略模式、状态模式、模版方法、NULL 对象
行为委托(多个对象协作完成一个行为,自己搞不定就委托给小伙伴):责任链、备忘录模式
分配行为:观察者、中介器模式、命令模式、解释器?、
调用行为(正交化):迭代器(分离遍历过程)、访问器(分离访问过程)
观察者模式
命令模式
责任链
策略模式
抽象的语意可以用不同的策略,即实现方案,来实现,它们语意相同(相同的输入给出同样的输出)但适用的场景、资源、问题规模等肯能各有千秋。
策略模式封装的复杂性:简化版的桥接模式。
状态模式
状态变更,引起行为变更,是一个数据(状态)驱动的策略模式。
结构模式(结构性8)
实例复用:享元模式。
结构复用(接口保持不变):代理模式、装饰模式(增强已有接口)、组合(Composite 增强已有接口)模式。
调用复杂性(接口改变):适配器模式(接口重构)、外观即门面模式(为子系统提供新的抽象层,降低调用复杂性)。
实现复杂性:桥接模式。
组合设计模式
桥接模式
隔离抽象与实现,即隔离接口与实现,实现编程与实现无关。
桥接模式封装的复杂性:把抽象与实现构造为两个独立的正交类体系,抽象封装语意(提供实现的上下文),实现封装具体的过程。
适配器模式
重构已有类的接口格式,但保持严格的一对一原则,如参数传递的方式、参数的类型、函数名称等。
适配器模式封装的复杂性:在系统演化或是引入第三方库时,某些接口可能与当前的设计原则和架构风格相背,用适配器统一接口的风格,简化调用成本,统一调用风格。
外观模式
外观模式封装的复杂性:屏蔽子系统的复杂性,为调用者提供简洁的接口,降低调用复杂性,可为特定的业务场景定义特定的外观模式。
装饰模式(非常强大的模式)
用组合和关联关系,替代继承,消除为给已有对象增加新特性中的复杂性。
装饰模式封装的复杂性:解除类继承过程中失去的灵活性(比如class A 和 class B 都是 class C 的具体直接子类,如果用继承给A和B增加新特性,那么需要创建两个子类),是一个反特化的过程。
享元模式(轻量级模式)
享元模式解决细粒度对象复用的问题,复用引起的改变必须足够小。
细粒度对象在复用过程中,使用享元模式封装变更的大小。不同的值能够引起享元状态发生巨大的改变(如 iOS cell 中文本的长度引起的布局上的巨大改变,即 iOS cell 复用的是布局不是模型)就不适合使用相同的享元。
享元模式封装的复杂性:根据变更的大小,返回复用的对象。
代理模式
每个类存在单一维度的变因,但在特定的依赖关系中,依赖方与被依赖方会引入特定的一个或是多个变因(如远程代理中距离的因素),代理模式封装这种因依赖而引入的变因。它本质上与被依赖对象的无关,变因的引入更多的是因为依赖者或依赖者所处的环境所具有的限制。
代理模式封装的变因:因调用者或是调用者所处的环境,而引入的复杂性。
类图
继承关系 is-a
泛化关系,空心箭头+直线表示;实现关系,空心箭头+虚线表示。
聚合关系(aggregation)
空心菱形箭头+直线表示,整体由部分构成;整体不存在部分仍然存在。
组合关系(composition)
用带实心菱形箭头+直线表示;整体不存在,则部分就不存在。
关联关系(association)
直线表示,对象之间的结构关系、一种静态关系,是一种“强关联”的关系;默认不强调方向,表示对象间相互知道;如果特别强调方向,表示A知道B,但 B不知道A。
依赖关系(dependency)
带箭头的虚线表示的;描述运行期间会用到另一个对象,体现为参数的传递。
- https://design-patterns.readthedocs.io/zh_CN/latest/ 图片来源
- https://www.freecodecamp.org/news/the-basic-design-patterns-all-developers-need-to-know/
- https://sourcemaking.com/design_patterns/adapter
- Dive Into DESIGN PATTERNS
- https://sourcemaking.com/antipatterns/software-development-antipatterns
- https://sourcemaking.com/refactoring
- 《哲学问题》 罗素 英
- Agile Software Development, Principles, Patterns, and Practices