通用设计模式

1. 设计模式

1. 设计原则

1. 开闭原则
  • 核心:面对需求,对程序的改动是通过增加新代码进行的,而不是改变原来的代码。
  • 原因:修改原本封装好的代码,可能会导致原本正常的代码变得无法正常运行,而且会变乱
  1. 在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。
  2. 抽象化是开闭原则的关键。
  3. 开闭原则还可以通过一个更加具体的“对可变性封装原则”来描述,对可变性封装原则要求找到系统的可变因素并将其封装起来。
2. 依赖倒转原则
  • 核心:代码要依赖于抽象的类,而不要依赖于具体的类;要针对接口或抽象类编程,而不是针对具体类编程。
  • 原因:依赖的类若发生变动,高级类可能会发生严重问题(牵一发而动全身)
  1. 实现开闭原则的关键是抽象化,并且从抽象化导出具体化实现,如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段。

  2. 依赖倒转原则的常用实现方式之一是在代码中使用抽象类,而将具体类放在配置文件中。

  3. 类之间的耦合

    零耦合关系。
    具体耦合关系。
    抽象耦合关系。

    依赖倒转原则要求客户端依赖于抽象耦合,以抽象方式耦合是依赖倒转原则的关键。

3. 里氏代换原则
  • 核心:有enemy1、enemy2两种类,标记目标成员变量时不应写两份,而是写它们的共同类enemy。
  • 原因:若enemy种类越来越多,不可能依次标记目标成员变量,这有违开闭原则。
  • 软件中如果能够使用父类对象,那么一定能够使用其子类对象。把父类都替换成它的子类,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类的话,那么它不一定能够使用父类。
  • 里氏代换原则是实现开闭原则的重要方式之一,由于使用父类对象的地方都可以使用子类对象,因此在程序中尽量使用父类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
4. 单一职责原则
  • 核心: 一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中,应该仅有一个引起它变化的原因。
  • 原因:一个类承担的职责越多,它被复用的可能性越小,而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。
  • 类的职责主要包括两个方面:数据职责和行为职责,数据职责通过其属性来体现,而行为职责通过其方法来体现。
  • 单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
5. 接口隔离原则
  • 核心:类应该完全依赖相应的专门的接口,不应该依赖那些它不需要的接口。 一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
  1. 接口隔离原则是指使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。
  2. 使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下,接口中的方法越少越好。
  3. 可以在进行系统设计时采用定制服务的方式,即为不同的客户端提供宽窄不同的接口,只提供用户需要的行为,而隐藏用户不需要的行为。
6. 合成复用原则
  • 核心:类中应尽量使用成员变量而不是用继承来达到复用的目的。
  • 原因:enemy1和enemy2各自继承atk方法,若想要让两个enemy使用不同方式的atk,需要增加一个atk类的方法,导致两个enemy继承过来的atk各自多了一个用不到的atk方法,这有违开闭原则,系统扩展性较差。
  1. 合成复用原则就是指在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用其已有功能的目的。简言之:要尽量使用组合/聚合关系,少用继承。
  2. 在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承。
7. 迪米特法则
  • 核心:一个软件实体应当尽可能少的与其他实体发生相互作用。
  • 原因:降低耦合
  1. 迪米特法则就是指一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易,这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度。
  2. 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及
  3. 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限
  4. 在类的设计上,只要有可能,一个类型应当设计成不变类
  5. 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。

2. 设计模式

1. 状态模式
  1. 核心:物体有不同的状态,如敌人有巡逻战斗两种状态,以此为主题分离类。
  2. 概述:当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
  3. 解决的问题:主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同的一系列类当中,可以把复杂的逻辑判断简单化。
  4. 例:类A、B继承state接口,表明A、B两个状态。
    在这里插入图片描述
2. 外观模式
  1. 核心:功能被集成到一个模块下,外部调用其下的子系统时候,只需访问它即可。
  2. 概述:一个子系统的外部与其内部的通信通过一个统一的外观类进行,外观类将客户类与子系统的内部复杂性分隔开,使得客户类只需要与外观角色打交道,而不需要与子系统内部的很多对象打交道。

在这里插入图片描述

3. 单例模式
  1. 核心:全局仅有一个的类(管理类)
  2. 概述:某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
  3. 创建方法:
  1. 创建私有变量,用以记录的唯一实例
  2. 内部进行实例化
  3. 把类的构造方法私有化,不让外部调用构造方法实例化
  4. 定义公有方法提供该类的全局唯一访问点
  5. 外部通过调用静态方法来返回唯一的实例
