1. 目录
2. 简介
本文是《深入浅出设计模式》的读书笔记
下文说到的接口分为广义接口和狭义接口。
广义接口包括接口和抽象类。而狭义接口则单指编程语言中的接口(interface)。
2.1. 设计原则
2.1.1. 变化与不变化
找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混合在一起。
把会变化的部分取出并封装起来,一边以后可以轻易地改动或扩充此部分,这样就不会影响到不需要改动的部分了。
2.1.2. 针对接口编程而不是针对实现编程
这里的接口是广义的接口。
面向接口编程是指设计类时,把实现的细节放在接口中,把一些实质动作,会改变的动作抽象成接口,然后在接口中实现,因为这些实现细节是很容易改变的有,因此我们要把他组件化,使其可复用。比如实现“小明在吃苹果”可以针对接口实现“{人接口}在{动作接口}{对象接口}”,然后在三个接口中分别实现小明类,吃动作类,苹果类。以后若还有类似需求,比如小明在切苹果。只要实现切动作类即可。具体的例子看下面。
比如《深入浅出设计模式》中第16页的例子。
要实现鸭子类的“叫”动作和“飞”动作。
如果是面向实现编程则简单的硬编码即可。
而如果面向接口编程则是,定义“叫动作”接口和“飞动作”接口,根据鸭子种类不同(绿头鸭嘎嘎叫,会飞,木头鸭不会叫不会飞,充气鸭吱吱叫不会飞)来组合,使代码可复用,不用一次次定义很多鸭子类。如下:
public class Duck{
QuackBehavior qb; //叫动作接口成员
FlyBehavior fb; //飞动作接口成员
public void quack(){ //叫动作函数
qb.quack();
}
public void fly(){ //飞动作函数
fb.fly();
}
}
//实现叫动作接口示例
//木头鸭不会叫
public class SlientQuack implement QuackBehavior{
public void quack(){
system.out.printout("I can not quack");
}
}
//创建木头鸭子类
public class WoolDuck extend Dcuk{
public WoolDuck(){
this.qb= new SlientQuack();
this.fb = new CanNotFly();
....
}
}
这样一来,在增加新鸭子类的时候,可以通过硬编码方式静态添加,也可以通过构造函数,传入接口动作参数动态生成新的鸭子类。
这样就把实现的细节委托给了接口对象。
2.1.3. 为了交互对象之间的松耦合设计而努力
这能简历有弹性的OO系统,能够应付变化,因为对象之间的相互依赖降到了最低。
2.1.4. 开放-关闭模式
对修改关闭,对扩展开放。
一个优秀的架构应该具有良好的扩展性,而对修改关闭,以避免修改后引入新bug和损坏依赖。
使用装饰器可以满足这个设计原则。扩展时先考虑组合,在考虑继承。
2.1.5. 依赖倒置原则
要依赖抽象而不是依赖具体类。
这个原则很像“面向接口编程”原则,在编程中,我们不能让高层组件直接依赖底层组件,不管高层底层,都应该依赖抽象。这个原则工厂模式总得到了体现。
依赖倒置中的倒置意思见下图:
将高层组件所依赖的组件抽象成抽象类或接口,而底层组件的调用也返回一个抽象类,或者实现这个抽象类。这样就能实现依赖倒置,并且解耦。
看下工厂模式的部分类图
工厂方法和具体产品都依赖产品这个抽象类或接口,也就是解耦合,我们可以不修改代码,就能添加新的子产品类,只要这个类实现了产品类接口。
2.1.6. 单一职责
一个类应该只有一个引起变化的原因。
3. 观察者模式
在Subject中有一个成员变量是Observer类型的数组,里面包含着所有“观察”着Subject的观察者。所以两个接口之间是1对N的聚合关系。
而ConcreteSub和ConcreteObserver是一个动态关联的关系,因为ConcreteObserver的update方法中有Subject类的参数,以此来分辨是哪个ConcreteSubject发来的更新操作。(一个观察者可以观察多个对象)。
3.1. 通知的模式
通知的模式分为push和pull,通常来说push模式更广泛一点,pull会因为多个观察者频发送request而造成网络拥堵。
但pull也有特点,可以让观察者自定义因素询问操作,比如在subject内声明一个changed变量,subject变化不大时,其值为false,只有当observer对象pull时,且change为真,subject才会发出回应,从而使得observer真正执行update操作。
4. 装饰者模式
有时,“继承”这个OOP特性会被滥用,用于构建出一个个和而不同的类,而装饰者模式则可以有效地解决“继承滥用”的问题。装饰者模式使用了“组合”而不是“继承”。
它动态地给一个对象添加职责,以区分和其他类的不同。
通过装饰者模式,还可以解决开放-关闭原则问题,因为装饰者模式并没有修改源代码,而是扩展原来的类。
在Decorator类对象中,会有一个成员变量指向被装饰的对象。这与适配器模式和代理模式相似。
与适配器模式相比较,适配器模式是更改接口,以对接另一个系统或对象。而装饰器模式不改变接口且添加新职责。
与代理模式相比较,装饰器模式更强调“装饰”或“增强”原来的类,而代理模式强调对原来的类做“权限管理”或“访问控制”等,而且做这些“新功能”的主体是代理类。本质来说就是意图不一样。
5. 适配器模式
适配器将一个接口转成另一个接口,使接口不兼容的那些类可以一起工作。
Adaptee本身并不满足Target接口的要求,所以要借助Adapter(适配器)。像Decoratot一样,在Adapter内也维护这一个成员变量指向原来的对象。
调用Adapter的request其实就是调用Adaptee的specilRequest()。
适配器又分为对象适配器和类适配器。
如果要使用类适配器的话,则要求编程语言支持多继承,或者Target是一个狭义接口Adaptee是个类才行。
而对象适配器则只需要实现Target这个广义接口既可以。
6. 代理模式
proxy对象持有对realSubject的引用,所以在必要的时候可以请求发给realSubject。比如,在使用虚拟代理时,对象已经被创建/加载出来了,这个时候就没必要用虚拟代理回应了。使用保护代理时,访问者有足够的权限,就没必要拒绝访问了。
代理模式分为以下三个用途:
- 远程代理,控制访问对象。
- 虚拟代理,控制访问创建开销大的资源,lazy load。
- 保护代理,基于权限控制对资源的访问。
虚拟代理就比如加载网站时,对一些大资源图片等,加载未完成时,浏览器会使用虚拟代理类先给页面返回“加载中,请稍后”等类似图片,一旦大资源被加载了就立即替换。
代理模式和装饰模式都有“修饰”原有对象的作用。两者的区别可以在看下装饰器小节中的总结。总之就是代理模式和装饰模式的意图不一样,代理是用代理类来委托工作,而装饰是为了增强原先的类。
7. 工厂模式
7.1. 简单工厂模式
简单工厂模式与其说是一种设计模式不如说他是编程习惯。
通过传入参数,分支判断,创建哪个产品类。
代码如下:
public class simpleFactory(string productType){
if(productType == "A"){
product p = new ProductA();
return p;
}
if(productType == "B"){
product p = new ProductB();
return p;
}
.....
}
类图如下:
7.2. 工厂模式
三种工厂模式作为构建型设计模式,他们的精髓在于,调用者不需要知道自己创建的子类类型,只需要通过工厂类调用即可。
工厂模式让子类决定要实例化的类是哪一个,通过子类的factoryMethod()返回一个该子类的对象。也就是把创建对象的过程交给了子类。你选择了哪个子类就自然用哪个子类的factoryMethod()方法。
也可以理解把简单工厂中的臃肿的工厂方法拆分给了每个产品,从而有了产品自己的工厂类,同时也让代码中的类变多了,每个产品除了自己之外,还要配套一个自己的工厂类。。
类图下方:
这样的模式优点是,在新增子类的时候,不需要像简单工厂模式那样修改主逻辑(分支判断),而是直接在子类中添加工厂模式。这就满足了“开放-关闭”原则。
简单工厂与工厂模式的区别:
简单工厂在一个地方吧全部事情做完,如果要添加新的种类则需要修改之前的代码,而工厂模式则是打了一个框架,让子类添加自己的工厂类。比起简单工厂,工厂模式更有弹性。
// 产品接口
public abstract Product(){
public Product factoryMethod();
....
}
// 产品实体类
public class ProductA extends Product{
}
public class ProductB extends Product{
}
// 工厂接口
public abstract class Factory{、
public Product createProduct(string type);
}
// 具体A产品工厂类
public class ProductFactoryA extends Factory {
public Product factoryMethod() {
return new ProductA();
}
}
// 具体B产品工厂类
public class ProductFactoryB extends Factory {
public Product factoryMethod() {
return new ProductB();
}
}
// 主函数调用
Factory factory = null;
// 生成A
factory = new ProductFactoryA();
Product p = facroty.factoryMethod();
// 生成B
factory = new ProductFactoryB();
Product p = facroty.factoryMethod();
8. 抽象工厂模式
抽象工厂模式为创建一组对象提供了一种解决方案。与工厂方法模式相比,抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品。
抽象工厂提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
抽象工厂是对工厂类进行归纳升华,工厂类是抽象工厂类的子类。
//省略掉了产品AB的声明,和工厂模式的一样,主要讲工厂类的不一样
//创建产品族的工厂接口
public interface Factory {
public ProductA createProductA();
public ProductB createProductB();
}
// 生成1版本产品的工厂
public class ProductFactory1 implements Factory {
@Override
public ProductA createProductA() {
return new ConcreteProductA1();
}
@Override
public ProductB createProductB() {
return new ConcreteProductB1();
}
}
//生成2版本产品的工厂
public class ProductFactory2 implements Factory {
@Override
public ProductA createProductA() {
return new ConcreteProductA2();
}
@Override
public ProductB createProductB() {
return new ConcreteProductB2();
}
}
// 主函数调用
Factory factory1 = null;
System.out.println("工厂1 :生产1版本产品AB");
factory1 = new ProductFactory1();
factory1.createProductA();
factory1.createProductB();
Factory factory2 = null;
System.out.println("工厂2 :生产2版本产品AB");
factory1 = new ProductFactory2();
factory1.createProductA();
factory1.createProductB();
9. 单例模式
有些对象只需要一个比如线程池和缓存,任务管理器等。
在多线程的时候,很容易创建出多个单例对象,这样就会造成很严重的隐患。
单例对象的创建方法有两种:
懒汉模式和饿汉模式,懒汉模式是通过双层加锁,而饿汉模式是通过静态声明。
懒汉模式的效率比饿汉模式低,但注意的是,在多个类加载器的时候,使用饿汉模式需要指定同一个类加载器,不然也会声明出多个单例对象。
10. 命令模式
将请求封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
用于记录一段期间内命令,然后恢复。如死机后恢复,或者redis的持久化等。
11. 责任链
命令模式最常见的就是java中的try-catch代码块。排除一个个错误类型,然后找到合适的处理分支。