首页 > 其他 > 详细

软件设计原则和模式

时间:2021-01-07 08:58:11      阅读:20      评论:0      收藏:0      [点我收藏+]

内聚&耦合

高内聚低耦合是设计的目标,但无论你怎么做都无法消除软件模块之间的耦合。所以掌握内聚和耦合的度是设计好坏的关键。

内聚

内聚是指模块使用者不可见的逻辑,开闭原则、单一职责、接口隔离都是提高内聚的准则。

耦合

耦合是指软件模块之间互相关联的部分。继承、全局变量、硬编码、程序员约定这些都属于高耦合的关键因素。

内聚&耦合的层级

内聚和耦合发生在软件的各个阶段

  • 编码
    编码阶段的内聚和耦合主要在于对数据的使用上,如果使用了逻辑范围以外的数据则可以认为发生了较强的耦合。例如,直接使用某个对象的成员变量。
  • 对象封装
    对象封装的时候需要先对需要封装的对象进行抽象,比较常见的是封装对象的时候违反单一职责原则,使一个对象维护多个抽象关系。
  • 模块划分
    模块是指多个类配合完成特定功能,在这个阶段较为常见的是模块中的某一个类或多个类聚合其他模块中类的部分逻辑。使模块与模块之间交织在一起,当需要修改某个对象方法或入参时,需要对各个相关联的模块或对象一并修改。
  • 软件分层(库、夹包)
    常见的情况是在引入某个库或夹包的同时需要被动引入其他库或夹包。C++中常见的情况是在头文件中包含其他库的头文件导致的库引入问题。
  • 架构 

重构

重构并不是在某个时间段内对代码进行重新梳理和修改,当我们发现代码中有任何值得重构的地方时我们都应该对其进行重构。重构与修改的规模并无任何关系。所有的重构方法都是极小的修改,例如:

  • 重命名
  • 添加参数
  • 传值变为传引用
  • 单向关联改为双向关联或反之
  • 合并条件式
  • ……

也有较为重度的重构,例如:

  • 隐藏委托关系
  • 提炼继承体系
  • 以工厂方法取代构造函数
  • ……

代码的坏味道

什么时候重构取决于程序员什么时候嗅到 ”代码的坏味道“ 这大多数取决于程序员对设计的敏感,以及对修改后收益的估算。针对 ”代码的坏味道“ 的另一种说法叫做《反模式》

反模式经常有以下根源

  • 匆忙
    一般情况下,当开发人员受到项目进度的压力时会为了方便而牺牲掉长期的架构性利益。
  • 漠然
    面向对象系统中关键的决策划分出现在稳定的可复用设计和可替换设计之间,使可替换部分通过配置进行装配。漠然的态度会导致开发人员的焦点落在完成需求上,从而放弃系统的可扩展性。
  • 思想狭隘
    拒绝实践有效的解决方案,经常性的使用 “程序员之间的约定" 和硬编码来应对需求。
  • 懒惰
    复用止于复制&粘贴,拒绝使用规则导向的设计和实践,甚至对问题不进行归纳和抽象就急于通过编码解决问题。结果就是产出的代码缺乏配置控制。常见的情况就是由于多次需求变更,致使一个函数需要有多个参数传入,且每次函数执行时并不需要所有参数。
  • 贪婪
    对过多的细节进行建模,导致复杂性上升。开发人员从根本上认为,高复杂性代表高技术。一个常见的情况就是过度设计(为需求预设很多可能性,并绞尽脑汁解决这些并不存在的需求。)
  • 无知
    大多开发人员会从自身认知出发对系统进行设计,但实际上其并不了解某些技术的细节。从而导致某些设计从根本上就站不住脚。
  • 自负
    大多数开发人员喜欢自己造轮子,这就是自负的典型表现。这导致代码中到处都有类似的功能,而不是对某个功能进行重构使其满足更多场景。

重构准备 - 测试代码

重构前我们需要先对需要重构的部分编写测试代码,测试代码是非常好的解耦合试金石。这基于一个事实,如果需要单独测试某个模块,则它必须先切断与其他模块的联系。在代码设计和编码阶段,先考虑如何测试可以提升设计的内聚程度。

如何重构

当你决定要重构某个系统的时候最好要先吃透原系统的代码和控制流程,并使用桩模块构建重构部分的边界。对一些设计问题比较多的模块进行重构过程并非能一步到位,大多数情况下需要几次,甚至几十次的小规模重构才能达到重构的最终目标。

原则,设计的底线

单一职责(SRP)