4. 中介者模式
  1. 核心:多个类在一个集合内,互相调用使用一个“中介者调用”
  2. 概述:定义一个中介对象来封装系列对象之间的交互。中介者使各个对象不需要显示地相互引用,从而使其耦合性松散,而且可以独立地改变他们之间的交互。
    在这里插入图片描述
5. 桥接模式
  1. 核心:类A继承自接口A,实例化时,内部的必要参数B需要继承自接口B的类B,调用时只需要调用接口B的方法,无需关心如何实现。
  2. 概述:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
    存在于多个实体中的共同的概念性联系,就是抽象化。作为一个过程,抽象化就是忽略一些信息,从而把不同的实体当做同样的实体对待。
  • 实现化
    抽象化给出的具体实现,就是实现化。
  • 脱耦
    所谓耦合,就是两个实体的行为的某种强关联。而将它们的强关联去掉,就是耦合的解脱,或称脱耦。在这里,脱耦是指将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联。
    将两个角色之间的继承关系改为聚合关系,就是将它们之间的强关联改换成为弱关联。因此,桥梁模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用组合/聚合关系而不是继承关系,从而使两者可以相对独立地变化。这就是桥梁模式的用意。
    在这里插入图片描述
6. 策略模式
  1. 核心:与桥接模式基本相同。
  2. 区别于桥接模式:

策略模式的关注点主要在于提取算法的抽象,实现不同算法间的替换。
而桥接模式的关注点在于实体和实现的分离,也就是可以相互独立的变化。 (>=策略)

7. 模板方法模式
  1. 核心:事物的一个方法:步骤大致相同,而在某一处特别的地方不同。可使他们继承类A,将那一个步骤拆解,对其特别的地方进行重写。
  2. 概述:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
8. 工厂模式

简单工厂

  1. 核心:不直接New对象而是间接通过工厂类new对象,这样当目标对象的构建发生变化时只需修改工厂,构建方式对用户透明。
  2. 概述:通过工厂模式,将创建产品实例的权利移交工厂,我们不再通过new来创建我们所需的对象,而是通过工厂来获取我们需要的产品。降低了产品使用者与使用者之间的耦合关系。
    在这里插入图片描述
    工厂模式
    有一个总工厂,各自类型的产品由它们的子工厂进行生产,返回给总工厂。
    在这里插入图片描述
    抽象工厂模式
    将类方法继承于接口。
9. 建造者模式
  1. 核心:

    接口规定各个功能模块,
    对应类实现各个功能模块而无需考虑模块之间的调用关系。
    总管理类调用对应类的各个模块完成产品,而无需考虑模块的实现方式。

  2. 概述:是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
    在这里插入图片描述
10. 享元模式
  1. 核心:一个对象池,能利用池子里的就不创建新的。但享元模式对象可被多个对象同时引用,需要改变时才用新的、其他对象
  2. 概述:减少创建对象的数量,以减少内存占用和提高性能。尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。
11. 组合模式
  1. 核心:层级式树状结构
  2. 概述:把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。它创建了对象组的树形结构。
    组合模式有三个基本部件: 即抽象部件、叶子部件、树枝部件,这三个部件组成的一颗树形结构。

    抽象构件(Component):叶子构件和容器构件继承的抽象类,包含叶子和容器的共有行为的声明,如业务方法,也可能包含管理叶子的方法。
    叶子构件(Leaf):树的叶结点,实现抽象构件的业务方法,对于容器特有的管理子结点的方法,可以使用空方法或者抛出异常处理。
    树枝构件(Composite):树的叶结点,实现抽象构件的业务方法,对于容器特有的管理子结点的方法,可以使用空方法或者抛出异常处理。

在这里插入图片描述

12. 命令模式
  1. 核心:命令并不立刻执行,而是通过命令类间接执行。这解除了指令与执行对象的耦合,转而让命令类自身决定如何完成命令。
    类似于工厂模式。工厂模式是构造,负责创建某种类型的实例。命令模式是行为,定义了需要执行的特定操作,这些操作可能是若干个,比一次性的函数使用更加灵活。
  2. 概述:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化; 对请求排队或记录请求日志,以及支持可撤销的操作。
13. 责任链模式
  1. 核心:对象A解决问题访问B,B解决不了找B中的引用C,依次直到找到解决办法为止。
  2. 概述:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
14. 观察者模式
  1. 核心:event模式,订阅事件监听以获得更新消息。
  2. 概述:定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
