本文梳理自:https://lrh1993.gitbooks.io/android_interview_guide/content/
为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。
创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。
Factory:工厂角色
Product:抽象产品角色
ConcreteProduct:具体产品角色
优点:
通过使用工厂类,外界不再需要关心如何创造各种具体的产品,只要提供一个产品的名称作为参数传给工厂,就可以直接得到一个想要的产品对象,并且可以按照接口规范来调用产品对象的所有功能(方法)。
构造容易,逻辑简单。
缺点:
简单工厂模式中的if else判断非常多,完全是Hard Code,如果有一个新产品要加进来,就要同时添加一个新产品类,并且必须修改工厂类,再加入一个 else if 分支才可以, 这样就违背了 “开放-关闭原则”中的对修改关闭的准则了。当系统中的具体产品类不断增多时候,就要不断的修改工厂类,对系统的维护和扩展不利。
一个工厂类中集合了所有的类的实例创建逻辑,违反了高内聚的责任分配原则,将全部的创建逻辑都集中到了一个工厂类当中,所有的业务逻辑都在这个工厂类中实现。什么时候它不能工作了,整个系统都会受到影响。因此一般只在很简单的情况下应用,比如当工厂类负责创建的对象比较少时。
简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。
这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
每个工厂只负责生产一种产品(一一对应)
Factory factory = new ConcreteFractory();
Product product = factory.CreateProduct();
Product:抽象产品
ConcreteProduct:具体产品
Factory:抽象工厂
ConcreteFactory:具体工厂
优点
工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”,这点比简单工厂模式更优秀。
缺点
在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
一个类不知道它所需要的对象的类:
一个类通过其子类来指定创建哪个对象:
将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
一个工厂可以提供多个不同种类产品对象
不再是一一对应
多个位于不同产品等级结构中属于不同类型的具体产品时使用
重要定义:
产品等级结构: 产品等级结构即产品的继承结构
产品族: 产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品
示例:
public static void Main(string[] args)
{
//使用平板工厂
//采购商要一台iPad和一台Tab
Factory factory = new Factory_Pad();
Apple apple = factory.createAppleProduct(); //工厂父类中的创建苹果产品的方法
apple.AppleStyle();
Sumsung sumsung = factory.createSumsungProduct(); //工厂父类中的创建三星产品的方法
sumsung.BangziStyle();
//使用手机工厂
//采购商又要一台iPhone和一台Note2
factory = new Factory_Phone();
apple = factory.createAppleProduct();
apple.AppleStyle();
sumsung = factory.createSumsungProduct();
sumsung.BangziStyle();
Console.ReadKey();
}
优点
抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。
由于这种隔离,更换一个具体工厂就变得相对容易。
所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。
增加新的具体工厂和产品族很方便,
缺点
在添加新的产品对象(不同于现有的产品等级结构)时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。
开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)。
一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
系统中有多于一个的产品族,而每次只使用其中某一产品族。与工厂方法模式的区别
属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。
系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
public class Singleton {
private volatile static Singleton instance; //声明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
可能存在的问题:
instance = new Singleton()
这句,这并非是一个原子操作(不可再分操作),事实上在 JVM 中这句话大概做了下面 3 件事情:
I、给 instance 分配内存
II、调用 Singleton 的构造函数来初始化成员变量
III、将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。
但是在 JVM 的即时编译器中存在指令重排序的优化。
也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。
如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
使用volatile
关键字禁止指令重排序优化
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
//实现懒加载,只有在第一次调用此方法时才会去创建对象
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
原理
组成
优缺点
优点
缺点
每次对值操作完后返回对象本身
public Builder setTitle(CharSequence title) {
P.mTitle = title;
return this;
}
结构型模式(Structural Pattern)描述如何将类或者对象结合在一起形成更大的结构
结构型模式可以分为两种
(1)类结构型模式
关心类的组合,由多个类可以组合成一个更大的系统
在类结构型模式中一般只存在继承关系和实现关系。
(2)对象结构型模式关心类与对象的组合
通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。
根据“合成复用原则”,在系统中尽量使用关联关系来替代继 承关系,因此大部分结构型模式都是对象结构型模式。
把一个类的接口变换成客户端所期待的另一种接口(把被适配的类的API转换成为目标类的API),从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法
这个过程对客户类是透明的,客户类并不直接访问适配者类
Android应用:ListView的getView等方法
1⃣类的适配器模式
目的:扩展被适配的类(Adaptee)
思想:通过继承/实现Target,获取目标方法,实现Adaptee转换接口
目标(Target)角色:这就是所期待得到的接口。
源(Adaptee)角色:现在需要适配的接口。
适配器(Adaper)角色:适配器类是本模式的核心。
public class Adapter extends Adaptee implements Target {
/**
* 由于源类Adaptee没有方法sampleOperation2()
* 因此适配器补充上这个方法
*/
@Override
public void sampleOperation2() {
//写相关的代码
}
}
2⃣对象的适配器模式
目的:使Target能够调用Adaptee的API
思想:适配器持有一个Adaptee对象实例,使用委派关系连接到Adaptee类
public class Adapter {
private Adaptee adaptee;
public Adapter(Adaptee adaptee){
this.adaptee = adaptee;
}
/**
* 源类Adaptee有方法sampleOperation1
* 因此适配器类直接委派即可
*/
public void sampleOperation1(){
//在
this.adaptee.sampleOperation1();
}
/**
* 源类Adaptee没有方法sampleOperation2
* 因此由适配器类需要补充此方法
*/
public void sampleOperation2(){
//写相关的代码
}
}
类适配器 | 对象适配器 |
---|---|
类适配器使用对象继承的方式,是静态的定义方式 | 对象适配器使用对象组合的方式,是动态组合的方式。 |
对于类适配器,由于适配器直接继承了Adaptee,使得适配器不能和Adaptee的子类一起工作,因为继承是静态的关系,当适配器继承了Adaptee后,就不可能再去处理 Adaptee的子类了 | 对于对象适配器,一个适配器可以把多种不同的源适配到同一个目标。换言之,同一个适配器可以把源类和它的子类都适配到目标接口。因为对象适配器采用的是对象组合的关系,只要对象类型正确,是不是子类都无所谓。 |
对于类适配器,适配器可以重定义Adaptee的部分行为,相当于子类覆盖父类的部分实现方法。 | 对于对象适配器,要重定义Adaptee的行为比较困难,这种情况下,需要定义Adaptee的子类来实现重定义,然后让适配器组合子类。虽然重定义Adaptee的行为比较困难,但是想要增加一些新的行为则方便的很,而且新增加的行为可同时适用于所有的源。 |
对于类适配器,仅仅引入了一个对象,并不需要额外的引用来间接得到Adaptee。 | 对于对象适配器,需要额外的引用来间接得到Adaptee。 |
优点:
更好的复用性
更好的扩展性
缺点:
外观类通过持有子系统对象实例,统一调度客户端对子系统的需求
在外观模式中,通常只需要一个外观类,并且此外观类只有一个实例,换言之它是一个单例类
能够用于有选择性地暴露方法
外观类中只暴露客户端可能用到的子系统的方法
子系统之间用于内部调度的方法不会暴露
外观(Facade)角色 :
子系统(SubSystem)角色 :
松散耦合
简单易用
更好的划分访问层次(有选择的暴露方法)
抽象构件(Component)角色:
具体构件(ConcreteComponent)角色:
装饰(Decorator)角色:
持有一个抽象构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
构造函数初始化
具体装饰(ConcreteDecorator)角色:
负责给构件对象“贴上”附加的责任。
可以获取到传入的对象示例,可以直接用或加以装饰
//客户端调用
public class Client {
public static void main(String[] args) {
TheGreatestSage sage = new Monkey();
// 第一种写法 单层装饰
TheGreatestSage bird = new Bird(sage);
TheGreatestSage fish = new Fish(bird);
// 第二种写法 双层装饰
//TheGreatestSage fish = new Fish(new Bird(sage));
fish.move();
}
}
定义:装饰者模式对客户端的透明性要求程序不要声明一个ConcreteComponent类型的变量,而应当声明一个Component类型的变量
TheGreatestSage sage = new Monkey();
TheGreatestSage bird = new Bird(sage);
Monkey sage = new Monkey();
Bird bird = new Bird(sage);
半透明的装饰者模式
TheGreatestSage sage = new Monkey();
Bird bird = new Bird(sage);
bird.fly();
透明和半透明两种区别:在于装饰角色的接口与抽象构件角色的接口是否完全一致。
透明的装饰者模式也就是理想的装饰者模式,要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。
如果装饰角色的接口与抽象构件角色接口不一致,也就是说装饰角色的接口比抽象构件角色的接口宽的话,装饰角色实际上已经成了一个适配器角色,这种装饰者模式也是可以接受的,称为“半透明”的装饰模式
优点:
装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
装饰模式允许系统动态决定“贴上”一个需要的“装饰”,或者除掉一个不需要的“装饰”。
继承关系则不同,继承关系是静态的,它在系统运行前就决定了。
通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。
缺点:
优点
缺点
行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。
行为型模式分为两种:
(1)类行为型模式:
类的行为型模式使用继承关系在几个类之间分配行为
类行为型模式主要通过多态等方式来分配父类与子类的职责。
(2)对象行为型模式:
对象的行为型模式则使用对象的聚合关联关系来分配行为
对象行为型模式主要是通过对象关联等方式来分配两个或多个类的职责。根据“合成复用原则”,系统中要尽量使用关联关系来取代继承关系,因此大部分行为型设计模式都属于对象行为型设计模式。
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
Observable类
以及一个Observer接口
Observer接口
只定义了一个方法,即update()
方法,当被观察者对象的状态发生变化时,被观察者对象的notifyObservers()
方法就会调用这一方法
Observable类
中使用Vector
(动态扩展大小的数组)来管理Observer
抽象主题(Subject)角色:
抽象主题角色把所有对观察者对象的引用保存在一个聚集(比如ArrayList对象)里,每个主题都可以有任何数量的观察者。
抽象主题提供一个接口,可以增加和删除观察者对象,抽象主题角色又叫做抽象被观察者(Observable)角色。
具体主题(ConcreteSubject)角色:
将有关状态存入具体观察者对象;
在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色又叫做具体被观察者(Concrete Observable)角色。
抽象观察者(Observer)角色:
具体观察者(ConcreteObserver)角色:
推模型(Observer的update方法传递的是主题对象中的部分参数)
拉模型(Observer的update方法传递的是主题对象自身)(自主性更强,推荐)
可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系
调用者持有命令的引用,命令持有接收者的引用
客户端(Client)角色:
命令(Command)角色:
具体命令(ConcreteCommand)角色:
请求者(Invoker)角色:
接收者(Receiver)角色:
优点
更松散的耦合
更动态的控制
很自然的复合命令
更好的扩展性
缺点
把游走的任务放在迭代器上,而不是聚合上。
简化了聚合的接口和实现,也让责任各得其所
抽象迭代器(Iterator)角色:
具体迭代器(ConcreteIterator)角色:
聚集(Aggregate)角色:
public abstract class Aggregate {
/**
* 工厂方法,创建相应迭代子对象的接口
*/
public abstract Iterator createIterator();
}
具体聚集(ConcreteAggregate)角色:
客户端(Client)角色:
优点
①简化了遍历方式,对于对象集合的遍历,还是比较麻烦的,对于数组或者有序列表,我们尚可以通过游标来取得,但用户需要在对集合了解很清楚的前提下,自行遍历对象,但是对于hash表来说,用户遍历起来就比较麻烦了。而引入了迭代器方法后,用户用起来就简单的多了。
②可以提供多种遍历方式,比如说对有序列表,我们可以根据需要提供正序遍历,倒序遍历两种迭代器,用户用起来只需要得到我们实现好的迭代器,就可以方便的对集合进行遍历了。+
③封装性良好,用户只需要得到迭代器就可以遍历,而对于遍历算法则不用去关心。
缺点
对算法封装,把调用算法的责任(行为)和算法本身(行为实现)分割开来,委派给不同的对象管理
环境(Context)角色:
抽象策略(Strategy)角色:
具体策略(ConcreteStrategy)角色:
public static void main(String[] args) {
//选择并创建需要使用的策略对象
MemberStrategy strategy = new AdvancedMemberStrategy();
//创建环境
Price price = new Price(strategy);
//计算价格
double quote = price.quote(300);
System.out.println("图书的最终价格为:" + quote);
}
策略模式对多态的使用
策略模式的重心
算法的平等性
策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正因为这个平等性,才能实现算法之间可以相互替换。所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。
所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现。
运行时策略的唯一性
公有的行为
策略模式的优点
(1)策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到父类里面,从而避免代码重复。
(2)使用策略模式可以避免使用多重条件(if-else)语句。多重条件语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重条件语句里面,比使用继承的办法还要原始和落后。
策略模式的缺点
(1)客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道算法或行为的情况。
(2)由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。
子类可以置换掉父类的可变部分,但是子类却不可以改变模板方法所代表的顶级逻辑
实现:准备一个抽象类,在模版方法中实现对基本方法(或hookMethod())
基于继承的代码复用
代表具体逻辑步骤的方法称做基本方法(primitive method),一般是抽象方法,让子类实现
abstractMethod():子类必须实现,模版中只做抽象声明
hookMethod():子类选择实现,模版中有一个基本实现(或空实现)
将这些基本方法汇总起来的方法叫做模板方法(template method)
抽象模板(Abstract Template)角色
定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。
定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
具体模板(Concrete Template)角色
实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
优点
在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。
模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。
可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。
在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。
缺点