设计模式(一)——设计模式初窥,七大设计原则

这是我根据学习时的学习笔记,如有错误请在评论区指出,谢谢!

设计模式初窥

设计模式功能

设计模式存在两种划分方式

  1. 根据目的

    1. 创建型模式:描述“如何创建一个对象”,将对象的创建和使用分离

    2. 结构型模式:用于指导如何将类和对象组织成复杂的巨大的结构

    3. 行为型模式:用于描述类或对象之间如何相互协作从而实现单个对象无法完成或不应该由单个对象完成的任务

  2. 根据作用对象

    1. 类模式:用于处理 类和子类之间的关系 ,这些关系通过继承来建立,是静态的。
    2. 对象模式:用于处理 对象间的关系,这些关系通过组合或聚合来实现,在运行中可变,具有动态性。
范围\目的 创建型模式 结构型模式 行为型模式
类模式 工厂方法 (类)适配器 模板方法、解释器
对象模式 单例 原型 抽象工厂 建造者 代理 (对象)适配器 桥接 装饰 外观 享元 组合 策略 命令 职责链 状态 观察者 中介者 迭代器 访问者 备忘录

上表体现了两种划分方式下,23种设计模式的划分。

23种设计模式的功能简介

GoF的设计模式中一共23种:

  1. 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。

  2. 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。

  3. 工厂方法(Factory Method)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。

  4. 抽象工厂(Abstract Factory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。

  5. 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。

  6. 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。

  7. 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。

  8. 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度 (有点抽象,之后详细来看)。

  9. 装饰(Decorator)模式:动态的给对象增加一些职责,即增加其额外的功能。

  10. 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。

  11. 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用(有点抽象,之后详细看)。

  12. 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。

  13. 模板方法(TemplateMethod)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

  14. 策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。

  15. 命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。

  16. 职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。

  17. 状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。

  18. 观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。

  19. 中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。

  20. 迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。

  21. 访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。

  22. 备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。

  23. 解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。

七大设计原则

在软件开发中,为了提高可维护性和可复用性,增强软件的可拓展性和灵活性,程序员应当遵循七条原则,以提高开发的效率以及降低维护和迭代难度。我不仅将在这里介绍这七条原则的内容,更重要的是,尽可能讲清楚为什么要遵循这样的规则以及在实际开发中能够带来的好处

这七条原则包括:

  • 开闭原则

  • 里氏替换原则

  • 依赖倒置原则

  • 单一职责原则

  • 接口隔离原则

  • 迪米特法则

  • 合成复用原则

开闭原则

开闭原则(Open Closed Principle,aka OCP),主要内容为:

软件实体应当对拓展开放,而对修改关闭

软件实体,指的是项目中的模块、或者一个类、或者一个接口、抑或是一个方法。实质是指 程序能够在需求改变是不修改软件实体之前的代码而仅通过拓展使其满足需求

开闭原则在软件开发过程中的作用是明显的,便按下不表,其真正让人困惑的是如何才能优雅地实现开闭原则。而实现开闭原则的重点在于一句话“抽象约束,封装变化”。

抽象约束,指抽象类或接口这一类相对较为抽象的软件实体应当被定义成一个稳定的抽象层,换句话说,尽可能让抽象类能够在迭代过程中稳定而不需要修改,即约束抽象软件实体的可变性。封装变化,指针对不同需求的变化,应当被封装在具体类中,即将可变的因素封装在具体软件实体中,而不应该影响抽象层,使得面对新的需求只需要新派生一个具体类即可而无需改变抽象类。

里氏替换原则

里氏替换原则(Liskov Substitution Principle, aka LSP),其主要内容为:

继承必须确保超类拥有的性质在子类中依然成立

可以说,里氏替换原则是继承复用关系的基础,决定了继承是否应当被使用,是实现开闭原则的具体规范。

里氏替换原则的作用在于 克服了继承中重写父类所导致可复用性变差的缺点,同时也 确保类的继承拓展不会为已有的系统引入新的错误,提高了健壮性

就实现而言,里氏替换原则就可以理解为:子类只拓展父类而不改变父类已有的功能,即避免Override使得父类的性质在子类中不再成立。具体实现中遵守:

  1. 子类能够实现父类的抽象方法,而不覆盖非抽象方法

  2. 子类可以增加特有的方法

  3. 子类重载父类方法时要求方法的前置条件更为宽松,即为调用该方法需要更简单,使得父类同样能调用该方法。可以理解为传入子类的参数范围是父类参数范围的子集,无论是数量上还是具体的值上。

  4. 子类重载或实现父类方法时的后置条件更加严格,即返回值和退出时的状态约束在业务逻辑上应当严格属于父类方法返回值与退出状态的子集。

印象最深刻的就是“正方形不是长方形的子类”这个样例,因为正方形更加严格。长方形允许修改一边长而另一边不变,但是正方形则会同时改变两边。当用户修改长方形对象A时,会先修改其长再修改其宽,但是对于正方形来说,第二次修改会覆盖前一次修改。这可以看作违反了第三条,因为长方形修改长或宽的函数的退出状态是“仅修改对应的边”,而正方形的相应退出状态是“既修改了长也修改了宽”,显然不是前者的子集。

依赖倒置原则原则

依赖倒置原则(Dependence Inversion Principle,aka DIP),其主要描述抽象的依赖。主要内容为:

高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖细节,而应该让细节依赖抽象

“面向接口编程,而不应该面向实现编程”,可以总结其核心思想。实际上,依赖倒置原则指出的一个非常容易理解的事实:软件实现的细节根据需求而变化,但是抽象层应该保持相对的稳定,这与开闭原则的思想有所重叠,也是为什么“细节应当依赖抽象”的原因。在这里,我们将抽象定义为接口和抽象类,将细节定义为具体类及其方法。

依赖倒置的主要作用和开闭原则有所相似,但是其更多地指导类的继承关系和实现:

  1. 降低耦合性,提高可维护性

  2. 减少并行开发带来的风险

依赖倒置原则具体的实现,体现在两个方面:

  1. 抽象软件实体的设计:设计更加稳定和全面的抽象
  2. 具体实现类的依赖设计:依赖抽象,具象来理解,即调用具体类的方法时所传入的参数必须是抽象的,而不应该是具体类。且在具体方法的实现中,应当调用抽象层的方法而不应该依赖具体类的方法

单一职责原则

单一职责原则(Single Responsibility Principle,aka SRP)。单一职责原则规定了类本身的设计规范,要求:

规定一个类应该有且只有一个引起它变化的原因,否则类应该被拆分

引起类变化,即改变该类的属性。单一职责要求一个类改变的原因只应该有一个。但是就我个人的理解,这个原则希望一个类不应该承担过多的功能,而不是严格要求“改变其属性的原因只有一个”。比如对一个BankAccount类,存和取都会改变其Money属性,但是显然这不应该对其进行拆分。但是比如对一个Worker类,如果其需要完成多个职责(即使这些职责不会改变其属性),也最好进行拆分以提高可读性。

单一职责原则明显降低了类的复杂度,提高了程序的可读性,也提高了后期维护的安全性(避免改动一个极为复杂的类)。

实现上来说,核心在于如何对职责进行拆分,如何确定多重职责的负担是否过重,这需要程序员的经验。当这些职责被拆分后,可以以接口方式进行继承到具体类。同时要注意到,即使是同一种功能如果面对特定对象属性时需要实现不同的功能,也应当进行拆分(比如传入一个bool值控制两种不同的业务逻辑,可以理解为特定对象属性实际上是不同的引发变化的原因)。

接口隔离原则

接口隔离原则(Interface Segregation Principle,aka ISP),针对接口的设计进行规范,主要内容包括:

一个类对另一个类的依赖应该建立在最小的接口上

这句话怎么理解呢?什么是一个类对另一个类的依赖?什么又是最小的接口?首先这里的类实际上包括类\抽象类\接口,而最小的接口表示实现单一功能所需的最小接口。这句话翻译翻译就是,任何一个接口都不应该过大,而应该尽量对每个具体行为单独实现一个接口(此时其为最小接口),然后由多个最小接口组成一个较大的接口。

接口隔离原则约束了接口,降低了类对接口的依赖性(因为不会有多个类同时依赖一个巨大的接口,而是各自依赖于最小接口),带来的好处有:

  1. 防止在一个较为简单的类中被迫实现一个巨大的接口内的所有方法而实际上用不到。

  2. 提高了内聚性,将结构关系转移到了接口继承上,在java中更为灵活

为了实现接口隔离原则,需要注意以下几点:

  1. 接口要尽可能小,但是要有限度,使得一个接口能够对应一个完全的业务逻辑

  2. 深入了解业务逻辑,减少对外交互,使得接口用最少方法实现最多的事情

迪米特法则

迪米特法则(Law of Demeter,aka LoD),又称最少知识原则。其规范了软件实体的交互。主要内容为:

若两个软件实体无须直接通信,那么就不要发生直接的相互调用,可以通过第三方进行转发

原话更为通俗:“只和你的挚友说话,不要和陌生人交谈”

挚友(immediate friend),指的是对象本身、对象的成员对象、该对象创建的对象、当前对象的方法的参数。对象和其挚友间存在很强的关联关系。

直接通信,指在该A对象中采用成员变量、方法参数、方法返回值等方式引用另一个B对象。直接调用的话,则指在A的方法中以局部变量方式调用另一个不属于以上挚友范围内的“陌生类”B。第三方转发,即让一个类C同时成为A和B的挚友,然后在所有在A和B中直接调用对方的地方使用C对象,然后利用C调用A或者B。

迪米特法则看起来好像没用,反而增加了类内方法实现的难度(因为新增了第三方类,从而需要去实现第三方类的方法)。事实上,迪米特法则确实需要适度使用以防止中介类过多。但是瑕不掩瑜,其大大降低类之间的耦合性,让耦合度高的几个类群相交互而耦合度低的类群分隔开来。以个人实践上来看,这不失于一种切割复杂耦合的好方式。

实现上:

  1. 对依赖者,其只依赖应该依赖的对象而不应该在类内使用陌生类

  2. 对被依赖者,其只应该暴露应该暴露的方法,而避免暴露过多方法适当类之间耦合过多。

  3. 类的设计上要遵守:

    • 类的结构上,优先考虑设计为不变类、降低类成员的访问权限、尽可能弱耦合,不要依赖过多的其他类。

    • 不保留类的属性成员而使用set和get

    • 少引用其他类(挚友尽可能少)

合成复用原则

合成复用原则(Composite Reuse Principle,CRP),又称组合复用原则。其规范的主要是类之间的关系,尤其是组合或聚合关系。

要求优先使用组合或聚合方式进行复用,而其次才应该是继承

组合或聚合,即A类设一个B类成员,从而调用B类成员的方法,实现复用。使用组合和聚合与使用继承的不同在于:

  1. 继承会导致父类暴露给子类,使得父类的不再透明。组合和聚合只是调用,被调用的类仍然是黑箱。

  2. 父类的改变会引起子类的变化。但是组合和聚合中只需要暴露出来的方法不变即可。

  3. 从父类继承的方法是静态的,无法在运行时进行改变。组合和聚合中可以在运行中根据情况进行动态调用。

实现上,组合和聚合关系利用将已有的对象纳入新对象找那个,从而作为新对象的成员对象实现,新对象可以调用已有对象的功能从而复用。

posted @ 2021-03-26 15:14  neumy  阅读(99)  评论(0编辑  收藏  举报