15. 备忘录模式
  1. 核心:用另一个类存储当前类的数据状态,方便未来恢复。
    使用字典保存不同版本的数据,可随时恢复到任意状态。
  2. 概述:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。
16. 访问者模式
  1. 核心:
  2. 概述:封装一些作用于某些数据结构中的各元素的操作,它可以在不改变数据结构的前提下赋予这些元素新的操作。
  3. 假如一个对象中存在着一些与本对象不相干(或者关系较弱)的操作,为了避免这些操作污染这个对象,则可以使用访问者模式来把这些操作封装到访问者中去。
    假如一组对象中,存在着相似的操作,为了避免出现大量重复的代码,也可以将这些重复的操作封装到访问者中去。
    在这里插入图片描述
17. 适配器模式
  1. 核心:使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作,更换方法时不影响原本的使用。
  2. 概述:将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。
    在这里插入图片描述
18. 代理模式
  1. 核心:类A1~An有接口B,C方法。类D也有接口B,C方法。A传参数给D,让D代替A完成C方法。
  2. 概述:代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。
19. 装饰模式
  1. 核心:将想要被装饰(进行某种处理)的类交付给装饰类,装饰类对该类持有引用,然后再j进行处理。装饰类又可装饰装饰类,进行多层装饰。
  2. 概述:动态的将责任附加到对象上,若要扩展功能,装饰模式提供了比继承更有弹性的替代方案。

2. 重构

1. 需要规避的注意事项

  1. 无意义的命名 -> 以功能决定命名
  2. 重复代码 -> 使用提炼函数封装
  3. 过长函数 -> 将一个过长函数分解成几个小函数
  4. 过长参数 -> 以对象作为传入参数
  5. 全局数据 -> 减少或不使用
  6. 可变数据 -> 避免直接修改原数据,而尝试修改其副本
  7. 发散性变化 -> 将行为拆解,不同行为之间通过一个清晰的数据结构进行沟通
  8. 霰弹式修改 -> 将本不该分散的逻辑拽回一处
  9. 依恋情结 -> 如果一个函数和另一个模块中的函数交流格外频繁,那应该尝试将它们组合到一起。
  10. 数据泥团 -> 将零散的数据提炼到一个独立对象中
  11. 基本类型偏执 -> 将那些零零散散、为实现同一功能而编写的零散数据合并成一个对象来调用。
  12. 重复的switch -> 使用多态,而非使用基本类型值来判断
  13. 循环语句 -> 采用管道取代循环(filter、map)
  14. 冗长的元素 -> 对于没有必要封装的简单行为,将其删除
  15. 夸夸其谈通用性 -> 移除那些永远不会使用到的钩子函数(在某种情况发生时会调用的函数)
  16. 临时字段 -> 面对特定情况而设定的内容,可以将其单独封装,甚至通过特例(变量不合法)来避免写出条件式代码
  17. 过长的消息链 ->对于需要多层调用才能拿到的数据,可使用中介者或提炼函数将其推入消息链
  18. 中间人 -> 当某个类的接口过多的函数都委托给其他类,这时就该移除中间人,和真正负责的对象打交道。
  19. 内幕交易 -> 将两个数据交换频繁发数据合并成一个数据
  20. 过大的类 -> 将做了太多事情的类分解
  21. 异曲同工的类 -> 将相同行为提炼超类
  22. 纯数据类 -> 出现并非作为函数返回值的纯数据类出现时,使用搬移函数将那些调用行为搬移到纯数据类中。
  23. 当你感觉需要撰写注释时,请先尝试重构,来让所有注释都变得多余

2. 软件测试

  1. 提前自测代码以降低应用出现bug的风险,
  2. 确保所有测试都完全自动化,让它们检查自己的测试结果。
  3. 总是确保测试不该通过时真的会失败
  4. 考虑可能出错的边界条件,把测试火力集中在那儿

3. 重构手段

1. 提炼函数

动机:

  1. 避免函数过长,需要很长时间阅读代码以明白函数意义。
  2. 复用代码。

做法:

  1. 创建新函数,根据函数意图对其命名。
2. 内联函数

动机:

  1. 若函数内容和名称一样清晰可见,且没有多处被调用,子类继承实现,那这样的函数事无意义的

做法:

  1. 删除函数,并将内容移交给调用函数中
3. 提炼变量

动机:

  1. 表达式可能复杂而又难以阅读

做法:

  1. 将长表达式分解成几个简短的表达式,并在最后将它们的结果合并
4. 内联变量

动机:

  1. 变量仅仅被调用一次,且并没有让代码变得优雅

做法:

  1. 删掉变量,使用变量的值来代替