如果一个类的职责过多,相当于把这些职责耦合在一起了。导致的结果可能是一个职责的变化会削弱或者抑制这个类完成其他职责的能力。当变化发生时,设计会遭受到意想不到的破坏。

常见的例子:

MVC中,在Module的部分耦合Control或View部分的逻辑。

开放封闭(OCP)

 如果程序中的一处改动会产生连锁反应,导致一系列相关模块的改动,那么设计就存在僵化性的臭味。正确使用OCP可以使今后进行相似的改动时只需要添加新的代码而不改动已经正常运行的代码。

OCP的两个特征是:

  1. 对于扩展是开放的
  2. 对于更改是封闭的

经典的设计模式中,有多个都符合OCP原则。设计的关键在于抽象,通过语义来定义对象和接口是是常用的方法。

典型的违反OCP原则的案例是switch - case 和 if - else 语句的使用。

为避免过度设计,可以在一定程度上接受违反OCP的情况,直到有需求刺激情时再进行重构。一但重构完成,则不必再对正常运行的代码进行重构。

OCP相关的模式有:策略、桥接、模板方法等

在许多方面,OCP都是面向对象设计的核心所在。遵循这个原则可以带来面向对象所生成的巨大好处(灵活性、可扩展性及可维护性)OCP原则适用于频繁变化的部分,对于较为稳定的部分可以适当放宽。所以拒绝滥用OCP和使用OCP同样重要。

等量代换(LSP)

子类型必须要能够替换掉他们的基类型。确切的说是对于每个类型S的对象O1,都存在一个类型T的对象的O2,使得在所有针对O1编写的程序P中,用O1替换O2后,程序P的功能不变(注意,不是行为),则S是T的子类型(并不要求继承)。

从定义上来看,等量代换的实质是指基于抽象的语义描述类型T或S,从而使其接口保持一致。一般情况下违反LSP原则的同时也会违反OCP原则。较为常见的情况是通过if-else判断类型值来决定后续的调用方法。

LSP的前提是从使用者的角度进行合理假设,其难点是如何通过抽象找到正确的合理假设。此外需要重申的一点,LSP 的对象关系并非 IS-A的关系,LSP注重的是行为方式的一致性,而非对象的逻辑关系。

明确合理假设的一个有效方法是在做抽象前先规定该对象的契约(隐喻,例如对于Shape对象,其行为是Paint),并在编写代码的过程中通过该契约获悉可以依赖的行为方式。

契约有两个关键点:

  1. 前置条件 - 在对象使用前该条件必须为 True
  2. 后置条件 - 在对象使用之后,该条件必须为 True

也就是说,对象使用前后,并不会改变代码的语义。从而,使我们必须假设,任何基类的派生类,只能使用约束更弱的前置条件(换句话说,派生类必须接受基类所能接受的一切)。

一个例子:

之前在做游戏的时候,有一个现成的网络库可以用。但当时评估下来,我们认为这个网络库在性能和可扩展性上有很严重的问题。所以我使用Adapter模式对其做了一层封装。后来我们自己重写了网络库,并实现了另一个Adapter。结果新的网络库工作的非常好,而我们并未对上层代码进行任何修改。

LSP的重要性:OCP 是面向对象中很多问题的核心,而LSP 是使OCP成为可能的主要原则之一。

依赖倒置(DIP)

依赖倒置 (DIP)的核心思想是好莱坞原则 ”不要打电话给我,需要的时候我们会打电话给你。“ 这句话要表达的更深层的含义是关于上下层之间的调用关系,底层不应该与上层之间有任何通讯。

依赖倒置的关键在于:

  1. 高层模块不应依赖于底层模块,二者都应该依赖于抽象
  2. 抽象不应该依赖于细节,细节应该依赖于抽象。

其中第一条解释了为什么该原则要使用”倒置“这个词。还是LSP中举的网络模块的例子,当上层模块拥有接口定义时,我们可以很容易的使用LSP原则来改变底层模块的行为。这基于一个实际问题,当底层模块有改动的时候,高层模块是否也需要跟着改动?如果高层模块独立于底层模块,那么高层模块就可以很容易被复用。该原则是框架设计的核心原则。

所有结构良好的面向对象架构都具有清晰的层次定义,每个层次通过一个定义好的、受控的接口向外提供一组内聚的服务。

使用DIP原则的关键是找出应用背后的抽象,是那些不随具体细节的改变而改变的语义。一般情况下,我们会想到使用多态来实现语义抽象。但例如C++的模板,以及Java的反射也可以实现DIP。

