设计模式总结(C++和Python实现)
前言
GoF的23种设计模式,包括创建型、结构型和行为型,其涵盖了面向对象思想的精髓以及诸多细节。本文结合《设计模式》和《大话设计模式》,并用C++和Python实现了《大话设计模式》中的23种模式案例。原文首发于个人博客Jennica.Space。
案例实现
创建型模式
工厂方法模式(Factory Method)
- 工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪个类。
- 工厂方法把简单工厂的内部判断逻辑移到了客户端代码,本来需要修改工厂类,现在是修改客户端。
- 简单工厂模式违背了开放-封闭原则,工厂方法模式借助多态,克服了该缺点,却保持了封装对象创建过程的优点。
抽象工厂模式(Abstract Factory)
- 抽象工厂模式:提供一个创建一系列相关或互相依赖对象的接口,只需要知道对象的系列,无需知道具体的对象。
- 在客户端中,具体工厂类只在初始化时出现一次,更改产品系列即可使用不同产品配置。
- 利用简单工厂类替换抽象工厂类及其子类,可以使客户端不再受不同系列的影响。 结合反射机制,Assembly.Load(“程序集名称”).CreateInstance(“命名空间”.“类名”),可以直接通过字符串创建对应类的实例。所有在简单工厂中,都可以通过反射去除switch或if,解除分支判断带来的耦合。
- 反射中使用的字符串可以通过配置文件传入,避免更改代码。
单例模式(Singleton)
- 单例模式:让类自身保证它只有一个实例,并提供一个全局访问点。
- 多线程下单例模式可能失效,需要采取双重锁定的的方式,确保被锁定的代码同一时刻只被一个进程访问。
- 饿汉式单例:即静态初始化方式,在类初始化时产生私有单例对象,会提前占用资源;渴汉式单例:在第一次被引用时将自己初始化,会产生多线程访问安全问题,需要添加双重锁定。
建造者模式(Builder)
- 建造者模式:将复杂对象的创建与表示分开,使得相同的创建过程可以有不同的表示。用户只需制定需要建造的类型,不需要知道建造的过程和细节。
- 指挥者是建造者模式中重要的类,用于控制建造过程,也可以隔离用户与建造过程的关联。
- 建造者隐藏了产品的组装细节,若需要改变一个产品的内部表示,可以再定义一个具体的建造者。
- 建造者模式是在当前创造复杂对象的算法,独立于该对象的组成部分和装配方式时适用的模式。
原型模式(Prototype)
- 原型模式:用原型实例指定创建对象的种类,并通过拷贝这些原型创建对象。本质是从一个对象再创建另一个可定制的对象,并且不需要知道创建细节。
- 原型抽象类的关键是有一个Clone()方法,原型具体类中复写Clone()创建当前对象的浅表副本。
- 对.Net而言,由于拷贝太常用原型抽象类并不需要,在System命名空间中提供了ICloneable接口,其中唯一的方法就是Clone(),只要实现这个接口就可以完成原型模式。
- 原型拷贝无需重新初始化对象,动态获取对象的运行状态。既隐藏了对象创建的细节,又提升性能。
- 在具体原型类中,MemberwiseClone()方法是浅拷贝,对值类型字段诸位拷贝,对引用类型只复制引用但不会把具体的对象值拷贝过来。
- 比起浅拷贝,深拷贝把引用对象的变量指向新对象,而不是原被引用的对象。对于需要深拷贝的每一层,都需要实现ICloneable原型模式。
- 数据集对象DataSet,Clone()是浅拷贝,Copy()是深拷贝。
结构型模式
***模式(Proxy)
- ***模式:为其他对象提供一种***以控制对这个对象的访问。实际上是在访问对象时引入一定程度的间接性。
- 远程***:为一个对象在不同地址空间提供局部代表,隐藏一个对象存在于不同空间的事实。如.Net加入Web引用,引入WebService,此时项目会生成WebReference的文件夹,就是***。
- 虚拟***:根据需要创建开销很大的对象,通过它存放实例化需很长时间的真实对象。HTML中的多图,就是通过虚拟***代替了真实图片,存储路径和尺寸。
- 安全***:控制真实对象的访问权限,用于对象应该拥有不同的访问权限时。
- 智能指引:当调用真实对象时,***处理一些另外的事情。通过***在访问对象时增加一些内务处理。
适配器模式(Adapter)
- 适配器模式:当系统数据和行为都一致,只有接口不符合时,将一个类的接口转化为客户端期望的另一个接口。
- 适配器模式用于服用一些现存的类,常用在第三方接口或软件开发后期双方都不易修改的时候。
- 在.Net中DataAdapter是用于DataSet和数据源间的适配器,Fill更改DataSet适配数据源,Update更改数据源适配DataSet。
外观模式(Facade)
- 外观模式:为子系统中一组接口提供一个一致的界面,即定义一个高层接口,增加子系统的易用性。
- 外观模式完美体现了依赖倒转原则和迪米特法则。
- 设计初期阶段,在MVC三层架构中,任意两层间建立外观Facade。
- 子系统会因不断演化变得复杂,增加外观Facade提供简单简单接口减少依赖。
- 在维护一个大的遗留系统时,新的开发又必须依赖其部分功能。此时,开发一个外观Facade类,从老系统中抽象出比较清晰的简单接口。让新系统只与Facade交互,而Facade与遗留代码交互所有的工作。
装饰模式(Decorator)
- 装饰模式:动态的给一个对象添加一些额外的职能,把所需功能按顺序串联起来并进行控制。
- 每个要装饰的功能放在单独的类中,并让这个类包装它所要修饰的对象。当需要执行特殊行为时,客户端就可以根据需要有选择的、有顺序的使用装饰功能包装对象了。
- 装饰模式有效的把类的核心职能和装饰功能区分开了,并且可以去除相关类中重复的装饰逻辑。
桥接模式(Bridge)
- 对象的继承关系编译时已确定,所以无法在运行时修改从父类继承的实现。由于紧耦合,父类中任何的改变必然会导致子类发生变化。当需要复用子类,但继承下来的方法不合适时,必须重写父类或用其他类替代。这种依赖性限制了灵活性和复用性。
- 合成/聚合复用原则:尽量使用合成和聚合而不是继承。可以保证每个类封装集中在单个任务上,不会出现规模太大的类及继承结构。
- 桥接模式:抽象类和其派生类分离,各自实现自己的对象。若系统可以从多角度分类,且每种分类都可能变化,则把多角度分离独立出来,降低耦合。
享元模式(Flyweight)
- 享元模式:运用共享技术有效支持大量细粒度对象。
- 在享元模式对象内部不随环境改变的共享部分是内部状态,不可共享需要通过调用传递进来的参数是外部状态。
- 使用享元模式的场景包括,一个应用程序产生了大量的实例对象,占用了大量内存开销;或对象的大多数状态为外部状态,删除内部状态后可以用较少的共享对象来取代组对象。
- 应用场景有正则表达式、浏览器、机器人指令集等。
组合模式(Composite)
- 组合模式:将对象的组合以树形的层次结构表示,对单个对象和组合结构的操作具有一致性。
- 透明方法:叶子和分枝对外接口无差别;安全方法:分枝具有添加删除叶子的接口,低层抽象接口和叶子没有。
- 基本对象组合成组合,组合又可以被组合,不断递归下去,在任何用到基本对象的地方都可以使用组合对象。
行为型模式
职责链模式(Chain of Responsibility)
- 职责链模式:使多个对象都有机会处理请求,解除请求发送者和接收者的耦合。将对象连成一条链,并沿这条链传递请求直到请求被解决。
- 请求交付给最小接受者,职责链中每一环保存后继的引用,使得请求有序沿链传递。 通过合理设置后继以及分支关系,避免一个请求到了链末端依旧无法被处理,或因配置错误得不到处理的情况。
策略模式(Strategy)
- 面向对象中并非类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。
- 策略模式:定义算法家族并分别封装,他们完成的工作相同,只是实现不同,可以互相替换。继承有助于析取这些算法的公共功能。此模式让算法的变化不会影响到使用算法的用户。
- 策略与工厂模式结合,使客户端需要认识的类减少,耦合度更加降低。
- 策略模式可以简化单元测试,因为每个算法可以通过自己的接口单独测试。
- 只要在不同时间内应用不同的业务规则,就可以考虑用策略模式来处理这种变化的可能性。
状态模式(State)
- 拥有过多分支的过长方法违背了单一职责原则,而且当需求变化时修改代码往往会违背开放-封闭原则,应该将分支变成一不同小类,将状态的判断逻辑转移到小类中。
- 状态模式:一个对象可能拥有多种状态,当内在状态改变时允许改变行为。
- 状态模式的好处是将与特定状态有关的行为局部化,并将不同状态的行为分隔开。
观察者模式(Observer)
- 观察者模式:多个观察者对象同时监听某一主题(通知者)对象,当该主题对象状态变化时会通知所有观察者对象,使它们能更新自己。
- 具体观察者保存一个指向具体主题对象的引用,抽象主题保存一个抽象观察者的引用集合,提供一个可以添加或删除观察者的接口。
- 抽象模式中有两方面,一方面依赖另一方面,使用观察者模式可将两者独立封装,解除耦合。
- 观察者模式让主题和观察者双方都依赖于抽象接口,而不依赖于具体。
- 委托就是一种引用方法类型。委托可看作函数的类,委托的实例代表具体函数。在主题对象内声明委托,不再依赖抽象观察者。 一个委托可以搭载多个相同原形和形式(参数和返回值)的方法,这些方法不需要属于一个类,且被依次唤醒。
迭代器模式(Iterator)
- 迭代器模式:提供一种方法顺序遍历一个聚集对象,为不同的聚集结构提供遍历所需接口,而不暴露对象内部的表示。
- 在高级编程语言如c#、c++、java等,都已经把迭代器模式设计进语言的一部分。
- 迭代器模式分离了对象的遍历行为,既不暴露内部结构又可以让外部代码透明的访问集合内部的数据。
备忘录模式(Memento)
- 备忘录模式:不破坏封装,获取对象内部状态并在其之外保存该对象,以便其未来恢复到当前状态。
- Orginator负责创建Memento,Memento封装Originator状态细节,Caretaker负责保管和交付Memento。
- 备忘录模式适用于需要维护历史状态的对象,或只需要保存原类属性中的小部分。
命令模式(Command)
- 命令模式:将请求分装为对象,将请求和执行分开,可以用不同的请求对客户参数化。可以对请求排队、通过或否决、记录日志、撤销或重做。
- 基于敏捷开发原则,不要给代码添加基于猜测而实际不需要的功能,在需要的时候通过重构实现。
模板方法模式(Template Method)
- 模板方法模式:定义一个操作中的算法框架,将一些步骤延迟到子类中。子类在不改变框架的前提下就可以重新定义某些特定步骤。
- 当不变和可变的行为在子类中混到一起时,可以通过把重复的行为移到同一地方,帮助子类摆脱重复不变行为的纠缠。
中介者模式(Mediator)
- 中介者模式:用一个中介对象来封装一系列对象间的交互。
- 中介者模式在系统中易用也容易被误用,当系统中出现了多对多的交互复杂的对象群时,更应考虑设计的问题。
- 由于控制集中化,中介者模式将交互复杂性变成了中介者的复杂性,中介者类会比任何一个同事类都复杂。
- 中介者模式应用的场合有,一组对象以定义良好但复杂的方式进行通信,以及想定制一个分布在多个类中的行为却不想产生太多子类。
解释器模式(Interpreter)
- 解释器模式:给定一种语言,定义它文法的一种表示,再定义一个解释器,使用该表示来解释语言中的句子。
- 如果一种特定类型发生的频率足够高,就可以将其实例表达为一个句子,构建解释器来解析。
- 解释器模式就是用“迷你语言”来表现程序要解决的问题,将句子抽象为语法树。由于各个节电的类大体相同,便于修改、扩展和实现。
- 解释器为文法中的每条规则定义了一个类,当文法过多时将难以维护,建议使用其他技术如语法分析程序或编译器生成器处理。
访问者模式(Visitor)
- 访问者模式:在不改变各元素的前提下定义作用于这些类的新的操作。
- 访问者模式使用双分派,将数据结构和作用于结构上的操作解耦,意味着执行的操作决定于请求的种类和接收者的状态。
- 如果系统具有较为稳定的数据结构,又有易于变化的算法操作,则适合使用访问者模式。
对比总结
- 工厂方法模式:为不同子类创建不同工厂;
- 抽象工厂模式:为不同系列建造不同工厂;
- 单例模式:保证实例唯一;
- 建造者模式:为不同类组装出一套相同的方法;
- 原型模式:实现深拷贝。
- ***模式:控制访问;
- 适配器模式:将接口转换为客户端期望的形式;
- 外观模式:整理出一套可用接口;
- 装饰模式:动态修改类的职能;
- 桥接模式:将多角度分类分离独立;
- 享元模式:共享实例;
- 组合模式:递归生成树形结构的组合对象。
- 职责链模式:按顺序让负责的对象们依次处理;
- 策略模式:将算法族抽象封装;
- 状态模式:将复杂的状态转移方式下发到每个状态内部;
- 观察者模式:发布和订阅;
- 迭代器模式:遍历容器;
- 备忘录模式:在对象之外备份及恢复;
- 命令模式:封装请求与执行分开;
- 模板方法模式:提炼共有方法。
- 中介者模式:用中介对象封装交互。
- 解释器模式:迷你语言;
- 访问者模式:解耦数据结构及操作。