5. 改变函数、变量声明

动机:

  1. 函数、变量名晦涩难懂,无法通过函数名直接判断用途

做法:

  1. 更改函数、变量名为更加友好的名称
6. 封装变量

动机:

  1. 变量作用域变大,数据可以被任意修改,导致代码变得不可控

做法:

  1. 将变量变得私有,改为暴露函数
7. 引入参数对象

动机:

  1. 函数的参数过多,一组数据出没于一个又一个函数

做法:

  1. 将数据封装成一个数据结构
8. 函数合成类、变换

动机:

  1. 函数的参数过多,一组数据出没于一个又一个函数

做法:

  1. 将数据封装成一个数据结构
  2. 如果原数据无需更改,将其封装成一个变换函数也是不错的选择
9. 拆分阶段

动机:

  1. 冗长的代码构建成一个函数,难以阅读和维护

做法:

  1. 将函数根据阶段拆分成若干个独立函数
10. 封装
  1. 对于数据类,可将其封装成类
  2. 封装集合,永远不要直接返回集合的值,而是封装成类。这样对集合修改时必须经过类。
  3. 以对象来取代基本数据类型
  4. 如果类中的某种功能及其需要的函数,参数可形成一个单独的组件而不需要类内其他部件参与它们的基本逻辑运算,那么可以将它们剥离成一个新类。反之两个类不再有单独存在的理由时,可将两个类融合
11. 隐藏委托关系、移除中间人
  1. 当几个数据存在互相调用的关系时,考虑使用隐藏委托关系(溢出中间人)来解耦。
  2. 如果这种关系涉及的对象过多,交互过于复杂,那么使用中间人可能已经不太合适了。
  3. 很难说多少个数据使用哪个才是合适的,随着代码不断变化,合适的尺度也在不断改变。你永远不必说对不起,只要把问题修补好就行了。
12. 搬移函数、字段

动机:

  1. 某函数、字段频繁在其他行为的上下文中调用,而对自身上下文关心甚少

做法:

  1. 将函数、字段搬移到合适的类中
13. 搬移语句到函数、调用者

动机:

  1. 某语句在使用时共用,或原本公用的语句因为某些原因表现出完全不同的行为

做法:

  1. 移语句到函数或调用者
14. 拆分循环、变量

动机:

  1. 某循环内做了太多的事情,导致需要修改循环时,需要同时理解这几件事情。
  2. 某变量在阶段性任务结束后仍然被继续使用,导致意外发生

做法:

  1. 将循环拆分成若干个
  2. 声明新的变量
15. 以查询取代派生变量

动机:

  1. 某些变量可一很容易地随时计算出来,因此可以去掉这些变量以减少需要维护的数据。

做法:

  1. 去掉不必要的数据定义
16. 将引用对象改为值对象、将值对象改为引用对象
  1. 将一个对象镶入另一个对象时,通常会选择这个对象的引用,这样将将它传给其他数据时,可能会导致不希望的修改。
  2. 如果数据频繁发生更改,值对象会导致过多的新对象产生,浪费空间
17. 分解、合并条件表达式
  1. 当某种事件发生的条件过于复杂时,不妨将其剥离成一个单独的函数
18. 使用多态取代条件表达式
  1. 简单的说,就是状态模式
19. 将查询函数与修改函数分离
  1. 当有需求对某个对象,在某种条件下进行特定操作时,可将判断对象是否满足条件,对对象进行操作,分离成两个函数。
20. 函数参数化
  1. 如果两个函数极为相似而区别仅在部分内容不同,可合并两个函数以参数决定行为区别。
21. 移除标记参数
  1. 如果某函数根据参数标记,决定去进行何种不同的行为时,不妨将行为分离成不同的函数,将通过参数标记判断执行何种函数的行为交付给调用者
22. 保持对象完整
  1. 如果需要将对象中的某几个参数剥离出来构建新对象作为参数,去执行某个函数。不妨改为将整个对象传入以应对变化
  2. 反之,如果需要让函数独立降低依赖性,那么就需要仅仅传入元素的值。
23. 以工厂函数取代构造函数
  1. 使用工厂函数取代直接构造函数
24. 以命令取代函数
  1. 将参数不再传递给死板的特定函数,而是转为特定的命令类,这样由于命令类有若干个不同的方法,我们便可以对这组数据进行若干次,若干种不同的行为操作。
  2. 链式调用构建的对象就像是一种命令类,不断进行着某项操作

猜你喜欢

转载自blog.csdn.net/qq_50682713/article/details/124917489