我们可以使用这样一条评判标准,如果程序的依赖关系是倒置的,他就是面向对象的设计。否则,他就是面向过程的设计。如果依赖不是倒置的,那么依赖的细节变更就会影响到整个程序运行的逻辑,所以它是构建富有弹性的代码的一条重要准则。

接口隔离(ISP)

如果类的接口不是内聚的就表示该接口违反了ISP原则。例如经常在代码中的“胖”接口,其可以分解成多组方法,“胖”接口的问题在于当某个客户要求对其改动时,它会影响该类的其他部分。如果有多个客户使用该类,则其他客户的代码也需要随之改动;同时,“胖”接口带来的问题是当修改其中某些模块的时候可能会影响其它功能。对于集成过多功能的类我们有两个方法将接口进行拆分:

  1. 使用委托分离接口
  2. 使用多重继承分离接口

在C++中,当某个类(A)中需要使用其他类(B)时需要包含类B的头文件,此时类B会被强制引用到使用者的文件中。这是一个很大的麻烦,如果类B是一个库的头文件,那么在使用类A的工程中也需要包含类B的头文件路径。而这正是ISP原则告诫我们需要避免的。

什么是设计模式

设计模式是从软件工程的经验出发,从而总结出来的常用的结构,我们称之为模式。

常用模式简介

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节,对象的创建由相关的工厂来完成。就像我们去商场购买商品时,不需要知道商品是怎么生产出来一样,因为它们由专门的厂商生产。

创建型
创建型模式分为以下几种。

  • 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
  • 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
  • 工厂方法(FactoryMethod)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
  • 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
  • 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。

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

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

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

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

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

建造者(Builder)模式的定义:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。

该模式的主要优点如下:

  1. 封装性好,构建和表示分离。
  2. 扩展性好,各个具体的建造者相互独立,有利于系统的解耦。
  3. 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。


其缺点如下:

    1. 产品的组成部分必须相同,这限制了其使用范围。
    2. 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。
结构型
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。

结构型模式分为以下 7 种:
  1. 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
  2. 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
  3. 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。
  4. 装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。
  5. 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
  6. 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
  7. 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。

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

7、适配器(Adapter)模式
适配器模式(Adapter)的定义如下:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

该模式的主要优点如下。

  • 客户端通过适配器可以透明地调用目标接口。
  • 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
  • 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
  • 在很多业务场景中符合开闭原则。


其缺点是:

  • 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
  • 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。

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

通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。这里将桥接模式的优缺点总结如下。

桥接(Bridge)模式的优点是:

  • 抽象与实现分离,扩展能力强
  • 符合开闭原则
  • 符合合成复用原则
  • 其实现细节对客户透明


缺点是:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。

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

10、外观(Facade)模式
外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。

在日常编码工作中,我们都在有意无意的大量使用外观模式。只要是高层模块需要调度多个子系统(2个以上的类对象),我们都会自觉地创建一个新的类封装这些子系统,提供精简的接口,让高层模块可以更加容易地间接调用这些子系统的功能。尤其是现阶段各种第三方SDK、开源类库,很大概率都会使用外观模式。

外观(Facade)模式是“迪米特法则”的典型应用,它有以下主要优点。

  1. 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
  2. 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
  3. 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。


外观(Facade)模式的主要缺点如下。

  1. 不能很好地限制客户使用子系统类,很容易带来未知风险。
  2. 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”

11、享元(Flyweight)模式
享元(Flyweight)模式的定义:运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
享元模式的主要优点是:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
其主要缺点是:

  1. 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
  2. 读取享元模式的外部状态会使得运行时间稍微变长。

游戏中的运用,游戏中道具数量众多,为了节省内存,通常将不会变化的数据封装在一个类中,并使用另一个类来引用。对外暴露一个统一的道具接口。

 

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

行为型

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。

行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。

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

比较常见的是排序算法,可以通过传递一个比较函数来决定排序是升序还是降序,这是模板模式的一种简化。当算法中有多个需要通过回调的返回结果控制程序流程或数据时会自然的想到使用模板方法

该模式的主要优点如下。

  1. 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
  2. 它在父类中提取了公共的部分代码,便于代码复用。
  3. 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。


该模式的主要缺点如下。

  1. 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度。
  2. 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
  3. 由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。

模板方法可以退化成一组回调函数,或者合并组合模式,这样可以增加灵活性。例如:

