1.六大设计原则
记住一个单词:SOLID
- S(SRP):单一职责原则:字如其名,类只负责一类功能
- O(OCP):开放封闭原则:扩展开放、修改封闭
- L(LSP):里氏替换原则:子类能够透明的使用父类的方法
- I(ISP):接口隔离原则:接口最小化,比如java中的函数式接口
- D(DIP):依赖倒置原则:针对抽象编程,别依赖实现类,多利用多态
- 此外还有一个迪米特法则(LoD):只与朋友进行交流,朋友包括:
- 该对象本身
- 作为方法的参数传递进来的对象
- 作为方法的参数传递进来的对象
- 该对象的组件对象(成员变量)
1.1 单一职责原则(Single Responsibility Principle)
1.1.1 概念
描述
接口应该只负责一项任务,以便接口可以更好的被复用。
优点
- 高内聚性:使类的责任变得清晰明确,每个类只关注一项功能或职责,从而提高了类的内聚性。
- 可复用性:类的功能单一更容易被其他类复用。
- 可扩展性:当系统需要进行修改或扩展时,单一职责原则使得修改的范围更加明确。如果一个类只有一个职责,那么只需要修改与该职责相关的代码,不会影响到其他功能的实现。
- 降低耦合性:当一个类承担多个职责时,不同职责之间可能存在相互依赖的关系,导致耦合度增加。而遵循单一职责原则可以将不同的职责分离开来,减少类之间的依赖关系,降低耦合性。
缺点
- 类的数量增加:功能越多,接口越多。
- 代码的分散性:代码的查找和理解的难度较大。
- 维护成本增加:当需要修改某个功能时,可能需要同时修改多个类,增加了维护的难度。
1.2 接口隔离原则(Interface Segregation Principle)
描述
客户端不应该依赖它不需要的接口。因此不建议把乱七八糟的接口都写在一个interface中。
优点
- 减少对不相关功能的依赖:接口隔离原则要求将大型接口拆分成多个小的、相关的接口,使得客户端只需要依赖与其需要的接口,而不需要依赖整个接口集合。这样可以减少对不相关功能的依赖,降低耦合度,提高系统的灵活性和可维护性。
- 接口精简明确:通过隔离接口,每个接口都具有明确的目的和职责,不再承担多个功能。这样可以使接口的设计更加精简和明确,提高了接口的可读性和可理解性。
- 支持接口的复用和扩展:通过细化接口,可以更方便地复用和扩展接口。当需要新增一个功能时,只需要在现有的接口基础上添加新的接口即可,而不需要修改已有的接口,从而避免了对现有代码的影响。
缺点(同单一职责原则)
- 类的数量增加:功能越多,接口越多。
- 代码的分散性:代码的查找和理解的难度较大。
- 维护成本增加:当需要修改某个功能时,可能需要同时修改多个类,增加了维护的难度。
1.3 依赖倒置原则(Dependency Inversion Principle)
描述
针对抽象编程。高层模块不应该依赖低层模块的具体实现,而应该依赖于抽象。
优点
- 解耦合:依赖倒置原则可以减少模块之间的直接依赖关系,从而实现解耦合。通过引入抽象层,高层模块和低层模块都依赖于抽象,而不是具体的实现类,使得模块之间的耦合度降低,提高了系统的灵活性和可维护性。
- 可替代性:由于高层模块依赖于抽象,而不是具体的实现类,因此可以很容易地替换具体的实现类,以适应不同的需求。这使得系统更加可扩展和可定制。
- 测试和调试的便利性:依赖倒置原则利于进行单元测试,因为可以通过使用抽象的实现类进行模拟或替代,从而更容易对高层模块进行测试和调试。
- 提高代码的可读性和可理解性:依赖倒置原则使代码结构更清晰,模块之间的依赖关系更明确,从而提高了代码的可读性和可理解性。
缺点
- 引入抽象层增加复杂性:为了实现依赖倒置原则,需要引入抽象层或接口,这增加了系统的复杂性。需要设计和管理抽象层及其实现类之间的关系,增加了代码量和开发工作量。
- 需要设计良好的抽象层次:为了实现依赖倒置原则,需要设计良好的抽象层次,确保抽象层能够满足系统的需求,并且不会引入过多的复杂性和不必要的抽象。
1.4 里氏替换原则(Liskov Substitution Principle)
描述
子类对象能透明的使用父类的方法。
优点
- 提高代码的可重用性:遵循里氏替换原则可以使得子类对象能够替换父类对象,从而提高代码的可重用性。通过继承和多态的机制,可以在不修改现有代码的情况下扩展和定制功能。
- 提高系统的可扩展性:通过符合里氏替换原则的设计,当需要新增子类时,只需要编写新的子类,而不需要修改已有的代码。这样可以方便地扩展系统的功能,同时不影响已有功能的正确性。
- 支持多态特性:里氏替换原则是实现多态的基础,多态使得代码更加灵活和可扩展,能够根据不同的对象类型调用相应的方法,增加了代码的可读性和可维护性。
缺点
- 适用范围有限:里氏替换原则并不适用于所有情况,它需要满足一定的前提条件,例如子类对象必须完全实现父类对象的行为约定。如果子类对象违反了父类对象的行为约定,可能导致程序出现错误或不可预测的行为。
- 增加继承的复杂性:继承是实现里氏替换原则的一种常见方式,但过度使用继承可能导致继承层次过深或过复杂,增加代码的复杂性和理解的难度。过度的继承关系还可能导致父类的改动影响到多个子类,降低了系统的灵活性。
- 设计约束:为了满足里氏替换原则,需要对类之间的继承关系进行仔细设计和抽象。这可能增加设计和开发的复杂性,需要权衡继承关系的合理性和稳定性。
1.5 开闭原则(Open-Closed Principle)
描述
对扩展(使用组合或继承进行功能增强)开放,对修改(对现有类进行修改)关闭
优点
- 可扩展性:遵循开闭原则可以使系统更容易进行扩展。当需要新增功能时,可以通过添加新的实体(类、模块等),而不需要修改已有的代码。这样可以避免修改现有代码带来的风险和影响。
- 可维护性:开闭原则有助于提高系统的可维护性。通过将变化的部分与稳定的部分分离开来,可以更方便地理解、修改和测试代码。只需要关注新增的实体,而不需要担心对现有代码的影响。
- 可复用性:开闭原则促进了代码的复用。通过定义抽象的接口或基类,并在其基础上进行扩展,可以实现更多的复用。新的实体可以通过继承或实现抽象接口来利用现有的代码逻辑。
缺点
- 抽象设计的复杂性:为了满足开闭原则,需要进行抽象设计和接口定义,这可能增加代码的复杂性和开发工作量。需要权衡抽象层次和接口设计的合理性,避免过度设计。
- 需要预见变化:开闭原则要求在设计初期就要考虑可能的扩展和变化,需要有一定的预见性。如果对系统未来的变化了解不够充分,可能会导致设计上的困难和冗余的抽象。
- 可能引入过度的灵活性:为了实现开闭原则,可能会引入过度的抽象和灵活性。这可能会增加代码的复杂性和理解的难度,同时也可能影响性能。
1.6 迪米特法则(Law of Demeter)
描述
也称为最少知识原则(Principle of Least Knowledge)。
它强调一个对象应该尽可能减少与其他对象之间的直接交互,只与直接的朋友(类自己、传入的参数、创建的对象、部件对象)进行通信。
优点
- 降低耦合度:迪米特法则可以减少对象之间的直接依赖关系,降低耦合度。对象只与其直接的朋友进行通信,不需要了解其他对象的具体实现细节,从而减少了对象之间的耦合程度,提高了系统的灵活性和可维护性。
- 提高模块的独立性:通过限制对象之间的交互,迪米特法则可以提高模块的独立性。一个模块的修改不会对其他模块产生太大的影响,降低了修改的风险和维护的难度。
- 提高代码的可读性和可理解性:迪米特法则要求对象只与其直接的朋友进行通信,减少了对象之间的关联,使得代码结构更加清晰和易于理解。
缺点
- 增加了间接性:迪米特法则要求对象只与直接的朋友通信,而不直接与其他对象交互。这可能导致消息的传递变得间接,增加了代码的复杂性和理解的难度。
- 可能引入过多的中间类:为了满足迪米特法则,可能需要引入更多的中间类来封装对象之间的通信,这可能增加代码的复杂性和开发工作量。
- 难以权衡:在实践中,迪米特法则要求在设计和编码过程中要有清晰的意识,并进行合理的设计。然而,在某些情况下,过度追求迪米特法则可能会导致过度的封装和间接性,增加了设计和开发的复杂性。
2.设计思想
2.1 合成复用原则
多用组合,少用继承(强耦合)
为什么要多用组合少用继承?
因为耦合度由低到高:关联(单向关联、双向关联、自关联)<依赖<聚合<组合<继承,使用组合是为了满足高内聚低耦合的设计理念。
谈到这里了,让我们一起了解一下继承和组合的优缺点吧。
继承的优点:
- 代码重用:子类可以继承父类的属性和方法,从而避免了重复编写相同的代码。
- 继承关系表达:通过继承可以表达类之间的层次关系,体现出共性和特性。
继承的缺点:
- 紧密耦合:子类与父类之间存在紧密的耦合关系,父类的变化可能会影响到子类的实现。
- 层次复杂性:继承层次过深或过复杂可能导致代码难以理解和维护。
- 运行时限制:继承关系在编译时就已经确定,限制了类的灵活性和动态性。
组合的优点:
- 松散耦合:通过组合关系,对象之间的依赖关系较为松散,一个对象的变化不会直接影响到其他对象。
- 灵活性:通过组合可以在运行时动态地改变对象之间的关系,更加灵活地组织和配置对象。
- 可替代性:对象之间的组合关系可以更容易地替换和重组,提供了更大的灵活性和可扩展性。
组合的缺点:
- 代码复杂性:使用组合需要更多的代码来实现对象之间的关系和交互。
- 可见性限制:组合模式可能会限制对某些对象的直接访问和调用,需要通过其他对象来进行间接访问。
了解了继承的组合的关系后,什么时候使用组合,什么时候使用继承呢?
优先使用组合,因为组合关系更加灵活、松散耦合,可以避免继承带来的一些问题。
当子类需要表达明确的"is-a"关系时或者需要重用大量相似的代码时使用继承。
组合分为了聚合和组合,他们的区别是什么?
聚合和组合的区别是生命周期不同:
-
聚合的容器和组件可以独立存在。比如图书馆和读者的关系,读者可以独立存在,他们可以在不同的图书馆借书。
-
组合的容器和组件生命周期相同,二者同时存在和销毁。比如图书馆和书架的关系,图书馆包含了书架,当图书馆不存在时,书架也不存在。
2.2 SOLID原则
根据这些原则能够编写易于维护、可扩展和可复用的代码。
2.3 GRASP原则(了解即可)
2.3.1 信息专家
专业的事情交给专业的人做
2.3.2 高内聚
老年机充电器无法拆开。这便是高内聚高耦合。
智能手机的充电器可以将充电头和充电线独立拆开,又能紧密连接起来。这便是高内聚低耦合。
2.3.3 低耦合
类之间存在的依赖数量尽可能少,类之间的关系尽量是低耦合的关系。
2.3.4 间接
类A和类B之间加入一个类C,从此A和B不再直接交互。好处便是解耦。
2.3.5 多态
隐藏子类的差异,可以消除不必要的分支语句。
2.3.6 纯虚构
C++中的定义虚函数。java中定义抽象方法。
2.3.7 受保护变化
设计应该允许系统的变化和不确定性发生时,最小化对其他部分的影响。这可以通过封装变化的部分、使用接口和抽象来实现。
2.3.8 控制器
UI和业务不能直接交互,通过控制器进行交互。
2.3.9 创造者
一个类应该负责创建与之相关的对象。即,如果一个类需要使用另一个类的实例,它应该在创建过程中负责实例化这个类。
3.其它设计原则
- 重用发布等价原则(REP):重用的粒度就是发布的粒度。
- 共同重用原则(CCP):一个包中的所有类应该是共同重用的。如果重用了包中的一个类,那么就要重用包中的所有类。相互之间没有紧密联系的类不应该在同一个包中。.共同封闭原则(CRP):包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包影响,则将对包中的所有类产生影响,而对其他的包不造成任何影响。
- 无依赖原则(ADP):在包的依赖关系中不允许存在环,细节不应该被依赖。
- 稳定依赖原则(SDP):朝着稳定的方向进行依赖。应该把封装系统高层设计的软件(例如抽象类)放进稳定的包中,不稳定的包中应该只包含那些很可能会改变的软件(例如具体类)。
- 稳定抽象原则(SAP):包的抽象程度应该和其他稳定程度一致。一个稳定的包应该也是抽象的,一个不稳定的包应该是具体的。
- 缺省抽象原则(DAP):在接口和实现接口的类之间引入一个抽象类,这个类实现了接口的大部分操作。
- 接口设计原则(IDP):规划一个接口而不是实现一个接口。
- 黑盒原则(BBP):多用类的聚合,少用类的继承。
- 不要构造具体的超类原则(DCSP):避免维护具体的超类。
写在最后
设计模式很重要,但很多人都是一知半解。想成为一个好的开发,掌握底层的东西远比会用框架来的实在。当你能随手设计出符合需求的设计模式时,我想你离架构师只有一步之遥了。
摘录书中的一句话:“在这里需要提醒的是,面向对象设计原则只是一些成功的经验总结。在实际的项目中,需要适度地考虑这些法则,不要为了套用法则而做项目,法则只是一个参考,跳出了这个法则,也不会有人惩罚你,项目也未必一定会失败。不要简单刻意地在项目中为了使用而使用,而要能够慢慢地将其融入自己编程习惯中,实际上,对待设计模式也应该是这个态度。”