原文地址: C#设计模式总结
使用设计模式的根本原因是适应变化,提高代码复用率,使软件更具有可维护性和可扩展性。
一. 设计原则
在进行设计的时候,应该遵循以下原则:
1.单一职责原则
就一个类而言,应该只有一个引起它变化的原因。如果一个类承担的职责过多,一个职责的变化可能会影响到其他的职责,并且,把多个职责耦合在一起,也会影响复用性。
2.开放封闭原则
开闭原则即OCP原则。一个软件实体(类、函数、模块等)应该对扩展开放,对修改关闭。即每次发生变化时,要通过添加新的代码来增强现有类型的行为,而不是修改原有的代码。
3.里氏代替原则
在软件开发过程中,子类替换父类后,程序的行为是一样的。只有当子类替换掉父类后,此时软件的功能不受影响时,父类才能真正地被复用,而子类也可以在父类的基础上添加新的行为。
4.依赖倒置原则
抽象不应该依赖于细节,细节应该依赖于抽象,也就是“面向接口编程,而不是面向实现编程”。
5.接口隔离原则
使用多个专门的接口比使用单一的总接口要好。也就是说不要让一个单一的接口承担过多的职责,而应把每个职责分离到多个专门的接口中,进行接口分离。
6.合成复用原则
在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分。新对象通过向这些对象的委派达到复用已有功能的目的。简单地说,就是尽量使用合成/聚合,尽 量不要使用继承。
7.迪米特法则
又叫最少知识原则。指的是一个对象应当对其他对象有尽可能少的了解。也就是说,一个模块或对象应尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立,这样当一个模块修改时,影响的模块就会越少,扩展起来更加容易。也可以表述成:只与你直接的朋友们通信;不要跟陌生人说话。
二. 设计模式
创建型
创建型模式就是用来创建对象的模式,抽象了实例化的过程。
1.单例模式
确保某一个类只有一个实例,并提供一个全局访问点。解决的是实体对象个数的问题,而其他的创建型模式都是解决new所带来的耦合关系问题。
- 实例:多线程
2.简单工厂模式
封装改变。将改变的代码用类来封装。
- 实例:自己做菜与餐馆点菜
- 优点:
- 解决了客户端直接依赖于具体对象的问题,客户端可以消除直接创建对象的责任,而仅仅是消费产品。实现了对责任的分割。
- 实现了代码复用
- 缺点:
- 工厂类中集中了所有产品的创建逻辑,一旦不能正常工作,整个系统都会受到影响。
- 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,变得复杂。
- 使用场景:
- 当工厂类负责创建的对象比较少时
- 客户只知道传入工厂类的参数,对于如何创建对象的逻辑不关心
3.工厂方法模式
定义一个创建对象的工厂接口,由其子类决定要实例化的类,将实际创建工作推迟到子类中。强调的是“单个对象”的变化。
- 实例:餐馆点菜
- 优点:
- 允许系统在不修改工厂类逻辑的情况下来添加新产品,克服了简单工厂模式中的缺点
- 缺点:
- 只适合单个对象,无法应对一个工厂需要创建多个对象的情况
4.抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,使得客户端可以在不必指定产品的具体类型的情况下,创建多个产品族中的产品对象,强调的是“系列对象”的变化。
- 实例:绝味鸭脖连锁店
- 优点:
- 将具体产品的创建延迟到具体工厂的子类中,将对象的创建封装起来,可以减少客户端与具体产品类之间的依赖,从而使系统耦合度降低,更有利于后期的维护和扩展
- 缺点:
- 很难支持新种类产品的变化。因为抽象工厂接口中已经确定了可以被创建的产品集合,如果要添加新产品,此时就需要去修改抽象工厂的接口,这样就涉及到抽象工厂类及其所有子类的改变,这违反了“开闭原则”。
- 使用场景:
- 一个系统不要求依赖产品类实例如何被创建、组合和表达的表达
- 这个系统有多个系列产品,而系统中只消费其中某一系列产品
- 系统要求提供一个产品类的库,所有产品以同样的接口出现,客户端不需要依赖具体实现
5.建造者模式
将一个产品的内部表示与产品的构造过程分割开来,从而可以使一个建造过程生成具体不同的内部表示的产品对象。强调的是产品的构造过程。
- 实例:组装电脑
- 要点:
- 在建造者模式中,指挥者是直接与客户端打交道的,指挥者将客户端创建产品的请求划分为对各个部件的建造请求,再将这些请求委派到具体的建造者角色,具体建造者角色是完成具体产品的构建工作的,却不为客户所知道。
- 建造者模式主要用于“分步骤来构建一个复杂的对象”,其中“分步骤”是一个固定的组合过程,而复杂对象的各个部分是经常变化的。
- 产品不需要抽象类,由于建造模式创建出来的最终产品可能差异很大,所以不大可能提炼出一个抽象产品类。
- 抽象工厂模式解决了系列产品的需求变化,而建造者模式解决的是“产品部分”的需求变化。
- 建造者隐藏了具体产品的组装过程,所以要改变一个产品的内部表示,只需要再实现一个具体的建造者就可以了,从而能很好地应对产品组成组件的需求变化。
6.原型工厂模式
通过给出一个原型对象来指明所要创建的对象类型,然后用复制的方法来创建出更多的同类型对象。
- 实例:悟空分身
- 优点:
- 向客户隐藏了创建新实例的复杂性
- 允许动态增加和减少产品类
- 简化了实例的创建结构,工厂方法模式需要有一个与产品类等级结构相同的等级结构,而原型模式不需要这样
- 产品类不需要事先确定产品的等级结构,因为原型模式适用于任何的等级结构
- 缺点:
- 每个类必须配备一个克隆方法
- 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候
结构型
7.适配器模式
适配器模式意在转换接口,它能够使原本不能在一起工作的两个类一起工作,所以经常用在类库的复用、代码迁移等方面。分为类适配器模式和对象适配器模式两种。
- 实例:电器插座和插头
- 类的适配器模式
- 优点:
- 可以在不修改原有代码的基础上复用现有类,很好地符合“开闭原则”
- 可以重新定义Adapter的部分行为,因为在类适配器模式中,Adapter是Adaptee的子类
- 仅仅引入一个对象,并不需要额外的字段来引用Adaptee实例
- 缺点:
- 用一个具体的Adapter类对Adaptee和Target进行匹配,当如果想要匹配一个类以及所有它的子类时,类的适配器模式就不能胜任了。
- 采用了“多继承”的实现方式,带来了不良的高耦合
- 优点:
- 对象的适配器模式
- 优点:
- 可以在不修改原有代码的基础上复用现有类,很好地符合“开闭原则”
- 采用对象组合的方式,更符合松耦合
- 缺点
- 使得重定义Adaptee的行为较困难,这就需要生成Adaptee的子类并且使得Adapter引用这个子类而不是引用Adaptee本身
- 优点:
- 使用场景:
- 系统需要复用现有类,而该类的接口不符合系统的需求
- 想要建立一个可重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作
- 对于对象适配器模式,在设计里需要改变多个已有子类的接口,如果使用类的适配器模式,就要针对每一个子类做一个适配器,而这不太实际。
8.桥接模式
桥接模式旨在将抽象化与实现化解耦,使得两者可以独立地变化。意思就是说,桥接模式把原来基类的实现化细节再进一步进行抽象,构造到一个实现化的结构中,然后再把原来的基类改造成一个抽象化的等级结构,这样就可以实现系统在多个维度的独立变化。
- 实例:遥控器与各种品牌电视机
- 优点:
- 把抽象接口与其实现解耦
- 抽象和实现可以独立扩展,不会影响到对方
- 实现细节对客户透明,对用户隐藏了实现的具体细节
- 缺点:
- 增加了系统复杂度
- 使用场景
- 如果一个系统需要在构件的抽象化角色和具体化角色之间添加更多的灵活性,避免在两个层次之间建立静态的联系
- 设计要求实现化角色的任何改变不应当影响客户端,或者实现化角色的改变对客户端是完全透明
- 需要跨越多个平台的图形和窗口系统上
- 一个类存在两个独立变化的维度,且两个维度都需要进行扩展
9.装饰者模式
又称包装模式,它可以动态地给一个对象添加一些额外的功能,装饰者模式较继承生成子类的方式更加灵活。虽然装饰者模式能够动态地将职责附加到对象上,但同时也会造成产生一些细小的对象,增加了系统的复杂度。
- 实例:手机与各种手机配件
- 优点
- 装饰者模式和继承的目的都是扩展对象的功能,但装饰者模式更灵活
- 通过使用不同的具体装饰类以及这些类的排列组合,设计师可以创造出很多不同行为的组合
- 良好的扩展性
- 缺点
- 会导致设计中出现许多小对象,如果过度使用,会让程序变得更复杂,查错困难
- 使用场景
- 需要扩展一个类的功能或给一个类增加附加责任
- 需要动态地给一个对象增加功能,这些功能可以再动态地撤销
- 需要增加由一些基本功能的排列组合而产生的非常大量的功能
10.组合模式
又称部分—整体模式。组合模式将对象组合成树形结构,用来表示整体与部分的关系。组合模式使得客户端将单个对象和组合对象同等对待。
- 实例:简单图形与由简单图形构成的复杂图形
- 优点:
- 组合模式使得客户端代码可以一致地处理对象和对象容器,无需关心处理的是单个对象还是组合的对象容器
- 将客户代码与复杂的对象容器结构解耦
- 可以更容易地往组合对象中加入新的构件
- 缺点:
- 使得设计更加复杂,客户端需要花更多时间清理类之间的层次关系(所有的设计模式共有的问题)
- 要点:
- 有时候系统需要遍历一个树枝结构的子构件很多次,这时候可以考虑把遍历子构件的结构存在父构件里作为缓存
- 客户端尽量不要直接调用树叶类中的方法,而是借用其父类的多态性完成调用,这样可以增加代码的复用性
- 使用场景
- 需要表示一个对象整体或部分的层次结构
- 希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象
11.外观模式
为子系统中的一组接口提供一个一致的门面,它提供了一个高层接口,这个接口使得与子系统的交互更加容易。
- 实例:学生选课系统
- 优点:
- 对用户屏蔽了子系统组件,从而简化了接口,减少了客户处理的对象数目并使子系统的使用更加简单
- 实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件是紧耦合的,松耦合使得子系统的组件变化不会影响到它的客户
- 缺点:
- 如果增加新的子系统可能需要修改外观类和客户端的源代码,不过这一点也是不可避免的
- 使用场景:
- 为一个复杂的子系统提供一个简单的接口
- 提供子系统的独立性
- 在层次化结构中,可以使用外观模式定义系统中每一层的入口。
12.享元模式
运用共享的技术有效地支持细粒度的对象,使其进行共享。在.NET类库中,String类的实现就使用了享元模式,String类采用字符串驻留池来使字符串进行共享。
- 实例:文本编辑器中的字面
- 优点:
- 降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力
- 缺点:
- 为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑更复杂
- 将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长
- 使用场景:
- 一个系统中有大量的对象
- 这些对象消耗大量的内存
- 这些对象中的状态大部分都可以被外部化
- 这些对象可以按照内部状态分成很多的组,当把外部对象从对象中剔除时,每一组都可以仅用一个对象代替
- 软件系统不依赖这些对象的身份
13.代理模式
给某一个对象提供一个代理,并由代理对象控制对原对象的访问。
- 实例:代购
- 优点:
- 能够将调用用于真正被调用的对象隔离,在一定程度上降低了系统的耦合性
- 代理对象在客户端和目标对象之间起到一个中介的作用,这样可以起到对目标对象的保护。
- 代理对象可以在对目标对象请求之前进行一个额外的操作,例如权限检查等
- 缺点:
- 由于在客户端和真实主题之间增加了一个代理对象,所以会造成请求的处理速度变慢
- 实现代理类也需要额外的工作,从而增加了系统的复杂度
行为型
分为类的行为模式和对象的行为模式两种。
- 类的行为模式:使用继承关系在几个类之间来分配行为
- 对象的行为模式:使用对象聚合的方式来分配行为
14.模板方法模式
在一个抽象类中定义一个操作中的算法骨架,而将一些具体步骤实现延迟到子类中去实现。模板方法使得子类可以在不改变算法结构的前提下,重新定义算法的特定步骤,从而达到复用代码的效果。
- 实例:做菜
- 优点:
- 实现了代码复用
- 能够灵活应对子步骤的变化,符合开闭原则
- 缺点:
- 因为引入了一个抽象类,如果具体实现过多的话,需要用户或开发人员花更多时间去清理类之间的关系
15.命令模式
命令模式属于对象的行为模式。命令模式把一个请求或操作封装到一个对象中,通过对命令的抽象化来使得发出命令的责任和执行命令的责任分隔开。命令模式的实现可以提供命令的撤销和恢复功能。
- 实例:军训
- 优点:
- 命令模式使得新的命令很容易被加入到系统里
- 可以设计一个命令队列来实现对请求的undo和redo操作
- 可以较容易地将命令写入日志
- 可以把命令对象聚合在一起,合成为合成命令。合成命令是合成模式的应用
- 缺点:
- 可能会导致系统有过多的具体命令类
- 使用场景
- 系统需要支持命令的撤销
- 系统需要在不同的时间指定请求、将请求排队
- 如果一个系统要将系统中所有的数据消息更新到日志里,以便在系统崩溃时,可以根据日志读回所有数据的更新命令,重新调用方法来一条一条地执行这些命令,从而恢复系统在崩溃前所做的数据更新
- 系统需要使用命令模式作为“callback”在面向对象中的替代
16.迭代器模式
迭代器模式是针对集合对象而生的。迭代器模式提供了一种方法来顺序访问一个集合对象中各个元素,而又无需暴露该对象的内部表示。
- 实例:IEnumerator接口(迭代器接口)和IEnumberable接口(聚集接口)
- 优点:
- 使得访问一个聚合对象的内容而无需暴露它的内部表示,即迭代抽象
- 为遍历不同的集合结构提供了一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作
- 缺点:
- 在遍历的同时更改迭代器所在的集合结构会导致出现异常,所以使用foreach语句只能对集合进行遍历,不能在遍历的同时更改集合中的元素
- 使用场景
- 系统需要访问一个聚合对象的内容而无需暴露它的内部表示
- 系统需要支持对聚合对象的多种遍历
- 系统需要为不同的聚合结构提供一个统一的接口
17.观察者模式
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己的行为。
- 实例:腾讯游戏订阅号
- 优点:
- 实现了表示层和数据逻辑层的分离,并定义了稳定的更新消息传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层,即观察者
- 在被观察者和观察者之间建立了一个抽象的耦合,被观察者并不知道任何一个具体的观察者,只是保存着抽象观察者的列表,每个具体观察者都符合一个抽象观察者的接口
- 支持广播通信。被观察者会向所有的注册过的观察者发出通知
- 缺点:
- 如果一个被观察者有很多直接和间接的观察者时,将所有的观察者都通知到会花费很多时间
- 虽然观察者模式可以随时使观察者知道所观察的对象发生了变化,但是观察者模式没有相应的机制使观察者知道所观察的对象是怎样发生变化的
- 如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃
- 使用场景
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面,将这两者封装在独立的对象中以使它们可以各自独立地改变和复用的情况下
- 当对一个对象的改变需要同时改变其他对象,而又不知道具体有多少对象有待改变的情况下
- 当一个对象必须通知其他对象,而又不能假定其他对象是谁的情况下
18.中介者模式
定义一个中介对象来封装一系列对象之间的交互关系。中介者使各个对象之间不需要显式地相互引用,从而使耦合性降低,而且可以独立地改变它们之间的交互行为。
- 实例:QQ游戏平台
- 优点:
- 简化了对象之间的关系,将系统的各个对象之间的相互关系进行封装,将各个同事类解耦,使得系统变为松耦合
- 提供系统的灵活性,使得各个同事对象独立而易于复用
- 缺点:
- 中介者角色承担了较多的责任,所以一旦这个中介者对象出现了问题,使得系统将会受到重大的影响
- 新增加一个同事类时,不得不去修改抽象中介者类和具体中介者类,此时可以使用观察者模式和状态模式来解决这个问题
- 使用场景:
- 一组定义良好的对象,现在需要进行复杂的相互通信
- 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类
19.状态模式
每个对象都有其对应的状态,而每个状态又对应一些相应的行为,如果某个对象有多个状态时,那么就会对应很多的行为。那么对这些状态的判断和根据状态完成的行为,就会导致多重条件语句,并且如果添加一种新的状态时,需要更改之前现有的代码。这样的设计显然违背了开闭原则,状态模式正是用来解决这样的问题的。
状态模式——允许一个对象在其内部状态改变时自动改变其行为,对象看起来就像是改变了它的类。
- 实例:银行账户状态
- 优点:
- 将状态判断逻辑分割到每个状态类里面,可以简化判断的逻辑
- 当有新的状态出现时,可以通过添加新的状态类来进行扩展,扩展性好
- 缺点:
- 如果状态过多的话,会导致有非常多的状态类,加大了开销
- 使用场景:
- 当一个对象状态转换的条件表达式过于复杂时可以使用状态者模式。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简单化
- 当一个对象行为取决于它的状态,并且它需要在运行时根据状态改变它的行为时
20.策略模式
策略模式是对算法的包装,是把使用算法的责任和算法本身分隔开,委派给不同的对象负责。策略模式通常把一系列的算法包装到一系列的策略类里面。可一句话概括:将每个算法封装到不同的策略类中,使得它们可以互换。
- 实例:所得税
- 优点:
- 策略类之间可以自由切换。由于策略类都实现同一个接口,所以使它们之间可以自由切换
- 易于扩展。增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码
- 避免使用多重条件选择语句,充分体现面向对象设计思想
- 缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这点可以考虑使用IOC容器和依赖注入的方式来解决
- 会造成很多的策略类
- 使用场景:
- 一个系统需要动态地在几种算法中选择一种的情况下
- 如果一个对象有很多的行为,如果不使用合适的模式,这些行为就只好使用多重的if-else语句来实现,此时,可以使用策略模式,把这些行为转移到相应的具体策略类里面,就可以避免使用难以维护的多重条件选择语句,并体现面向对象设计的概念
21.责任链模式
某个请求需要多个对象进行处理,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链子,并沿着这条链子传递该请求,直到有对象处理它为止。
- 实例:公司采购审批
- 优点:
- 降低了请求的发送者和接收者之间的耦合
- 把多个条件判定分散到各个处理类中,使得代码更加清晰,责任更加明确
- 缺点:
- 在找到正确的处理对象之前,所有的条件判定都要执行一遍,当责任链过长时,可能会引起性能的问题
- 可能导致某个请求不被处理
- 使用场景:
- 一个系统的审批需要多个对象才能完成处理的情况下,例如请假系统等
- 代码中存在多个if-else语句的情况下,此时可以考虑使用责任链模式来对代码进行重构
22.访问者模式
访问者模式是封装一些施加于某种数据结构之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。访问者模式适用于数据机构相对稳定的系统,它把数据结构和作用于数据结构之上的操作之间的耦合度降低,使得操作集合可以相对自由的改变。
- 实例:打印对象
- 优点:
- 使得添加新的操作变得容易。如果一些操作依赖于一个复杂的结构对象的话,那么一般而言,添加新的操作会变得很复杂。而使用访问者模式,添加新的操作就意味着添加一个新的访问者类。
- 使得有关的行为操作集中到一个访问者对象中,而不是分散到一个个的元素类中
- 可以访问属于不同的等级结构的成员对象,而迭代只能访问属于同一个等级结构的成员对象
- 缺点:
- 增加新的元素类变得困难。每增加一个新的元素,意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中添加相应的具体操作
- 使用场景:
- 如果系统有比较稳定的数据结构,而又有易于变化的算法时,此时可以考虑使用访问者模式。因为访问者模式使得算法操作的添加比较容易
- 如果一组类中,存在着相似的操作,为了避免出现大量重复的代码,可以考虑把重复的操作封装到访问者中
- 如果一个对象存在着一些与本身对象不相干、或关系比较弱的操作时,为了避免操作污染这个对象,则可以考虑把这些操作封装到访问者对象中
23.备忘录模式
在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以把该对象恢复到原先的状态。
- 实例:手机通讯录
- 优点:
- 如果某个操作错误地破坏了数据的完整性,此时可以使用备忘录模式将数据恢复成原来正确的数据
- 备份的状态数据保存在发起人角色之外,这样发起人就不需要对各个备份的状态进行管理,而是由备忘录角色进行管理,而备忘录角色又是由管理者角色管理,符合单一职责原则
- 缺点:
- 在实际的系统中,可能需要维护多个备份,需要额外的资源,这样对资源的消耗比较严重。
- 使用场景:
- 如果系统需要提供回滚操作时,使用备忘录模式非常合适。
24.解释器模式
解释器模式是给定一种语言,定义它文法的一种表示,并定义一种解释器,这个解释器使用该表示来解释语言中的句子。



