1. 面向对象原则
(1)开闭原则
所谓开闭原则(Open Closed Principle, OCP)指的就是“软件实体应当对扩展开放,对修改关闭”,简单讲就是软件系统中包含的各种组件应该在不修改现有代码的基础上,引入新功能。开闭原则中“开”,是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中“闭”,是指对于原有代码的修改是封闭的,即不应该修改原有的代码。
(2)里氏替换原则
里氏替换原则(Liskov Substitution Principle,LSP)中说,任何基类可以出现的地方,子类一定可以出现。里氏替换原则可以理解为是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。
(3)依赖倒置原则
依赖倒置原则(Dependence Inversion Principle,DIP)就是说要依赖于抽象,不要依赖于实现。也即意味着要针对接口编程,不要针对实现编程,具体表现上在应当使用接口和抽象类进行各种类型声明以及数据类型的转换。可以认为开闭原则与依赖倒置原则是目标和手段的关系。如果说开闭原则是目标,依赖倒转原则是到达开闭原则的手段。如果要达到最好的开闭原则,就要尽量的遵守依赖倒置原则。
(4)单一职责原则
单一职责原则(Simple Responsibility Pinciple,SRP)强调一个类应该只有一个职责,并把职责定义为变化的原因。每一个职责都是变化的一个维度,如果一个类有一个以上的职责,这些职责就耦合在了一起,当一个职责发生变化时,可能会影响其它的职责,这就会导致脆弱的设计。另外,多个职责耦合在一起也会影响复用性。如果发现一个类有多于一个的职责,就应该通过分离接口等方式做到尽量解耦。
(5)接口隔离原则
关于隔离的含义就在于类之间的依赖关系应该建立在最小的接口上,一个类不应该依赖它不需用的接口。接口隔离原则(Interface Segregation Principle,ISP)就是说建立单一接口,不要建立臃肿庞大的接口。接口隔离原则与单一职责的审视角度是不相同的,单一职责要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要求接口尽量细化,接口的方法尽量少
以下五条原则一般被称为面向对象领域的S(SRP).O(OCP).L(LSP).I(ISP).D(DIP)原则。
2. 组件设计原则
组件设计原则有时候也称为分包(Package)原则。任何一个软件系统都可以看做是一系列组件的集合,良好的组件设计能够把系统分解为一些大小恰到好处的组件,从而使每个开发团队都可以只关注单个的组件而无需关心整个系统。对于组件而言,最核心的关注点就是内聚(Cohesion)和耦合(Coupling),所谓内聚是指一个模块内各个元素彼此结合的紧密程度,而耦合指的是一个软件结构内不同模块之间互连程度的度量。基于这两个关注点,组件设计原则也包括组件内聚原则(Component Cohesion Principle)和组件耦合原则(Component Coupling Principle)两大类。
组件内聚原则包括:
(1)重用-发布等价原则
重用-发布等价原则(Release-Reuse Equivalency Principle,REP)关注粒度,强调重用的粒度等于发布的粒度。重用-发布等价思想从用户观点的角度上为我们规范了组件设计的原则:在设计组件时,组件中应该包含的元素要么都可以重用,要么都不可以重用。
(2)共同封闭原则
共同封闭原则(Common Closure Principle,CCP)关注变化,即一个组件不应该包括多个引起变化的原因。组件中所有类对同一种性质的变化是共同封闭的,一个变化如果对一个封闭的组件产生影响,则将对该组件中的所有类产生影响,但对其他组件将不产生影响。该原则类似开放封闭原则,即对修改应该是封闭的,但对扩展应该是开放的。从这个角度看,组件越大越能满足共同封闭原则。
(3)共同重用原则
共同重用原则(Common Reuse Principle,CRP)关注重用,认为一个组件中的所有类应该是共同重用的,如果重用了组件中的一个类就应该重用组件中的所有类。即放入一个组件中的类是不可分开的,仅仅依赖其中一部分类的情况不应该存在。显然,根据共同重用原则,组件应该越小越好。
我们可以明显看到共同封闭原则和共同重用原则具有互斥性,不同的原则面向不同的场景和生命周期。同样,组件耦合原则也包含以下三条设计原则:
(4)无环依赖原则
无环依赖原则(Acyclic Dependencies Principle,ADP)认为在组件之间不应该存在循环依赖关系。通过将系统划分为不同的可发布组件,对某一个组件的修改所产生的影响不应该必须扩展到其他组件。我们在后续的4.4.1节架构模式中会进一步探讨当组件之间存在循环依赖时如何进行分析和消除。
(5)稳定抽象原则
稳定抽象原则(Stable Abstractions Principle,SAP)认为组件的抽象程度应该与其稳定程度保持一致。即一个稳定的组件应该也是抽象的,这样该组件的稳定性就不会无法扩展。另一方面,一个不稳定的组件应该是具体的,因为他的不稳定性使其内部代码更易于修改。
(6)稳定依赖原则
稳定依赖原则(Stable Dependencies Principle,SDP)认为被依赖者应该比依赖者更稳定。一个好的设计中的组件之间的依赖应该朝着稳定的方向进行。一个组件只应该依赖那些比自己更稳定的组件。在下图中,我们认为组件X是稳定的,因为X被很多其他组件依赖,相当于责任担当着。而X没有依赖别的包,所有它具备很高的独立性。同时,我们认为组件Y是不稳定的,因为Y没有被其他的组件所依赖,但Y自身依赖很多别的组件。
3. 其他原则
除了面向对象和组件设计原则,业界还存在一批具有代表性的设计原则,包括:
(1)合成/聚合复用原则
合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)强调在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,而新的对象通过向这些对象的委派达到复用这些对象的目的。也即推荐首先使用合成/聚合,合成/聚合则使系统灵活,其次才考虑继承,达到复用的目的。而使用继承时,要严格遵循里氏替换原则。有效地使用继承会有助于对问题的理解,降低复杂度,而滥用继承会增加系统构建、维护时的难度及系统的复杂度。所以合成/聚合复用原则在很多时候就体现为一句话,即组合由于继承。
(2)迪米特法则
迪米特法则(Law of Demeter,LoD)认为一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展也会相对容易。这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度。如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。设计模式中的门面模式和调停者模式实际上就是迪米特法则的具体应用。
(3)命令-查询分离原则
当一个方法返回一个值来响应一个请求,它就具有查询(Query)的性质;当一个方法要改变对象的状态,它就具有命令(Command)的性质。通常,一个方法可能是纯的Command模式或者是纯的Query模式,或者是两者的混合体。在设计接口时,如果可能,应该尽量使接口单一化,保证方法的行为严格的是命令或者是查询,这样查询方法不会改变对象的状态,没有副作用,而会改变对象的状态的方法不可能有返回值。这就是命令-查询分离(Command-Query Separation,CQS)原则。查询功能和命令功能的分离,有助于系统性能,也有利于系统的安全性。
(4)惯例优于配置原则
惯例优于配置(Convention over Configuration,CoC)原则简单讲就是将一些公认的配置方式和信息作为内部缺省的规则来使用。我们的应用只需要指定惯例外的信息即可,从而减少了大量配置信息,在大型系统中,过多的配置信息很多时候已经成为影响开发效率的一个源头。目前流行的微服务开发框架SpringBoot就是该原则的典型表现。
(5)关注点分离原则
关注点分离(Separation of Concerns,SoC)原则就是指在软件开发中,通过各种手段将问题的各个关注点分开。实现关注点分离的方法主要有两种,一种是标准化,另一种是抽象与包装。标准化就是制定一套标准,让使用者都遵守它,这样使用标准的人就不用担心别人会有很多种不同的实现,使自己的程序不能和别人的兼容。另一方面,不断地把程序的某些部分抽象并包装起来,也是实现关注点分离的好方法。诸如组件、分层、面向服务等概念都是在不同的层次上做抽象和包装以使得使用者不用关心它的内部实现细节。
如果对文章感兴趣,可以关注我的微信公众号:程序员向架构师转型,或扫描下面的二维码。
我出版了《系统架构设计:程序员向架构师转型之路》、《向技术管理者转型:软件开发人员跨越行业、技术、管理的转型思维与实践》、《微服务设计原理与架构》、《微服务架构实战》等书籍,并翻译有《深入RabbitMQ》和《Spring5响应式编程实战》,欢迎交流。