设计模式
1. 原则
1. 针对接口编程,而不是针对实现编程。即不将变量声明为某个特定的具体类的实例对象,而是遵从抽象类所定义的接口,如Abstract Factory,Builder,Factory Method,Prototype,Singleton
2. 优先使用对象组合,而不是类继承 –>好处:1. 存在较少的依赖关系;2. 有助于保持每个类被封装,并被集中在单个任务上。
3. 三个目的:a)缩短开发时间;b)降低维护成本;c)在应用程序之间和内部轻松集成
2. 创建型模式
2.1 Simple Factory 简单工厂
又叫静态工厂方法(Static FactoryMethod)。
- 优点: 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量
- 缺点:
- 由于工厂类集中了所有实例的创建逻辑,这就直接导致一旦这个工厂出了问题,所有的客户端都会受到牵连。
- 工厂类需要判断何时创建何种接口的产品,违背了单一职责原则。
- 违背了“开放-关闭原则”,因为当我们新增加一个产品的时候必须修改工厂类,相应的工厂类就需要重新编译一遍,工厂和产品种类耦合度高。但这一点可以利用反射在一定程度上解决,但这仅限于产品类的构造及初始化相同的场景。对于各产品实例化或者初始化不同的场景,很难利用反射满足“开放-关闭”原则。
public class CarFactory { private static Map<String, Class> allCars; static { Reflections reflections = new Reflections("com.jasongj.product"); Set<Class<?>> annotatedClasses = reflections.getTypesAnnotatedWith(Vehicle.class); allCars = new ConcurrentHashMap<String, Class>(); for (Class<?> classObject : annotatedClasses) { Vehicle vehicle = (Vehicle) classObject.getAnnotation(Vehicle.class); allCars.put(vehicle.type(), classObject); } allCars = Collections.unmodifiableMap(allCars); } public static Car newCar() { Car car = null; String type = null; try { XMLConfiguration config = new XMLConfiguration("car.xml"); type = config.getString("factory3.type"); LOG.info("car type is {}", type); } catch (ConfigurationException ex) { LOG.error("Parsing xml configuration file failed", ex); } if (allCars.containsKey(type)) { LOG.info("created car type is {}", type); try { car = (Car) allCars.get(type).newInstance(); } catch (InstantiationException | IllegalAccessException ex) { LOG.error("Instantiate car failed", ex); } } else { LOG.error("specified car type {} does not exist", type); } return car; } }
2.2 Factory Method 工厂方法
- 优点:解耦,隔离了产品类。高层模块只需要知道产品的抽象类,其他的实现类都不需要关心。通过FactoryMethod(图中的CreateProduct方法)返回Product具体的类
- 因为每个具体工厂类只负责创建产品,没有简单工厂中的逻辑判断,因此符合单一职责原则。
- 新增一种产品时,只需要增加相应的具体产品类和相应的工厂子类即可,相比于简单工厂模式需要修改判断逻辑而言,工厂方法模式更符合开-闭原则。
- 缺点:
- 添加新产品时,除了增加新产品类外,还要提供与之对应的具体工厂类,系统类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
- 虽然保证了工厂方法内的对修改关闭,但对于使用工厂方法的类,如果要换用另外一种产品,仍然需要修改实例化的具体工厂。
- 一个具体工厂只能创建一种具体产品
- Abstract Factroy通常用工厂方法来实现
抽象工厂代码:
public interface IFactory { ICar CreateCar(); }
抽象产品代码:
public interface ICar { void GetCar(); }
具体工厂代码:
public class SportFactory : IFactory { public ICar CreateCar() { return new SportCar(); } } public class JeepFactory : IFactory { public ICar CreateCar() { return new JeepCar(); } } public class HatchbackFactory : IFactory { public ICar CreateCar() { return new HatchbackCar(); } }
具体产品代码:
public class SportCar : ICar { public void GetCar() { Console.WriteLine("场务把跑车交给范·迪塞尔"); } } public class JeepCar : ICar { public void GetCar() { Console.WriteLine("场务把越野车交给范·迪塞尔"); } } public class HatchbackCar : ICar { public void GetCar() { Console.WriteLine("场务把两箱车交给范·迪塞尔"); } }
客户端代码:
static void Main(string[] args) { // 利用.NET提供的反射可以根据类名来创建它的实例,非常方便 IFactory factory = Assembly.LoadFrom(Path.Combine(codeBase, dllName)).CreateInstance(factoryType) as IFactory; ICar car = factory.CreateCar(); car.GetCar(); }
2.3 Abstract Factory 抽象工厂
- 工厂实现为Singleton(单件),因为一个应用中一般每个产品系列只需一个ConcreteFactory的实例
- 抽象工厂模式与工厂方法模式最大的区别在于抽象工厂中每个工厂可以创建多个种类的产品。
- 优点:增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
抽象工厂代码:
public abstract class AbstractEquipment { public abstract AbstractCar CreateCar(); public abstract AbstractBackpack CreateBackpack(); }
抽象产品代码:
public abstract class AbstractCar { public abstract string Type { get; } public abstract string Color { get; } } public abstract class AbstractBackpack { public abstract string Type { get; } public abstract string Color { get; } }
具体工厂代码:
public class SportEquipment : AbstractEquipment { public override AbstractCar CreateCar() { return new SportCar(); } public override AbstractBackpack CreateBackpack() { return new SportBackpack(); } } public class JeepEquipment : AbstractEquipment { public override AbstractCar CreateCar() { return new JeeptCar(); } public override AbstractBackpack CreateBackpack() { return new JeepBackpack(); } }
具体产品代码:
public class SportCar : AbstractCar { private string type = "Sport"; private string color = "Red"; public override string Type { get { return type; } } public override string Color { get { return color; } } } public class SportBackpack : AbstractBackpack { private string type = "Sport"; private string color = "Red"; public override string Type { get { return type; } } public override string Color { get { return color; } } } //越野类的具体产品这里就不列出来了
客户端代码:
static void Main(string[] args) { AbstractEquipment factory = (AbstractEquipment)Assembly.Load(assemblyName).CreateInstance(fullTypeName); AbstractCar car = factory.CreateCar(); AbstractBackpack backpack = factory.CreateBackpack(); car.Type(); backpack.Color(); }
2.4 Builder 生成器
- 产品没有抽象类,因为由具体生成器生成的产品,它们的表示相差很大(比如Director是一个阅读器,ConcreteBuilder是各种文档格式转换器),定义公共父类没有意义
- Builder与Abstract Factory区别:Builder着重于一步步构造一个复杂对象,最后一步返回产品;Abstract Factory着重于多个系列的产品对象(可以简单,可以复杂),产品是立即返回的。
- Builder在Java中可以用来实现参数默认值。Java实现参数默认值还可以用函数的重载,但在参数很多的情况下,重载函数的数量会非常多,这时就得用到Builder。
2.5 Prototype 原型
- 优点:不像Factory Method,Prototype只需要一个工厂类,不用实现具体子类,在调用工厂类构造函数的时候,把不同类型通过参数初始化Creator对象。Creator对象通过Clone,return对应类的产品。
class MazePrototypeFactory: public MazeFactory{ public: MazePrototypeFactory(Wall*, Room*); virtual Room* MakeRoom(int) const; virtual Wall* MakeWall() const; private: Room* _prototypeRoom; Wall* _prototypeWall; } MazePrototypeFactory::MazePrototypeFactory(Wall* w, Room* r){ _prototypeRoom = r; _prototypeWall = w; } MazePrototypeFactory simpleMazeFactory(new Wall, new Room); MazePrototypeFactory bombedMazeFactory(new BombedWall, new RoomWithABomb);
- 缺点:Prototype的子类都必须实现Clone操作,难点如下:
- 类已经存在时难以新增Clone操作
- 克隆是深拷贝,尤其是对循环引用的对象,eg:
var a={};a.b=a;
2.6 Singleton 单件
- 保证一个类仅有一个实例,并提供一个访问它的全局访问点
3. 结构型模式
3.1 Adapter 适配器
别名:包装器Wrapper
- Adapter适用于你想使用一个已经存在的类,而它的接口不符合你的需求。封装该类,提供接口,还可以在封装的过程中提供一些新功能。
3.2 Bridge 桥接
- 抽象类+继承的问题:高耦合,难以对抽象部分和实现部分独立地进行修改。如果有N个维度,每个维度有M种变化,则需要M^N个具体类,类多且非常多的重复功能。多一种维度,比如下面的例子中多一种手自一体档(AMT),则需要增加3个类,BMWAMT,BenZAMT,LandRoverAMT。
- 桥接把抽象化与实现化解耦,使得二者可以独立变化。桥接是一个拆出来的接口,它与一方绑定,即实现桥接口,另一方在抽象类中调用桥接口。当把每个维度拆分开来,只需要M*N个类,并且由于每个维度独立变化,基本不会出现重复代码。
- Bridge与Adapter区别:Bridge目的是接口部分和实现部分分离;Adapter用来帮助无关的类协同工作
抽象车代码:
public abstract class AbstractCar { protected Transmission gear; public abstract void run(); public void setTransmission(Transmission gear) { this.gear = gear; } }
抽象变速器代码:
public abstract class Transmission{ public abstract void gear(); }
具体车代码:
public class BMWCar extends AbstractCar{ @Override public void run() { gear.gear(); LOG.info("BMW is running"); }; } public class BenZCar extends AbstractCar{ @Override public void run() { gear.gear(); LOG.info("BenZCar is running"); }; } public class LandRoverCar extends AbstractCar{ @Override public void run() { gear.gear(); LOG.info("LandRoverCar is running"); }; }
具体变速器代码:
public class Manual extends Transmission { @Override public void gear() { LOG.info("Manual transmission"); } } public class Auto extends Transmission { @Override public void gear() { LOG.info("Auto transmission"); } }
客户端代码:
public class BridgeClient { public static void main(String[] args) { Transmission auto = new Auto(); AbstractCar bmw = new BMWCar(); bmw.setTransmission(auto); bmw.run(); Transmission manual = new Manual(); AbstractCar benz = new BenZCar(); benz.setTransmission(manual); benz.run(); } }
3.3 Composite 组合
- Composite使得用户对单个对象(Leaf)和组合对象(composite)的使用具有一致性。Component是对外接口。
- Composite对象的继承关系是树结构
3.4 Decorator 装饰
- 动态地给一个对象添加一些额外的职责(比如,为TextView添加一个粗黑边框,可以使用BorderDecorator添加)
- Decorator经常和Composite模式一起使用,它们通常有一个公共的父类,因此装饰必须支持具有Add、Remove和GetChild操作的Component接口。
3.5 Facade 外观
- 为子系统中的一组接口提供一个一致的界面(比如一个编程环境,包含了Scanner、Parser、ProgramNode、BytecodeStream、ProgramNodeBUilder等子系统,客户不需要了解这些复杂的子系统,通过Facade提供一个高层的接口给客户,降低客户与子系统的耦合度)
- Facade:知道哪些子系统负责处理请求;将客户的请求代理给适当的子系统对象。通常仅需要一个Facade对象,所以Facade对象通常是Singleton模式。
3.6 Flyweight 享元
- 运用共享技术有效地支持大量细粒度的对象。例如,文档编辑器,为每一个字母创建一个flyweight,那么每个字符共享一个flyweight对象,比如apprent在无力上的存储:
- 参与者
- Flyweight:接口
- ConcreteFlyweight:实现Flyweight接口,ConcreteFlyweight对象必须是可共享的,例如Character
- UnsharedConcreteFlyweight:并非所有的Flyweight子类都需要被共享。UnsharedConcreteFlyweight对象(例如Row,Column)通常将ConcreteFlyweight对象作为子节点
- FlyweightFactory:创建并管理flyweight对象
- Client:维持一个对flyweight的引用
- Flyweight模式经常和Composite模式结合起来,用共享叶节点的有向无环(叶节点不能存储指向父节点的指针)图实现一个逻辑上的层次结构
3.7 Proxy 代理
提供代理以提供对一个对象的访问。应用:
- 远程代理(Remote Proxy):为一个对象在不同的地址空间提供代理
- 虚代理(Virtual Proxy):当需要创建开销很大的对象,比如打开有很多大图片的文件,可以使用ImageProxy先替代这些图片,不显示。
- 保护代理(Protection Proxy):控制对原始对象的访问,用于对象有不同的访问权限的时候。
- 智能引用(Smart Reference)
4. 行为模式
涉及到算法和对象间职责的分配
4.1 Chain of responsibility 职责链
- 使多个对象都有机会处理请求,从而避免请求的发送着和接受这之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,知道有一个对象处理它为止
- 职责链常与Composite一起使用,一个Composite的父构件作为它的后继。
4.2 Command 命令
将请求封装为一个对象,从而可以用不同的请求对客户参数化、对请求排队等
4.3 Interpreter 解释器
为语言定义一个文法
- 参与者
- AbstractExpression:如RegularExpression
- TerminalExpression:一个句子中的每个终结符(语言中用到的基本元素,比如名词、动词、形容词)需要该类的一个实例
- NonterminalExpression:非终结符(语法中用到的元素,比如主语、短语、词组、句子)。对文法中的每一条规则都需要一个NonterminalExpression类
- Context(上下文):包含解释其之外的一些全局信息
- Client:构建表示该文法定义的语言中一个特定的橘子的抽象语法树。该语法树由NonterminalExpression和TerminalExpresssion的实例装配而成。调用解释操作。
- 优点:易于改变和拓展文法
- 缺点;复杂的文法难以实现,因为解释器模式为文法中的每一条规则至少定义了一个类,复杂的文法会导致大量的类,难以管理和维护。
- 抽象语法树是一个Composite模式的实例。Flyweight模式说明了如何在抽象语法树中共享终结符
4.4 Iterator 迭代器
别名: Cursor
多态迭代(Polymorphic iteration)
- 优点减少迭代器和列表的耦合度。比如定义一个抽象的迭代器类Iterator,为不同的列表实现定义具体的Iterator子类。
- 缺点:要求用一个Factory Method动态的分配迭代器对象,且堆中的迭代器对象客户必须负责删除。Proxy提供了一个补救方法:可食用一个栈分配的Proxy作为世纪迭代器的中间代理,该代理在析构器中删除该迭代器。
4.5 Mediator 中介者
- 用一个中介对象来封装一系列的对象交互。终结者降低了个对象之间的耦合,而且可以独立地改变它们之间的交互。
- 每一个同事对象在需与其他的同事通信的时候,与它的中介者通信。Client要使用同事对象,也与中介者通信。
- Facade与Mediator的区别;Facade为子系统提供了一个接口,协议是单向的。而Mediator的协议是多向的
4.6 Memento 备忘录
别名:token
- 捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。
- 不能提供一个接口让其他对象得到这些状态,这样会破坏对象的封装性。只有Originator(原发器)可以访问备忘录。Caretake(负责人)可以对备忘录的内容进行操作,比如检查、撤销等。
4.7 Observer 观察者
别名:依赖(Dependents),发布-订阅(Publish-Subscribe)
定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于他的对象都得到通知并被自动更新
4.8 State 状态
- 允许一个对象在其内部状态改变时改变它的行为。比如,TCPConnection的状态机。
- 状态对象通常是Singleton。Flyweight模式解释了怎么共享状态。
4.9 Strategy 策略
- 封装一系列算法,在不同的时候使用不同的算法/Strategy
- 继承有助于吸取出这些算法中的公共功能。
- 优点:分离算法和数据。你可以直接生称一个Context类的子类,从而给它以不同的行为。但这会将行为硬行编制到Context中,奖算法的实现与Context的实现混合起来,难以理解、维护和拓展。
- 缺点:客户必须了解不同的Strategy才能进行选择。另外,ConcreteStrategy很有可能不会全用到Stragey接口的信息,导致Context会创建和初始化一些永远不会用到的参数。
4.10 Template method 模版方法
- 一次性实现一个算法的不变的部分,将可变的行为留给子类来实现。
- 模版方法应该指明哪些操作是钩子操作hook operations(可以被重定义),以及哪些是抽象操作(必须被重定义)。钩子函数提醒子类去调用被继承的行为。例
void ParentClass::operation(){ HookOperation(); }
void DerivedClasss::HookOperation(){ ... }
4.11 Visitor 访问者
- Visitor可以在不改变各元素的类的前提下定义作用与这些元素的新操作。例如,要为源程序(表示为一个抽象语法树)进行各种操作(类型检查、代码优化、流程分析等等),那么可以将一个类中的相关操作包装在一个独立的对象(Visitor)中,并在遍历抽象语法树时将此对象传递给当前访问的元素。当一个元素接受该访问者,该元素想访问者发送一个包含自身类信息的请求,然后访问者将为该元素执行该操作。
- 优点:
- 易于增加新的操作
- 访问者集中相关的操作而分离无关的操作。相关的行为不是分布在定义该对象结构的各个类上,而是集中在一个访问者中;无关行为则被放在它们各自的访问者子类中。分离了类和算法。
- 可以访问不具有相同父类的对象,而迭代器能够访问的元素都有一个共同的父类
- 缺点:增加新的ConcreteElement类很难,每增加一个新类,要在Visitor中添加一个新的抽象操作,并在ConcreteVisitor类中实现相应的操作。
举例:Java的Collection,每次访问它的元素时,要用instance of判断它的类型,再进行相应的操作,这样会有许多if...else...语句。
Iterator iterator = collection.iterator() while (iterator.hasNext()) { Object o = iterator.next(); if (o instanceof Collection) messyPrintCollection((Collection)o); else if (o instanceof String) System.out.println("''''"+o.toString()+"''''"); else if (o instanceof Float) System.out.println(o.toString()+"f"); else System.out.println(o.toString()); }
针对上例,定义接口叫Element,用来定义一个Accept操作,也就是说让Collection每个元素具备可访问性。
public class StringElement implements Element { private String value; public StringElement(String string) { value = string; } public String getValue(){ return value; } //定义accept的具体内容 public void accept(Visitor visitor) { visitor.visitString(this); }
这样上例的访问可以改成:
// 接口visitor访问者如下: public interface Visitor { public void visitString(StringElement stringE); public void visitFloat(FloatElement floatE); public void visitCollection(Collection collection); } // 访问者的实现: public class ConcreteVisitor implements Visitor { //在本方法中,我们实现了对Collection的元素的成功访问 public void visitCollection(Collection collection) { Iterator iterator = collection.iterator() while (iterator.hasNext()) { Object o = iterator.next(); if (o instanceof Visitable) ((Visitable)o).accept(this); } } public void visitString(StringElement stringE) { System.out.println("''''"+stringE.getValue()+"''''"); } public void visitFloat(FloatElement floatE){ System.out.println(floatE.getValue().toString()+"f"); } }