目录
2.1. 单一职责原则(Single Responsibility Principle)SRP
2.2. 开放封闭原则(Open - ClosedPrinciple)OCP
2.3. 里氏替换原则(Liskov Substitution Principle)LSP
2.4. 接口隔离法则(Interface Segregation Principle)ISL
2.5. 依赖倒置原则(Dependency-Inversion Principle)DIP
3.2. 合成/聚合复用原则(Composite/Aggregate ReusePrinciple)CARP
一. 前言
在软件开发中,前人对软件系统的设计和开发总结了一些原则和模式, 不管用什么语言做开发,都将对我们系统设计和开发提供指导意义,在满足需求且不破坏系统稳定性的前提下保持高可扩展性、高内聚、低耦合。形成灵活、稳定的系统结构。本文主要将总结这些常见的原则,和具体阐述意义。
面向对象的基本原则(SOLID,又被尊称为软件开发原则)是五个,但是在经常被提到的除了单一职责原则、开放封闭原则、里氏替换原则、依赖倒置原则、接口隔离原则这五个之外还有迪米特法则和合成复用原则等, 所以在常见的文章中有写成六大或七大原则的。
二. 五大基本开发原则
2.1. 单一职责原则(Single Responsibility Principle)SRP
Single-Responsibility Principle,一个类最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合,高内聚在面向对象原则的引申,将职责定义为引起变化的原因,以提高内聚性减少引起变化的原因。
原则分析:
- 一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性越小,而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。
- 类的职责主要包括两个方面:数据职责和行为职责,数据职责通过其属性来体现,而行为职责通过其方法来体现。
- 单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
优点:
- 降低类的复杂性,类的职责清晰明确。比如数据职责和行为职责清晰明确。
- 提高类的可读性和维护性。
- 变更引起的风险减低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的类有影响,对其他接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
注意:
单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否合理,但是“职责”和“变化原因”都是没有具体标准的,一个类到底要负责那些职责? 这些职责怎么细化? 细化后是否都要有一个接口或类? 这些都需从实际的情况考虑。因项目而异,因环境而异。
SpringMVC 中 Entity、DAO、Service、Controller、Util 等的分离就是单一职责的标准体现。
2.2. 开放封闭原则(Open - ClosedPrinciple)OCP
Open-ClosedPrinciple,OCP,对扩展开放,对修改关闭(设计模式的核心原则)。一个软件实体(如类、模块和函数)应该对扩展开放,对修改关闭。意思是,在一个系统或者模块中,对于扩展是开放的,对于修改是关闭的,一个好的系统是在不修改源代码的情况下,可以扩展你的功能。而实现开闭原则的关键就是抽象化。
原则分析
- 当软件实体因需求要变化时,尽量通过扩展已有软件实体,可以提供新的行为,以满足对软件的新的需求,而不是修改已有的代码,使变化中的软件有一定的适应性和灵活性。已有软件模块,特别是最重要的抽象层模块不能再修改,这使变化中的软件系统有一定的稳定性和延续性。
- 实现开闭原则的关键就是抽象化:在"开-闭"原则中,不允许修改的是抽象的类或者接口,允许扩展的是具体的实现类,抽象类和接口在"开-闭"原则中扮演着极其重要的角色:即要预知可能变化的需求;又预见所有可能已知的扩展。所以在这里"抽象化"是关键!
- 可变性的封闭原则:找到系统的可变因素,将它封装起来,这是对"开-闭"原则最好的实现。不要把你的可变因素放在多个类中,或者散落在程序的各个角落。你应该将可变的因素封套起来,并且切忌不要把所用的可变因素封套在一起。最好的解决办法是,分块封套你的可变因素!避免超大类、超长类、超长方法的出现!给你的程序增加艺术气息,将程序艺术化是我们的目标!
设计模式中模板方法模式和观察者模式都是开闭原则的极好体现。
2.3. 里氏替换原则(Liskov Substitution Principle)LSP
Liskov Substitution Principle,LSP:任何基类可以出现的地方,子类也可以出现;这一思想表现为对继承机制的约束规范,只有子类能够替换其基类时,才能够保证系统在运行期内识别子类,这是保证继承复用的基础。
定义
- 第一种定义方式相对严格:如果对每一个类型为 S 的对象 o1,都有类型为 T 的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有变化,那么类型 S 是类型 T 的子类型。
- 第二种更容易理解的定义方式:所有引用基类(父类)的地方必须能透明地使用其子类的对象。即子类能够必须能够替换基类能够从出现的地方。子类也能在基类的基础上新增行为。 (里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士、麻省理工学院教授BarbaraLiskov 和卡内基.梅隆大学 Jeannette Wing 教授于1994年提出。其原文如下: Let q(x) be a property provableabout objects x of type T. Then q(y) should be true for objects y of type Swhere S is a subtype of T. )。
原则分析
- 讲的是基类和子类的关系,只有这种关系存在时,里氏代换原则才存在。正方形是长方形是理解里氏代换原则的经典例子。
- 里氏代换原则可以通俗表述为:在软件中如果能够使用基类对象,那么一定能够使用其子类对象。把基类都替换成它的子类,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类的话,那么它不一定能够使用基类。
- 里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
2.4. 接口隔离法则(Interface Segregation Principle)ISL
Interface Segregation Principle,ISL:客户端不应该依赖那些它不需要的接口。(这个法则与迪米特法则是相通的)。另一种定义方法:一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。 注意,在该定义中的接口指的是所定义的方法。例如外面调用某个类的 public 方法。这个方法对外就是接口。
原则分析
- 接口隔离原则是指使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。
- 一个接口就只代表一个角色,每个角色都有它特定的一个接口,此时这个原则可以叫做“角色隔离原则”。
- 接口仅仅提供客户端需要的行为,即所需的方法,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。
- 使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下,接口中的方法越少越好。
- 可以在进行系统设计时采用定制服务的方式,即为不同的客户端提供宽窄不同的接口,只提供用户需要的行为,而隐藏用户不需要的行为。
2.5. 依赖倒置原则(Dependency-Inversion Principle)DIP
Dependency-Inversion Principle 要依赖抽象,而不要依赖具体的实现,具体而言就是高层模块不依赖于底层模块,二者共同依赖于抽象。抽象不依赖于具体,具体依赖于抽象。
定义
依赖倒置也是著名的好莱坞法则:高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。简单的说,依赖倒置原则要求客户端依赖于抽象耦合。原则表述:
- 抽象不应当依赖于细节,细节应当依赖于抽象;
- 要针对接口编程,不针对实现编程。
原则分析
- 如果说开闭原则是面向对象设计的目标,依赖倒转原则是到达面向设计“开闭”原则的手段。如果要达到最好的“开闭”原则,就要尽量的遵守依赖倒转原则。可以说依赖倒转原则是对“抽象化”的最好规范!我个人感觉,依赖倒转原则也是里氏代换原则的补充。你理解了里氏代换原则,再来理解依赖倒转原则应该是很容易的。
- 依赖倒转原则的常用实现方式之一是在代码中使用抽象类,而将具体类放在配置文件中。类之间的耦合:零耦合关系,具体耦合关系,抽象耦合关系。
- 依赖倒转原则要求客户端依赖于抽象耦合,以抽象方式耦合是依赖倒转原则的关键。
三. 扩展原则
3.1. 迪米特法则(Law of Demeter)LoD
Law of Demeter,LoD:系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度。
定义
又叫最少知识原则(Least Knowledge Principle 或简写为 LKP)几种形式定义:
- 不要和“陌生人”说话。英文定义为:Don't talk to strangers.
- 只与你的直接朋友通信。英文定义为:Talk only to your immediate friends.
- 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
简单地说,也就是一个对象应当对其它对象有尽可能少的了解。一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的 public 方法,我就调用这么多,其他的一概不关心。
注意
系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度,因为在你的系统中,扩展的时候,你可能需要修改这些类,而类与类之间的关系,决定了修改的复杂度,相互作用越多,则修改难度就越大,反之,如果相互作用的越小,则修改起来的难度就越小。例如 A 类依赖 B 类,则 B类依赖 C 类,当你在修改 A 类的时候,你要考虑 B 类是否会受到影响,而 B 类的影响是否又会影响到 C 类,如果此时 C 类再依赖 D 类的话,呵呵,我想这样的修改有的受了。
设计模式中外观模式(Facade)和中介者模式(Mediator)都是迪米特法则的极好体现。
3.2. 合成/聚合复用原则(Composite/Aggregate ReusePrinciple)CARP
Composite/Aggregate ReusePrinciple,CARP 经常又叫做合成复用原则(Composite ReusePrinciple或CRP):要尽量使用对象组合,而不是继承关系达到软件复用的目的。就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的。简而言之,要尽量使用合成/聚合,尽量不要使用继承。
原则分析
- 在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承。
- 继承复用:实现简单,易于扩展。破坏系统的封装性;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;只能在有限的环境中使用。(“白箱”复用)
- 组合/聚合复用:耦合度相对较低,选择性地调用成员对象的操作;可以在运行时动态进行。(“黑箱”复用)
- 组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏替换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
- 此原则和里氏替换原则是相辅相成的,两者都是具体实现"开-闭"原则的规范。违反这一原则,就无法实现"开-闭"原则。
聚合和组合的区别
- 合成(组合):表示一个整体与部分的关系,指一个依托整体而存在的关系(整体与部分不可以分开);比如眼睛和嘴对于头来说就是组合关系,没有了头就没有眼睛和嘴,它们是不可分割的。在 UML 中,组合关系用带实心菱形的直线表示。
- 聚合:聚合是比合成关系的一种更强的依赖关系,也表示整体与部分的关系(整体与部分可以分开);比如螺丝和汽车玩具的关系,螺丝脱离玩具依然可以用在其它设备之上。在 UML中,聚合关系用带空心菱形的直线表示。
本文详细介绍了面向对象设计的五大基本原则(SOLID):单一职责原则、开放封闭原则、里氏替换原则、接口隔离原则和依赖倒置原则,以及两个扩展原则——迪米特法则和合成/聚合复用原则。这些原则旨在提高软件的可维护性、可扩展性和低耦合性,通过实例和设计模式展示了它们的应用和重要性。
1076





