设计模式描述的是类与相互通讯的对象之间的关系,包括角色,责任等。
目录
本文是参考 菜鸟教程/设计模式 所做的个人总结,这里仅总结对设计模式的理解,没有太多的代码。
uml图中的一些解释:
-------泛化,可以用来表示继承关系
-----实现,实现接口
-----依赖,方法参数需要传入另一个类的对象,就表示依赖这个类
-----关联,表示类与类之间的联接,它使一个类知道另一个类的属性和方法,这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的。
-----聚合,强的关联关系。聚合是整体和个体之间的关系,即has-a的关系
-----组合,关联关系的一种特例。组合是一种整体与部分的关系,即contains-a的关系
设计原则:
1.依赖倒置(DIP)高程模块不应该依赖于低层,二者都应该依赖u于抽象。抽象不应该依赖于细节,实现细节应该依赖于抽象。
(简单来说,使用抽象为中间项,调用 ----》抽象---》实现。实现细节继承于抽象,抽象改变,实现跟着改变,而不是实现细节拉着抽象改变)
2.开发封闭原则:对扩展开发,对更改封闭。类模块应该扩展,不可修改。
3.单一职责原则(srp):一个类应该仅有一个I一年期变化的原因。变化的方向隐含着类的责任。
4.里氏替换原则(LSP):子类必须能够替换对应的基类(IS-A,就是public继承)
5.接口隔离原则(ISP): 不应该强迫客户程序依赖他们不用的方法,接口应该小而完备
6.优先使用对象组合,而不是继承:类继承为“白箱复用”,组合为“黑箱复用”。相对来说继承的父子类耦合性高,组合低
7.封装变化点:(感觉就是说封装)
8.针对接口编程,而不是针对实现编程:不将变量类型生命为某个具体类,二是声明为接口(感觉和第一差不多一个意思)
创建模式
这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
- 工厂模式(Factory Pattern)
- 抽象工厂模式(Abstract Factory Pattern)
- 单例模式(Singleton Pattern)
- 建造者模式(Builder Pattern)
- 原型模式(Prototype Pattern)
工厂模式
使用一个类(工厂)来选择某些类(产品)里面的其中一个创建
好处是:以后只需要记住工厂和产品的关键字就行了,不需要记住具体的类
优点是符合开闭原则,支持扩展产品类,不支持修改。需要添加新产品直接添加新的产品类和对应的新的工厂类就行了(一个工厂对应一个产品)
对比直接new的优势,当产品很简单,一个new就能搞定的话,直接new有优势,但是,当产品很复杂,由多个类构成的话,工厂模式的优势就体现出来了,如果直接new,每使用这个复杂类都要new好多个类
补充代码:
#include <iostream>
#include <string>
class project //抽象产品
{
public:
virtual void create() = 0;
};
class kileC51 : public project //具体产品
{
public:
virtual void create()
{
std::cout << "c51" << std::endl;
}
};
class kileArm : public project//具体产品
{
public:
virtual void create()
{
std::cout << "arm" << std::endl;
}
};
class factory //抽象工厂
{
public:
virtual project *create() = 0;
//virtual project *create2() = 0;
};
class kileC51Factory : public factory//具体工厂
{
public:
project *create()
{
return new kileC51();
}
};
class kileArmFactory : public factory//具体工厂
{
public:
project *create()
{
return new kileArm();
}
};
int main()
{
project *c51 = 0;
project *arm = 0;
factory *c51F = 0;
factory *armF = 0;
c51F = new kileC51Factory;
armF = new kileArmFactory;
c51 = c51F->create();
c51->create();
arm = armF->create();
arm->create();
}
抽象工厂模式
在工厂的基础上整合多个工厂。(多了一个整合的工厂基类,本质是多个工厂模式)
这种整合,在实现的时候需要在抽象工厂上定义具体的产品种类,一旦这么做了,后期需要修改具体的产品种类时将会是一个巨大的工程,所以需要一开始就严格定义好接口的new产品的种类
对比工厂方法模式,优点是减少工厂类(工厂方法模式是一对一,抽象工厂是一对多,如下代码,4个具体的产品,工厂方法需要4个工厂,而抽象工厂方法仅需要2个),缺点是不一定遵循开闭原则。
补充代码
#include <iostream>
#include <string>
class project //抽象产品
{
public:
virtual void create() = 0;
};
class kileC51 : public project //具体产品
{
public:
virtual void create()
{
std::cout << "c51" << std::endl;
}
};
class kileArm : public project//具体产品
{
public:
virtual void create()
{
std::cout << "arm" << std::endl;
}
};
class iar1 : public project//具体产品
{
public:
virtual void create()
{
std::cout << "iar1" << std::endl;
}
};
class iar2 : public project//具体产品
{
public:
virtual void create()
{
std::cout << "iar2" << std::endl;
}
};
class factory //抽象工厂
{
public:
//这里要定死种类的,一旦变更会非常麻烦,继承的所有子类都要修改
virtual project *create() = 0;
virtual project *create2() = 0;
};
class Factory1 : public factory//具体工厂
{
public:
project *create()
{
return new kileC51();
}
project *create2()
{
return new iar1();
}
};
class Factory2 : public factory//具体工厂
{
public:
project *create()
{
return new kileArm();
}
project *create2()
{
return new iar2();
}
};
单例模式
把构造函数私有化,使其不能被创建,不能继承,只有唯一一个
用于解决一个全局使用的类频繁地创建与销毁,减少开销
单例模式有很多种创建方法,常用的是饿汉式:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
建造者模式
使用场景: 1、需要生成的对象具有复杂的内部结构。 2、需要生成的对象内部属性本身相互依赖。
建造者模式是把一个复杂的类细分为一个个小类的组合(可能是同一层关系,也可能是上下级关系),例如
一个肯德基的套餐可能有汉堡、可乐等等,其他套餐也是这种基本类型的其他组合方式
这些套餐就是复杂的类。
具体流程:点一个套餐------->套餐分配员分(MealBuilder)配套餐--------->把包装好的菜放到一个集合(Meal)里-------->上包装(Packing)(new)--------->做好菜(new)
原型模式
实际上感觉和c++的拷贝构造一样,实现上是要在类内写一个成员new一个this(this)
java的实现依赖于clone()函数(被复制的类的成员函数)复制一个现有对象代替new
拷贝构造与原型模式区别:
相同点:原型模式和拷贝构造函数都是要产生对象的复制品。
不同点:原型模式实现的是一个clone接口,注意是接口,也就是基于多态的clone虚函数。也就是说原型模式能够通过基类指针来复制派生类对象。拷贝构造函数完不成这样的任务。
原型模式的核心是克隆,构造函数只是克隆的一个办法而已。
行为型模式
这些设计模式特别关注对象之间的通信。
- 责任链模式(Chain of Responsibility Pattern)
- 命令模式(Command Pattern)
- 解释器模式(Interpreter Pattern)
- 迭代器模式(Iterator Pattern)
- 中介者模式(Mediator Pattern)
- 备忘录模式(Memento Pattern)
- 观察者模式(Observer Pattern)
- 状态模式(State Pattern)
- 空对象模式(Null Object Pattern)
- 策略模式(Strategy Pattern)
- 模板模式(Template Pattern)
- 访问者模式(Visitor Pattern)
责任链模式
使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系
注:这里的请求、命令正是可以和命令模式进行结合的地方
做法:每个消息都会有一个附带一个处理等级,达不到处理等级回向下传递,怎么传递?每个接收者对象都会有一个设置下一个处理请求对象setNextLogger的成员函数,该函数:
public void setNextLogger(Logger nextLogger){
this.nextLogger = nextLogger;
}
//使用时会设置传递对象
public class ChainPatternDemo {
private static AbstractLogger getChainOfLoggers(){
AbstractLogger errorLogger = new ErrorLogger(AbstractLogger.ERROR);
AbstractLogger fileLogger = new FileLogger(AbstractLogger.DEBUG);
AbstractLogger consoleLogger = new ConsoleLogger(AbstractLogger.INFO);
errorLogger.setNextLogger(fileLogger);
fileLogger.setNextLogger(consoleLogger);
return errorLogger;
}
会设置达不到处理等级时传递的对象 。
命令模式
把命令(请求)封装到一个类,然后具体的子类对象通过输入的形参执行者绑定执行者,把请求者和执行者通过命令类解耦
这样做的好处是使执行者能够对应多个请求者,通过命令类
例如:一个开关对象的请求者可以命令电灯对象的执行者是否开关
解释器模式
对于一些固定文法构建一个解释句子的解释器,当一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题
虽然使用解释器模式的实例不是很多,但对于满足以上特点,且对运行效率要求不是很高的应用实例,如果用解释器模式来实现,其效果是非常好的
常用于语法树,编译器,符号解析,正则表达等
解释器模式是一种类行为型模式,其主要优点如下。
- 扩展性好。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
- 容易实现。在语法树中的每个表达式节点类都是相似的,所以实现其文法较为容易。
解释器模式的主要缺点如下。
- 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
- 会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护。
- 可应用的场景比较少。在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。
迭代器模式
迭代器模式的目的是提供一种方法顺序访问聚合对象中的元素,而不暴露其内部表示
例如c++中的stl迭代器
一个做法是在需要迭代器的类中写一个创建(new)迭代器对象的成员方法,每次类内的数据++一次就判断一次迭代器真假,迭代器超过数据长度为假。上面是一个描述顺序结构的简单迭代器写法,以前在c++primer上看到的实现迭代器的做法。
总之,迭代器支持的类需要在类内关联这个迭代器对象,如上uml图。
中介者模式
定义一个中介者对象, 封装一系列对象的交互关系, 使得各对象不必显示的相互引用, 从而使其耦合松散, 而且可以独立的改变它们的交互
//ChatRoom.java
import java.util.Date;
public class ChatRoom {
public static void showMessage(User user, String message){
System.out.println(new Date().toString()
+ " [" + user.getName() +"] : " + message);
}
}
//创建 user 类。
//User.java
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User(String name){
this.name = name;
}
public void sendMessage(String message){
ChatRoom.showMessage(this,message); //这里就是使用中介者处理交互信息(显示通信内容)
}
}
//使用 User 对象来显示他们之间的通信。
//MediatorPatternDemo.java
public class MediatorPatternDemo {
public static void main(String[] args) {
User robert = new User("Robert");
User john = new User("John");
robert.sendMessage("Hi! John!");
john.sendMessage("Hello! Robert!");
}
}
//执行程序,输出结果:
//Thu Jan 31 16:05:46 IST 2013 [Robert] : Hi! John!
//Thu Jan 31 16:05:46 IST 2013 [John] : Hello! Robert!
备忘录模式
描述:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
备忘录模式随处可见,浏览器退回,windows的ctrl+z,棋牌游戏的悔棋等等。
做法是依赖原发器,备忘录,负责人3个角色,备忘录角色负责保存原发器的状态(一般来说是保存原发器类的成员变量),负责人负责管理备忘录(添加删除原发器某一时刻的状态,什么时候启用备忘录等需要备忘录的操作)。
例如:原发器(棋手)下棋时备忘录记录(调用备忘录记录,或者说new一个备忘录类记录这时棋子的状态)这手棋前后的角色,坐标,悔棋时负责人拿出备忘录记录(添加到备忘录数组里的)的状态恢复棋盘。
观察者模式
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
做法是再被订阅者中维护一个订阅表,属性更新时告诉订阅表中的对象更新(调用订阅表中的对象的函数),
订阅者使用被订阅者的订阅函数把自己加到订阅表里
public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject(); //创建被订阅者
new HexaObserver(subject); //创建观察者并使用构造函数调用subject的订阅方法来订阅被观察者
new OctalObserver(subject);
new BinaryObserver(subject);
System.out.println("First state change: 15");
subject.setState(15); //修改属性,会调用订阅表里的观察者对象的成员方法告诉订阅者有修改
System.out.println("Second state change: 10");
subject.setState(10);
}
}
状态模式
实际上状态模式和if...else的功能时一样的,判断不同的条件执行不同的代码,不过如果有很多条件时使用if...else代码会非常的臃肿,难以解读,并且难以维护,例如有1到100的100个条件(状态)时你就要写100个if...else,而且每个分支都会有几十行代码甚至几百行代码时(实际可能远不止如此)阅读起来就会非常的可怕,这时使用状态模式相对而言能够简单些(如果当代码两到一定层次时,我觉得没差多少)
做法: 创建2个角色,状态接口state(可能有很多子类状态),不同状态的不同输出类context,实际状态对象做2件事情,1.给context设置当前状态,2.在该状态时要做什么。context要做的很简单,输出某状态下的动作,所有输出在context中(他会有一个成员方法,比如getstate()返回一个状态对象,然后动作都是getstate().xxx)。
策略模式
策略模式和状态模式感觉差不多的,不过状态模式多一个设置状态的工作,而策略模式是可以直接使用的。他们都是可以在if住够多且复杂的时候代替if使代码容易阅读维护。(判断使用哪个策略可以工厂模式)
public class StrategyPatternDemo {
public static void main(String[] args) { //判断使用什么策略交由其他了完成,例如工厂类
Context context = new Context(new OperationAdd()); //设置了对应的策略
System.out.println("10 + 5 = " + context.executeStrategy(10, 5)); //使用策略
context = new Context(new OperationSubstract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
context = new Context(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
}
模板模式
思路:将一些特定步骤的具体实现、延迟到子类。使得可以在不改变算法流程的情况下,通过不同的子类、来实现“定制”流程中的特定的步骤
感觉和普通的设计基类子类没有什么大区别。就是要把所有的方法,变量全部在基类中定下来,公共的方法写在基类中,不一样的方法写成接口,在所有子类中实现
空对象模式
既是把对象当作NULL来使用,当有些操作不规范是返回空对象事很好的方法
做法是把实现接口的其中一个子类的成员变量设为空值的标志,如整形设为0,字符串设为this is NULL等
例如:当工厂类的判断创建类的字符串入参输入不规范时返回一个空对象(有点像swich的default)
访问者模式
结构型模式
这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
- 适配器模式(Adapter Pattern)
- 桥接模式(Bridge Pattern)
- 过滤器模式(Filter、Criteria Pattern)
- 组合模式(Composite Pattern)
- 装饰器模式(Decorator Pattern)
- 外观模式(Facade Pattern)
- 享元模式(Flyweight Pattern)
- 代理模式(Proxy Pattern)
适配器模式
当要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。
例如:一个MP3播放器本来只能播放mp3格式的文件,现在要扩展可以播放MP4和vlc文件。
MedioaAdapter适配器想要实现播放mp4格式,也要在类里new一个扩展的格式类(适配器继承或依赖已有的对象,实现想要的目标接口)。这样比直接在原MP3播放器类new新的扩展类好在哪里?
假如有很多扩展格式的话就不要一个一个的加入,只要加入到适配器对象,再调用适配器就解决了,把扩展和原来的类解耦,方便以后扩展。
适配器模式的优点:
更好的复用性
系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。
更好的扩展性
在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。
适配器模式的缺点:
过多的使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是A接口,其实内部被适配成了B接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
桥接模式
桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化,因为在在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活,所以把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。
例如:设计一个能画形状和形状颜色的draw类,有2种方法:
1.继承,一个抽象形状类有多个具体形状类继承,每个具体形状类又有具体颜色类继承(17个类)
2.一个类控制形状,一个类控制颜色,最后组合(颜色组合图形或者图形组合颜色都可以),这个就是桥接(9个类)
过滤器模式
和其名字一样,使用一个类,根据另一个需要过滤的类的条件进行过滤归类划分。
关键实现:实现一个存有被过滤的类的数组的接口,由具体的子类根据条件决定那些类能够保存到该对象的数组里(除了被过滤类的条件,还会有些组合类,比如和,或这些,符合和的条件的被过滤类才能够保存,或也一样)。
总之,和创建多个被过滤类数组,,使用if判断什么类加到什么数组的功能一样。
优点:简单,解耦,使用方便。(事实上不是很明白他的好处)
例如:人类对象,有姓名,性别,婚配等属性,我们可以设计一个过滤器对象对其条件(男人,女人,是否婚配和组合条件等)进行过滤。
组合模式
类对象里有分部分整体的,能够像树一样一层一层向下,层次分明,有主次关系的对象,合适使用组合模式
例如:
关键实现:在类内创建一个自身的数组,层层向下
装饰器模式
注意:装饰器对象也和被装饰对象一样,继承同一个接口 使用同一个抽象方法 draw();
装饰器与适配器都有一个别名叫做 包装模式(Wrapper),它们看似都是起到包装一个类或对象的作用,但是使用它们的目的很不一一样。适配器模式的意义是要将一个接口转变成另一个接口,它的目的是通过改变接口来达到重复使用的目的。
而装饰器模式不是要改变被装饰对象的接口,而是恰恰要保持原有的接口,但是增强原有对象的功能,或者改变原有对象的处理方式而提升性能。所以这两个模式设计的目的是不同的
外观模式
外观模式是一个很简单的模式,主要的内容就是把系统多数的复杂的东西全部都集合起来,给客户暴露一个访问的接口,来隐藏系统的复杂性。
优点: 1、减少系统相互依赖。 2、提高灵活性。 3、提高了安全性, 4、在层次化结构中,可以使用外观模式定义系统中每一层的入口。
缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
关键实现:在外观类中new其他类的对象,写一个成员方法,里面调用其他对象的方法。(一个外观类整和其他类的各种方法)
享元模式
主要用于减少创建对象的数量,以减少内存占用和提高性能,
何时使用: 1、系统中有大量对象。 2、这些对象消耗大量内存。 3、这些对象的状态大部分可以外部化。 4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。 5、系统不依赖于这些对象身份,这些对象是不可分辨的。
关键实现:创建对象,保存到数组里(一般是哈希容器)用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。
代理模式
不直接访问(对象在远程上,或者涉及到安全问题等),通过第三方对象进行访问并对其进行控制。
例如:windows的快捷键访问,linux的软连接,代售点买车票等等
代理模式按照职责划分可以分为;1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。
关键实现:由代理类内部new需要访问的对象,通过成员方法对该对象进行访问(可能会做什么处理)。(代理模式有很多实现方法,这是其中之一)。
(待补充.....)