设计模式的几大原则:
设计原则名称 | 设计原则描述 | 重要性 |
单一职责原则 | - 尽量避免相同的职责分散在不同的类当中 - 避免一个类承担过多不同类型的职责 | ★★★★☆ |
开闭原则 | 对扩展开放,对修改封闭。对于新增需求,尽量少修改已有代码。 | ★★★★★
|
依赖倒转原则 | 针对抽象层编程,不要针对具体类编程。 | ★★★★★ |
里氏替换原则 | 子类型必须能够替换它们的父类型 | ★★★★☆ |
合成复用原则 | 尽量少用继承,多用组合/聚合 | ★★★★☆ |
接口隔离原则 | 使用多个专门的接口来取代一个统一的接口 | ★★☆☆☆ |
迪米特法则 | 一个软件实体对其它实体的引用越少越好 | ★★★☆☆ |
设计模式分为创建型模式、结构型模式、行为模式。
这里先总结23种设计模式中的13种,也是之前项目用到的或者看到过的设计模式。
创建型模式(对象实例化的模式,创建型模式用于解耦对象的实例化过程):工厂模式,抽象工厂模式,单例模式,建造者模式,原型模式
结构型模式(把类或对象结合在一起形成一个更大的结构):桥接模式,适配器模式,装饰器模式,代理模式
行为模式(类和对象如何交互,及划分责任和算法):模板模式,策略模式,状态模式,观察者模式
创建型模式
简单工厂模式
经常在系统开发中用到,但是这并不是Factory 模式的最大威力所在
工厂模式
Fact ory 模式不单是提供了创建对象的接口,其最重要的是延迟了子类的实例化
即:让子类来决定要创建哪个对象
Factory 模式对于对象的创建给予开发人员提供了很好的实现策略,但是,Factory模式仅仅局限于一类类(就是说Product 是一类,有一个共同的基类),如果我们要为不同类的类提供一个对象创建的接口,那就要用AbstractFactory了。
关键代码:创建过程在其子类执行。
抽象工厂模式
AbstractFactory模式就是用来解决这类问题的:要创建一组相关或者相互依赖的对象。
一、一句話概括工厂模式
簡単工厂:一个工厂類,一个産品抽象類。
工厂方法:多个工厂類,一个産品抽象類。
抽象工厂:多个工厂類,多个産品抽象類。
二、生活中的工厂模式
簡単工厂類:一个麦当労店,可以生産多種漢堡。
工厂方法類:一个麦当労店,可以生産多種漢堡。一个肯德基店,也可以生産多種漢堡。
抽象工厂類:百盛集団下有肯德基和百事公司,肯德基生産漢堡,百事公司生成百事可楽。
单例模式
面试常考的一种设计模式
记住几个关键点:
一个static成员变量
非public的构造函数(protected和private都可以)
getInstance()的接口来获得这个唯一的实例
懒汉模式和饿汉模式
用一段代码直接说明:
Singleton* Singleton::Instance()
{
if (_instance == 0)
{
_instance = new Singleton (); //延迟实例化,就是懒汉模式,否则就是饿汉模式
}
Singleton 模式在开发中经常用到,且不说我们开发过程中一些变量必须是唯一的,比如说打印机的实例等等。
建造者模式
当我们要创建的对象很复杂的时候(通常是由很多其他的对象组合而成),我们要要复杂对象的创建过程和这个对象的表示(展示)分离开来,这样做的好处就是通过一步步的进行复杂对象的构建,由于在每一步的构造过程中可以引入参数,使得经过相同的步骤创建最后得到的对象的展示不一样
应用实例:去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的"套餐"。
三个角色:建造者、具体的建造者、监工、使用者(严格来说不算)
- 建造者角色:定义生成实例所需要的所有方法;
- 具体的建造者角色:实现生成实例所需要的所有方法,并且定义获取最终生成实例的方法;
- 监工角色:定义使用建造者角色中的方法来生成实例的方法;
- 使用者:使用建造者模式。
Builder 模式的关键是其中的Director 对象并不直接返回对象,而是通过一步步(BuildPartA ,BuildPartB ,BuildPartC )来一步步进行对象的创建。当然这里Director 可以提供一个默认的返回对象的接口(即返回通用的复杂对象的创建,即不指定或者特定唯一指定BuildPart 中的参数)。
Builder 模式和AbstractFactory模式在功能上很相似,因为都是用来创建大的复杂的对象,它们的区别是:Builder 模式强调的是一步步创建对象,并通过相同的创建过程可以获得不同的结果对象,一般来说Builder 模式中对象不是直接返回的。而在AbstractFactory 模式中对象是直接返回的,AbstractFactory 模式强调的是为创建多个相互依赖的对象提供一个同一的接口。
注意事项:与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。
定义中“将一个复杂的构建过程与其表示相分离”,表示并不是由建造者负责一切,而是由监工负责控制(定义)一个复杂的构建过程,由各个不同的建造者分别负责实现构建过程中所用到的所有构建步骤。不然,就无法做到“使得同样的构建过程可以创建不同的表示”这一目标。
原型模式
Prototype 模式也正是提供了自我复制的功能,就是说新对象的创建可以通过已有对象进行创建。
Prototy pe 模式的结构和实现都很简单,其关键就是(C++ 中)拷贝构造函数的实现方式,这也是C++ 实现技术层面上的事情
关键代码:
ConcretePrototype::ConcretePrototype(const ConcretePrototype & cp )
{
cout <<"ConcretePrototype copy ..."<<endl; //拷贝构造(浅拷贝),完成自我复制,真实代码中,要考虑指针共享内存单元的问题。
}
Prototype* Concrete Prototype:: Clone() const
{
return new ConcretePrototype(*this);//调用拷贝构造函数
}
int main(int argc,char * argv[ ])
{
Prototype * p = new ConcretePrototype() ;
Prototype * p1 = p->Clone();
return 0;
}
结构型模式
总结面向对象实际上就两句话:一是松耦合(Coupling),二是高内聚(Cohesion)。面向对象系统追求的目标就是尽可能地提高系统模块内部的内聚(Cohesion)、尽可能降低模块间的耦合(Coupling )
桥接模式
系统被分为两个相对独立的部分,左边是抽象部分,右边是实现部分,这两个部分可以互相独立地进行修改(墙上的开关,可以看到的开关是抽象的,不用管里面具体怎么实现的):
Bridge是设计模式中比较复杂和难理解的模式之一,也是OO开发与设计中经常会用到的模式之一。使用组合(委托)的方式将抽象和实现彻底地解耦,这样的好处是抽象和实现可以分别独立地变化,系统的耦合性也得到了很好的降低
桥接模式中有四个角色:
抽象化角色:使用实现者角色提供的接口来定义基本功能接口。
持有实现者角色,并在功能接口中委托给它,起到搭建桥梁的作用;
注意,抽象化角色并不是指它就是一个抽象类,而是指抽象了实现。
改善后的抽象化角色:作为抽象化角色的子类,增加新的功能,也就是增加新的接口(方法);与其构成类的功能层次结构;
实现者角色:提供了用于抽象化角色的接口;它是一个抽象类或者接口。
具体的实现者角色:作为实现者角色的子类,通过实现具体方法来实现接口;与其构成类的实现层次结构。
如果抽象和实现两者做不到独立地变化,就不算桥接模式。
优点: 1、抽象和实现的分离。 2、优秀的扩展能力。 3、实现细节对客户透明。
缺点:桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
使用Bridge 模式带来问题方式的解决方案的根本区别在于是通过继承还是通过组合的方式去实现一个功能需求。因此面向对象分析和设计中有一个原则就是:尽量少用继承,多用组合/聚合
Favor Composition Over Inheritance。其原因也正在这里
适配器模式
实际上在软件系统设计和开发中,这种问题也会经常遇到:我们为了完成某项工作购买了一个第三方的库来加快开发。这就带来了一个问题:我们在应用程序中已经设计好了接口,与这个第三方提供的接口不一致,为了使得这些接口不兼容的类(不能在一起工作)可以在一起工作了,Adapter 模式提供了将一个类(第三方库)的接口转化为客户(购买使用者)希望的接口。
Adapter 模式的两种类别:类模式和对象模式
注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。
在Adapter 模式的结构图中可以看到,类模式的Adapter 采用继承的方式复用Adaptee的接口,而在对象模式的Adapter 中我们则采用组合的方式实现Adaptee的复用
优点: 1、可以让任何两个没有关联的类一起运行。 2、提高了类的复用。 3、增加了类的透明度。 4、灵活性好。
缺点: 1、过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
装饰器模式(Python也有类似思想=>@符)
在OO设计和开发过程,可能会经常遇到以下的情况:我们需要为一个已经定义好的类添加新的职责(操作),通常的情况我们会定义一个新类继承自定义好的类。通过继承的方式解决这样的情况还带来了系统的复杂性,因为继承的深度会变得很深。
而Decorator 提供了一种给类增加职责的方法,不是通过继承实现的,而是通过组合。
如下图所示:ConcreteDecorator 给ConcreteComponent 类添加了动作AddedBehavior。
关于图片中的问号的解释:
首先,ConcreteComponent 和Decorator 需要有同样的接口,ConcreteComponent 和Decorator 有一个公共基类,就可以利用OO中多态的思想来实现只要是Component 型别的对象都可以提供修饰操作的类,这种情况下你就算新建了100 个Component 型别的类ConcreteComponent ,也都可以由Decorator一个类搞定。这也正是Decorator 模式的关键和威力所在了。
当然如果你只用给Component 型别类添加一种修饰,则Decorator这个基类就不是很必要了。
关键代码: 1、Component 类充当抽象角色,不应该具体实现。 2、修饰类引用和继承 Component 类,具体扩展类重写父类方法。
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
缺点:多层装饰比较复杂。
注意事项:可代替继承。
Decorator 模式除了采用组合的方式取得了比采用继承方式更好的效果,Decorator 模式还给设计带来一种“即用即付”的方式来添加职责。在OO设计和分析经常有这样一种情况:为了多态,通过父类指针指向其具体子类,但是这就带来另外一个问题,当具体子类要添加新的职责,就必须向其父类添加一个这个职责的抽象接口,否则是通过父类指针是调用不到这个方法了。这样处于高层的父类就承载了太多的特征(方法),并且继承自这个父类的所有子类都不可避免继承了父类的这些接口,但是可能这并不是这个具体子类所需要的。而在Decorator 模式提供了一种较好的解决方法,当需要添加一个操作的时候就可以通过Decorator 模式来解决,你可以一步步添加新的职责。
代理模式
一个类代表另一个类的功能
客户端无法直接操作实际对象。那么为什么无法直接操作?一种情况是你需要调用的对象在另外一台机器上,你需要跨越网络才能访问,如果让你直接coding去调用,你需要处理网络连接、处理打包、解包等等非常复杂的步骤,所以为了简化客户端的处理,我们使用代理模式,在客户端建立一个远程对象的代理,客户端就象调用本地对象一样调用该代理,再由代理去跟实际对象联系,对于客户端来说可能根本没有感觉到调用的东西在网络另外一端,这实际上就是Web Service的工作原理。另一种情况虽然你所要调用的对象就在本地,但是由于调用非常耗时,你怕影响你正常的操作,所以特意找个代理来处理这种耗时情况,一个最容易理解的就是Word里面装了很大一张图片,在word被打开的时候我们肯定要加载里面的内容一起打开,但是如果等加载完这个大图片再打开Word用户等得可能早已经跳脚了,所以我们可以为这个图片设置一个代理,让代理慢慢打开这个图片而不影响Word本来的打开的功能。申明一下我只是猜可能Word是这么做的,具体到底怎么做的,俺也不知道。
意图:为其他对象提供一种代理以控制对这个对象的访问。
注意事项: 1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。 2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
行为模式
模板模式
其关键是将通用算法(逻辑)封装起来,而将算法细节让子类实现(多态)。 唯一注意的是我们将原语操作(细节算法)定义未保护(Protected )成员,只供模板方法调用(子类可以)。
意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
主要解决:一些方法通用,却在每一个子类都重新写了这一方法。
关键代码:在抽象类实现,其他步骤在子类实现。
优点: 1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。
缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
Template 模式获得一种反向控制结构效果,这也是面向对象系统的分析和设计中一个原则DIP(依赖倒置:Dependency Inversion Principles )。其含义就是父类调用子类的操作(高层模块调用低层模块的操作),低层模块实现高层模块声明的接口。这样控制权在父类(高层模块),低层模块反而要依赖高层模块。
策略模式
Strategy 模式和Template 模式要解决的问题是相同(类似)的,都是为了给业务逻辑(算法)具体实现和抽象接口之间的解耦。Strategy 模式将逻辑(算法)封装到一个类(Context )里面,通过组合的方式将具体算法的实现在组合对象中实现,再通过委托的方式将抽象接口的实现委托给组合对象实现
这里的关键就是将算法的逻辑抽象接口(DoAction )封装到一个类中(Context ),再通过委托的方式将具体的算法实现委托给具体的Strategy 类来实现(ConcreteStrategeA 类)。
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。
策略模式和模板模式解决的是类似的问题,但是策略模式是逻辑算法封装在一个类中,采取组合的方式解决这个问题。
模板模式是采用继承的方式,将逻辑算法放在抽象基类中,并定义好接口,子类中实现细节。
观察者模式
Observer 模式要解决的问题为:建立一个一(Subject)对多(Observer )的依赖关系,并且做到当“一”变化的时候,依赖这个“一”的多也能够同步改变。最常见的一个例子就是:对同一组数据进行统计分析时候,我们希望能够提供多种形式的表示(例如以表格进行统计显示、柱状图统计显示、百分比统计显示等)。这些表示都依赖于同一组数据,我们当然需要当数据改变的时候,所有的统计的显示都能够同时改变。
这里的目标Subject 提供依赖于它的观察者Observer 的注册(Attach)和注销(Detach )操作,并且提供了使得依赖于它的所有观察者同步的操作(Notify)。观察者Observer 则提供一个Update 操作,注意这里的Observer 的Update 操作并不在Observer 改变了Subject 目标状态的时候就对自己进行更新,这个更新操作要延迟到Subject 对象发出Notify 通知所有Observer 进行修改(调用Update )。
关键代码:在Observer 模式的实现中,Subject 维护一个list 作为存储其所有观察者的容器。每当调用Notify 操作就遍历list 中的Observer 对象,并广播通知改变状态(调用Observer 的Update操作)。目标的状态state 可以由Subject 自己改变,也可以由Observer 的某个操作引起state 的改变(可调用Subject 的SetState 操作)。Notify 操作可以由Subject 目标主动广播(推消息)
,也可以由Observer 观察者来调用(拉消息)(因为Observer 维护一个指向Subject 的指针)。
仔细分析定义,要精确理解观察者模式主要注意三点:
1.定义了对象间的一对多依赖关系;
2.当 Subject 对象的状态发生改变时,所有依赖于该 Subject 对象的 Observer 对象都会得到通知;
3.Observer 对象得到通知后,会自动更新,而不是被动;
其它的所有点都是细枝末节,由具体业务需求来决定。比如:
应该在什么时候订阅主题(或者说注册观察者)?是实例化观察者对象的同时?还是由客户自主决定?
是否应该实现取消订阅功能(或者说取消注册)?
主题对象通知观察者时,是否携带消息?换句话说,是“推”消息?还是“拉”消息?
命令模式
Command 模式通过将请求封装到一个对象(Command )中,并将请求的接受者Receiver 存放到具体的ConcreteComma nd类中,从而实现调用操作的对象和操作的具体实现者之间的解耦。
Command 模式结构图中,将请求的接收者(处理者)放到Command 的具体子类
ConcreteCommand 中,当请求到来时(Invoker 发出Invoke 消息激活Command 对象),
ConcreteCommand 将处理请求交给Receiver 对象进行处理。
关键代码:定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口
在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合