游戏中的技能释放,需要逐次调用选取释放目标,释放技能,计算伤害,计算技能效果这样的过程。此时就可以使用模板方法来实现一个技能释放的模板方法,通过调用技能对象提供的算法来决定

  1. 技能是范围释放或针对目标,范围释放还有圆形选择区域、扇形选择区域,矩形选择区域、线形选择区域等区分
  2. 伤害计算需要针对目标和施法者的数值进行计算,某些技能还会需要进行延迟计算。
  3. 某些技能会有
    1. 击退、击晕、麻痹等debuf效果
    2. 护盾、祝福、加速等buf效果

14、策略(Strategy)模式

策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

策略模式的主要优点如下。

  1. 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句,如 if...else 语句、switch...case 语句。
  2. 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
  3. 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
  4. 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
  5. 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。


其主要缺点如下。

  1. 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
  2. 策略模式造成很多的策略类,增加维护难度。

策略模式最主要的是通过配置来改变程序行为,很多开发在描述策略模式时往往忽略配置的重要性。通常情况下策略模式会和工厂方法配合使用。账号系统中,短信发送就是典型的策略模式。

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

命令模式一方面会解耦调用时间,一方面会解耦调用职责。一般情况下,当所设计的模块有明显的层次划分,且命令的执行和命令的调用在不同的层时,可以通过命令模式将命令通过队列进行传递。消息其实是命令模式的一种退化表现。

16、职责链(Chain of Responsibility)模式
责任链(Chain of Responsibility)模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

注意:责任链模式也叫职责链模式。

在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。

责任链模式是一种对象行为型模式,其主要优点如下。

  1. 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
  2. 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
  3. 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
  4. 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
  5. 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。


其主要缺点如下。

  1. 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
  2. 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
  3. 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。

典型的应用是窗口消息处理,每一个窗口都会处理其关心的消息。但职责链模式大多数情况下都会面临消息传递路径的不确定性,比如,消息是否处理后还要继续向下传递;消息处理后是否需要将加工过的数据再返给其上层处理。

另一个应用是游戏中的行为树,这也是比较典型的职责链模型。

职责链通常会与组合模式、观察者模式一起使用。

17、状态(State)模式
状态(State)模式的定义:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。

状态模式是一种对象行为型模式,其主要优点如下。

  1. 结构清晰,状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
  2. 将状态转换显示化,减少对象间的相互依赖。将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
  3. 状态类职责明确,有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。


状态模式的主要缺点如下。

  1. 状态模式的使用必然会增加系统的类与对象的个数。
  2. 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
  3. 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,而且修改某个状态类的行为也需要修改对应类的源码。

 

18、观察者(Observer)模式
观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。

观察者模式是一种对象行为型模式,其主要优点如下。

  1. 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
  2. 目标与观察者之间建立了一套触发机制。


它的主要缺点如下。

  1. 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
  2. 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。

 

19、中介者(Mediator)模式

中介者(Mediator)模式的定义:定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。

中介者模式是一种对象行为型模式,其主要优点如下。

  1. 类之间各司其职,符合迪米特法则。
  2. 降低了对象之间的耦合性,使得对象易于独立地被复用。
  3. 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。


其主要缺点是:中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。

中介模式的关键是消息(调用)的翻译,它隐藏了两个沟通对象之间的实现细节,所以可以很容易的将任何一个沟通对象进行替换。从而使两个沟通对象之间解耦。

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

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

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

UNDO,游戏存档,取档,其关键是对数据的保存和管理。

23、解释器(Interpreter)模式
解释器(Interpreter)模式的定义:给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。

这里提到的文法和句子的概念同编译原理中的描述相同,“文法”指语言的语法规则,而“句子”是语言集中的元素。例如,汉语中的句子有很多,“我是中国人”是其中的一个句子,可以用一棵语法树来直观地描述语言中的句子。

解释器模式是一种类行为型模式,其主要优点如下。

  1. 扩展性好。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
  2. 容易实现。在语法树中的每个表达式节点类都是相似的,所以实现其文法较为容易。


解释器模式的主要缺点如下。

  1. 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
  2. 会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护。
  3. 可应用的场景比较少。在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。

解释器的关键在于读取结构化数据,和根据数据执行逻辑这两点。代码中读取配置文件并不能算作解释器模式。

策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

策略模式的主要优点如下。

  1. 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句,如 if...else 语句、switch...case 语句。
  2. 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
  3. 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
  4. 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
  5. 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。


其主要缺点如下。

  1. 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
  2. 策略模式造成很多的策略类,增加维护难度。

软件设计原则和模式

原文:https://www.cnblogs.com/albertclass/p/14182173.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!