1、适配器模式适配器在生活中无处不在,比如电脑的转接头、读卡器、电源转接头。他们的共同点就是接口标准不一样,需要通过适配器转换后才能使用。就拿读卡器来说,存储卡的接口只能适配相机或者手机的卡槽。而电脑普遍为 USB 接口。那么如何在电脑上使用存储卡呢?我们可以用读卡器,一头卡槽能够插入存储卡,另一头 USB 可以插在电脑上。通过适配器可以解决接口不兼容的问题。还有个例子就是电脑的变压器,电脑一般接收20V电压,但是我国电压是220V,因此就需要变压器做转换,如下图所示,进来是220V,出来被转为20V。变压器其实就是适配器。 1. 实现适配器模式我们通过如下例子,来看看如何实现适配器模式。假如我们的电视机屏幕输出为 4K 画质,但播放器只能输出 2K 的画质,此时就需要一个适配器完成 2K 到 4K 的转换。代码如下: 只能输出 2k 信号的 player : public class Player { public TwoThousandSignal play() { return new TwoThousandSignal(); } }
我们定义一个更为现代的播放器的接口,输出 4K 信号: public interface ModernPlayer { FourThousandSignal play(); } 代码块123
这个接口的实现就是一个适配器( adapter ),通过复用 Player 输出的 2K 信号,转化为 4K 信号,让支持 ModernPlayer 的设备来播放 2K 信号源。 public class ModernPlayerAdapter implements ModernPlayer {
private Player player = new Player();
@Override public FourThousandSignal play() { TwoThousandSignal twoThousandSignal = player.play(); return convertToFourThousandSignal(twoThousandSignal); }
private FourThousandSignal convertToFourThousandSignal(TwoThousandSignal twoThousandSignal) { //4k信号通过算法计算,从2k转换而来。省略转换逻辑, return new FourThousandSignal(); } }
电视机作为调用方,只需要使用 ModernPlayerAdapter 的实例就可以播放 2K 信号,代码如下: public class Television { private ModernPlayer modernPlayer = new ModernPlayerAdapter();
public void display(){ modernPlayer.play(); } } 代码块1234567
看代码是不是很像代理模式?ModernPlayerAdapter 只是调用了Adaptee的方法,获得 2k 信号后转换为 4K 信号。区别在于 Player 并没有实现 ModernPlayer 接口。而代理模式,Proxy 和 RealSubject 是都需要实现同一个接口的。Adapter 的作用是适配不同接口,两个接口的返回值是不同的,Adapter 中需要实现转换逻辑。 类图: 2. 适配器模式优点不需要修改现有的接口和实现,就能复用已有的类; 灵活度高,可以在接口不变的情况下,兼容多种不同的类。
3. 适配器模式适用场景想要使用一个已有的类,但是此类的接口并不符合使用方要; 多个类做的事情相同或者类似,但是各自接口又不同。调用方希望统一接口。
第一个场景可以认为是亡羊补牢。由于种种原因造成系统接口不同,但功能却类似。此时很可能我们不能直接修改已经存在的接口,我们只能通过适配器模式去适配这个接口。 第二个场景其实也很常见。比如我们开发一个比价网站,需要从不同网站抓取同类商品的价格,然后按照自己系统的数据结构保存。不同网站抓取到的数据肯定是不同的,可能字段名不一样,也可能数据结构都不同。但是最终都要保存为同样的数据结构,此时就需要适配器来做转换。 4. 小结当我们面对难以改造,又想复用的对象时,可以考虑采用适配器模式。但切记一定不要滥用适配器。我们应该在最初设计程序的时候就考虑代码的可扩展性。而不是最后通过适配器来解决问题。能修改重构的,尽量去修改。实在不能修改的,比如外部系统的接口,我们就只能通过适配器模式来解决问题。 2、模版方法模式我先问个问题,把大象放冰箱,总共分几步?你一定脱口而出:三步!第一步把冰箱门打开,第二步把大象放进去,第三部把冰箱门关上。这道题是不是太简单了?但当我们考虑细节的时候就没这么简单了。 假如你的冰箱门加了锁,那么第一步开门时就需要开锁。第二步把大象放进去也有细节要考虑。如果你的冰箱是卧式的,那么大象需要躺在里面。如果你的冰箱是立式的,那么大象可以站在里面。像这种主体逻辑一样,细节实现不同的场景,我们可以考虑使用模版方法模式。 在软件建模中,把大象放冰箱是一个算法,我们将其定义为一个模版方法。模版方法用一系列抽象的操作定义一个算法。就像我们例子中的三步,打开冰箱、大象放进去、关上冰箱门。具体如何开门和关门,如何放大象进去,则在子类中实现。冰箱不同,采用的方式自然也不同。 模版方法中定义操作的方法和先后步骤。而真正的操作方法实现则在子类中。 1. 实现模版方法我们看看用代码如何实现把大象放冰箱。为了便于理解,我们尽量少引入类,我们假设冰箱自身有个行为是把大象放进来 冰箱抽象类 Fridge : public abstract class Fridge { public void placeElephant(){ System.out.println("开始装大象"); openDoor(); putElephant(); closeDoor(); System.out.println("结束"); }
public abstract void openDoor();
public abstract void putElephant();
public abstract void closeDoor(); }
Fridge 类中定义了一个模版方法 placeELephan, 里面按照顺序调用 openDoor、putELephant、closeDoor。这三个方法留待子类实现。
下面是两个具体的冰箱实现类代码。 立式冰箱类 VerticalFridge : public class VerticalFridge extends Fridge{ @Override public void openDoor() { System.out.println("打开立式冰箱门"); }
@Override public void putElephant() { System.out.println("将大象站着放进去"); }
@Override public void closeDoor() { System.out.println("关上立式冰箱门"); } }
卧式冰箱 HorizontalFridge : public class HorizontalFridge extends Fridge{ @Override public void openDoor() { System.out.println("打开卧式冰箱门"); }
@Override public void putElephant() { System.out.println("将大象躺着放进去"); }
@Override public void closeDoor() { System.out.println("关上卧式冰箱门"); } }
两个冰箱子类各自实现三个抽象方法。 客户端代码: public class Client { public static void main(String[] args) { Fridge fridge;
fridge = new HorizontalFridge(); fridge.placeElephant();
System.out.println("---------I'm a line-----------");
fridge = new VerticalFridge(); fridge.placeElephant(); } }
客户端代码中,两个不同的子类分表调用了placeElephant 方法,输出如下: 开始装大象 打开卧式冰箱门 将大象躺着放进去 关上卧式冰箱门 结束 ---------I'm a line----------- 开始装大象 打开立式冰箱门 将大象站着放进去 关上立式冰箱门 结束 代码块1234567891011
两个子类开始和结束的步骤一样,中间步骤的顺序也一样。这些逻辑在父类中实现。每个步骤具体的逻辑则在子类中各自实现。 类图: 2. 模版方法优缺点2.1 优点分离了算法中变和不变的部分。不变的部分定义在父类的模版方法中。变的部分通过子类实现。不变的算法部分可以被充分复用。当变的部分有新需求时,可以定义新的子类。从而实现了开闭原则。 2.2 缺点模版意味着死板,我们设定好模版就必须按照模版的一、二、三步来执行。如果我们想调换顺序,或者增加几步就很难做到。除非定义新的模版。或者很小心的改动已有模版,避免影响现有程序逻辑。但这已经违反了开闭原则。 3. 模版方法适用场景如果我们发现一系列的算法,主干一样,只是在局部的实现上有区别。此时我们可以考虑使用模版方法。把算法主干及不变的部分提炼出来,在父类中实现。抽象出变化部分的方法,交由不同的子类自己去实现。 4.小结一般来说我们都是在子类中调用父类的方法。而模版方法恰恰相反,是父类的方法中调用子类的实现。正是因为这样,我们才能把不变的行为抽象到父类中,变化的部分留给子类实现。此外还有一类方法叫做钩子方法,它不是抽象的方法,父类有其缺省实现,一般是空方法。但是子类也可以通过重写去覆盖。 3、装饰者模式每天我们出门前,一定都会选择今天上衣穿什么,裤子穿什么,搭配什么鞋子,大衣穿什么。最后一定是做好选择,打扮好才会出门。这个过程其实就是装饰者模要做的事情 ---- 对一个对象增加额外的功能。 我们再看一个例子。我们都吃过煎饼,除了面饼之外,我们还要加鸡蛋、加葱花、香菜、面酱、辣酱。现在还有新花样,加辣条、加鸡柳。一切都始于一张面饼,摊煎饼的过程就是在不断对这张面饼添加新特性。 我们通过继承也可以为对象增加功能,比如我们有个煎饼的父类,默认已经有面饼、面酱、鸡蛋啊。那么我们可以派生出 全都放的普通煎饼、不辣的普通煎饼、不辣不放香菜的普通煎饼、不辣不放葱的普通煎饼、全都放的辣条煎饼、全都放的鸡柳煎饼…… 这只是很小一部分。通过继承的话,由于情况太多,会造成对象爆炸。 那我们还可以通过组合的方式来扩展类啊,比如煎饼对象中,我们可以设置不同属性,比如是否有葱、是否有香菜、是否有辣条、是否有鸡柳等等。这样看起来也能很好的解决摊煎饼的问题。但如果想要加肠、加油条怎么办?想要加两个鸡蛋怎么办?我们只能修改煎饼对象。这就违反了开闭原则。显然这样也是不够灵活的。 装饰者模式能够很好的解决对象的动态扩展,不管你想穿什么,都可以随便搭配。不过这个煎饼要怎么做,也都能随意的扩展支持,而不需要改已有的代码。接下来我们就来看看如何通过装饰者模式来摊煎饼的。 1. 实现装饰者模式对于摊煎饼来说,我们都是对于一个基础的煎饼对象做装饰,比如我想要一套两个鸡蛋、有辣椒、葱、辣条的煎饼,那么我只需要先声明一个基本的煎饼对象,然后用加鸡蛋装饰类装饰它,然后再用加辣酱装饰类装饰它,再用加葱的装饰类装饰它,最后再用加辣条的装饰类装饰它。最终就得到了我想要的煎饼。不过请注意,不管你怎么装饰,最终得到的还是煎饼,并不是其他东西。 装饰者模式的核心思想是对已有的对象,一层一层的用装饰类去装饰它,扩展它的特性。这样做可以更为动态的为对象增加功能。我们看看代码如何实现: 先定义煎饼接口: public interface Pancake { void cook(); } 代码块123
接口里只定义了一个制作方法。 煎饼接口的实现类: public class BasicPancake implements Pancake { @Override public void cook() { System.out.println("加一勺面"); System.out.println("加一个鸡蛋"); } } 代码块1234567
作为一个最基本的煎饼,总得有面,有鸡蛋吧。其他的材料留给装饰类来实现。 接下来我们定义装饰抽象类: public abstract class PancakeDecorator implements Pancake { protected Pancake pancake;
public void setPancake(Pancake pancake) { this.pancake = pancake; }
public void cook() { if (pancake != null) { pancake.cook(); } } }
可以看到 PancakeDecorator 同样要实现 Pancke 接口。并且持有 Pancke 类型的引用,自己实现的 cook 方法实际调用了持有的 Pancake 对象的 cook 方法。 加辣酱的装饰类代码如下,其他装饰实现类是类似的。 public class AddSpicyDecorator extends PancakeDecorator{ @Override public void cook(){ super.cook(); System.out.println("加辣酱"); } }
cook 方法首先调父类的 cook 方法,然后再加入自己的特性。 客户端代码如下,我们看看如何利用装饰类来生成你想要的煎饼。 public class Client { public static void main(String[] args) { Pancake pancake = new BasicPancake(); PancakeDecorator addEggPancake = new AddEggDecorator(); addEggPancake.setPancake(pancake);
PancakeDecorator addSaucePancake = new AddSauceDecorator(); addSaucePancake.setPancake(addEggPancake);
PancakeDecorator addLaTiaoPancake = new AddLaTiaoDecorator(); addLaTiaoPancake.setPancake(addSaucePancake);
addLaTiaoPancake.cook(); } }
我们声明了三个包装类,对 BasicPancake 层层包装,最后得到一套两个鸡蛋、加辣酱、加辣条的煎饼。运行后输出如下: 加一勺面 加一个鸡蛋 加一个鸡蛋 加面酱 加辣条 代码块12345
如果你研发了新煎饼,要加新的辅料,比如香肠、榨菜之类,那么只需要增加装饰类的实现即可。从而实现了开闭原则。 类图如下: 2. 装饰者模式优缺点2.1 优点动态的为对象添加额外职责:通过组合不同装饰类,非常灵活的为对象增加额外的职责; 避免子类爆炸:当不同的特性组合,构成不同的子类时,必然造成子类爆炸。但通过装饰者灵活组合,可以避免这个问; 分离核心功能和装饰功能:核心业务保留在 Component 的子类中。而装饰特性在 Decorator 的实现类中去实现。面对装饰特性的变化,实现了开闭原则,只需要增加装饰实现类; 很方便的重复添加特性:我想要一套两个鸡蛋,双份辣条的煎饼。是不是只需要多装饰一次就可以了?就是这么简单。
2.2 缺点由于不是通过继承实现添加职责,所以被装饰后的对象并不能通过对象本身就能了解其特性。而需要分析所有对其装饰过的对象; 装饰模式会造成有很多功能类似的小对象。通过组合不同的装饰实现,来达成不同的需求。这样对于不了解系统的人,比较难以学习。过多的装饰类进行装饰,也稍显繁琐。
3. 装饰者模式适用场景使用装饰者模式,有以下几种情况: 需要一个装饰的载体。不能将全部特性都放在装饰类中。换句话讲得有个装饰主体,核心特性在主体对象中实现。例如浏览器窗口,不管是加边框还是滚动条,都是基于窗口的; 有多种特性可以任意搭配,对主体进行扩展。并且你想以动态、透明的方式来实; 不能以生成子类的方式扩展。可能有两种情况,一是对大量子类带来的类爆炸有所顾虑。二是类定义被隐藏,或者不能用于生成子类。
4. 小结装饰者模式的优势在于动态、透明的添加特性。要记住装饰者装饰完的对象还是之前的对象类型。通过分离核心特性和装饰特性,客户端代码可以灵活的搭配使用包装对象,从而得到具有想要行为的对象。不过要注意,有些时候装饰的顺序是要保证的。比如先放鸡蛋,再放芝麻,芝麻就不会掉下去了。最好的做法是保证装饰类的独立。 4、抽象工厂模式抽象工厂模式用来解决产品族的实例化问题。比如说现在有个家居设计软件,通过软件模拟房间,摆放各种虚拟的家具,看效果如何。我们可以放入电视柜、茶几、餐桌、床等等。这一系列的家具就叫做产品族。产品族面临的问题是,当一个产品族切换到另外一个产品族时,如何让代码的修改最小。也就是说如何做到开闭原则。 想把设计好的方案从简约现代切换到欧式风格家具,怎么才能做到修改最小?如果采用简单工厂,那么每种产品都对应一个工厂,工厂负责产出不同风格的产品。设计方案中用到 n 种产品就要修改 n 处代码。这显然不是最佳的方法。此时,我们需要抽象工厂模式来解决这个问题。抽象工厂模式中,每个工厂的实现负责生产自己产品族的产品。示意图如下: 1. 实现抽象工厂为了便于理解和展示,我们假设只有两种家具----椅子和桌子。 首先定义每种家具的接口,只有一个方法用来获取家具说明。 椅子: public interface Chair { void getChairIntroduction(); } 代码块123
桌子: public interface Desk { void getDeskIntroduction(); } 代码块123
以椅子为例,我们分别实现简约现代和欧式两种风格。简约现代风格椅子: public class ModernStyleChair implements Chair { @Override public void getChairIntroduction() { System.out.println("这是一个现代简约风格的椅子"); } }
欧式风格椅子: public class EuropeanStyleChair implements Chair { @Override public void getChairIntroduction() { System.out.println("这是一个欧式风格的椅子"); } }
桌子也有两种实现,代码这里省略。 产品我们已经编写完成。接下来我们来看看工厂的代码。 首先我们定义一个家具工厂接口,可以生产椅子和桌子: public interface FurnitureFactory { Chair createChair(); Desk createDesk(); } 代码块1234
由于我们支持两种不同的风格,所以我们编写两个实现类。 简约风格家具工厂: public class ModernFurnitureFactory implements FurnitureFactory{ @Override public Chair createChair() { return new ModernStyleChair(); }
@Override public Desk createDesk() { return new ModernStyleDesk(); } }
欧式风格家具工厂: public class EuropeanFurnitureFactory implements FurnitureFactory{ @Override public Chair createChair() { return new EuropeanStyleChair(); }
@Override public Desk createDesk() { return new EuropeanStyleDesk(); } }
上面的代码中,每种工厂各自实现如何生产两种不同的家具。 客户端代码如下: public class Client { public static void main(String[] args) { FurnitureFactory furnitureFactory = new EuropeanFurnitureFactory(); Chair chair = furnitureFactory.createChair(); Desk desk = furnitureFactory.createDesk();
chair.getChairIntroduction(); desk.getDeskIntroduction(); } } 代码块12345678910
客户端代码中,我们实例化的是欧式家具工厂,那么所生产的椅子和桌子应该是欧式风格。执行后输出如下: 这是一个欧式风格的椅子 这是一个欧式风格的桌子 代码块12
和我们的预期相符。如果想要更换产品族,从现代简约切换到欧式,我们只需要修改一处代码。 FurnitureFactory furnitureFactory = new ModernFurnitureFactory(); 代码块1
仅通过更换抽象工厂的实现即可实现。修改后执行结果如下: 这是一个现代简约风格的椅子 这是一个现代简约风格的桌子 代码块12
可以看到已经切换到简约风格的产品族。这个过程中并不需要改任何产品使用的代码。 如果增加别的风格产品族,只需要新建新风格的产品族产品,增加新风格产品族的工厂实现即可。类图: 2. 抽象工厂优缺点2.1 优点分离了产品类和客户端类:客户端只依赖抽象的产品接口。此外,如何生产产品被封装在工厂内部; 方便切换产品族:客户端代码只需要初始化一次工厂实现。这意味着在切换产品族的时候,只需要修改一行代码,换一个工厂实现即可; 保证产品的一致性:使用抽象工厂,可以保证你从相同工厂生产的产品都属于同一个产品族。不会出现椅子是现代简约风格,而桌子是欧式风格的情况。
2.2 缺点添加新的产品时,改动较多。例子从两个维度定义产品,一是不同产品,比如桌子、椅子。另外是不同族,例如现代简约和欧式。使用抽象工厂,优化了产品族,也就是第二个维度变化的难度。但是当添加新的产品时改动就会比较多。比如我们要添加一个新的产品是电视柜。那么需要修改抽象工厂,添加生产电视柜的方法。此外,有几种工厂的实现,我们就需要修改几个类,添加具体的生产实现。 3. 抽象工厂适用场景你的系统中,需要使用不同产品族中的某一个产品族来操作。 比如说DB源。如果想切换DB,只需要切换DB源即可,其他代码基本上不需要改动; 你的系统中,需要保证某些产品的一致性。 比如操作系统的外观,当切换到夜间模式时,所有的组件都会换为夜间模式风格。
4. 小结抽象工厂可以做到一组产品的使用和生产相分离。通过抽象工厂模式,我们切换一组产品族的,只需要更换抽象工厂实现即可。由于产品生产被分离出去,所以添加新的产品族完全通过扩展来实现的。很好的实现了开闭原则。如果你要生产的产品很多,而且是一个产品族。并且面临不同产品族切换的情况。那么可以考虑通过抽象工厂来实现。 5、观察者模式观察者模式也称为订阅者模式,实际上我觉得订阅者更容易理解。这种设计模式在生活中很常见。比如订阅期刊杂志、定牛奶等等。我们使用的软件中也很常见。比如说微博,你关注了某位明星,其实你就是他的观察者。每当你关注的明星发了新的动态,你就会接收到通知。观察者模式基于发布订阅的方式。订阅者订阅目标对象,目标对象维护订阅者的集合。一旦目标对象状态变化,需要通知所有订阅者,从而触发订阅者的某个行为。 1. 实现观察者模式实现观察者模式,在目标对象中需要维护所有他的观察者引用。观察者可以观察多个不同目标对象的,所以需要让观察者知道是哪个目标对象发送的通知。下面我们通过一个简单的例子来看看如何实现观察者模式。 这个例子叫老师点名了。上大学时候,经常有同学旷课在宿舍打游戏,并且嘱咐去上课的同学,老师要是点名了给我打电话。还好宿舍离教学楼近,接到通知的同学赶紧跑去教室也能赶上。有的胆子大点的同学,接到通知后也不去上课,而是找个关系好的同学帮忙喊声到。 去上课的同学是通知者(目标对象),他持有所有需要他通知老师点名的同学(观察者)的引用,才能在老师点名的时候通知到每个人。程序中我们一般用容器存储观察者。当通知的时候循环调用所有观察者暴露出的更新方法。 “老师点名了” 目标对象代码如下: public class TeacherRollCallSubject { private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer){ observers.add(observer); }
public void removeObserver(Observer observer){ observers.remove(observer); }
public void notifyObservers (){ observers.forEach(Observer::update); } }
观察者只需要实现一个方法,供通知者做通知的时候调用。我们先定义观察者的接口。 public interface Observer { void update(); } 代码块123
我们定义第一类观察者的实现,他接到通知后,会马上去教室。 public class GotoClassObserver implements Observer { @Override public void update() { System.out.println("老师点名了!"); System.out.println("我要马上赶到教室去!"); } }
第二类观察者,接到通知后,会通知自己的好朋友帮自己答到。 public class AskForHelpObserver implements Observer { @Override public void update() { System.out.println("老师点名了!"); System.out.println("赶紧给XX发信息,让他替我答到!"); } } 代码块1234567
客户端代码中,分别声明两个不同的观察者,然后让这两个观察者都观察老师点名了目标对象。最后出发目标对象的通知方法。客户端代码如: public class Client { public static void main(String[] args) { Observer studentOne = new GotoClassObserver(); Observer studentTwo = new AskForHelpObserver();
TeacherRollCallSubject subject = new TeacherRollCallSubject(); subject.addObserver(studentOne); subject.addObserver(studentTwo);
subject.notifyObservers(); } }
运行后输出如下: 老师点名了! 我要马上赶到教室去! 老师点名了! 赶紧给XX发信息,让他替我答到! 代码块1234
可以看到每个观察者都接到了通知,并且按照自己实现的响应方式作出不同的逻辑处理。第一个同学会赶到教室。第二个同学则是给好朋友发信息,让其替他答到。 2. 观察者模式优缺点2.1 优点1、目标对象状态的变化,不需要观察者真的一直观察。当存在大量观察者时,如果所有的观察者都去轮询状态,那么系统资源的消耗极大。而观察者模式避免了这种情况; 2、观察者模式支持广播,状态变化时,目标对象的所有观察者都会得到通知; 3、符合开闭原则,目标对象依赖的是观察者的接口,可以很方便的对观察者进行扩展,而不需要修改已有观察者。反过来观察者也是依赖的目标接口。 2.2 缺点1、观察者模式中,观察者接口限定了方法签名。有一定的局限性。 3. 观察者模式适用场景1、抽象模型可以分为两个部分,一部分行为取决于另外一部分状态的变化。并且你想让这两部分各自独立。每部分都可以独自使用和复用;2、一个对象的变化,需要通知其他对象,并且有多少对象需要通知并不清楚3、你想让通知与被通知双方松耦合。 4. 小结观察者模式最大的优点就是把目标和观察者解耦,观察者根据目标对象状态的变化作出响应。而目标者可以把自己状态变化广播给所有注册的观察者。实际使用中有推/拉两种模型。 观察者收到消息后再决定如何处理,比如查询与变化相关的自己感兴趣的数据。推模型,目标对象需要知道所有观察者对数据的需求。而拉模型效率会比较差,观察者收到消息后,还需要自己再去获取改变的内容。关于推拉模型总结如下: 6、抽象工厂模式抽象工厂模式用来解决产品族的实例化问题。比如说现在有个家居设计软件,通过软件模拟房间,摆放各种虚拟的家具,看效果如何。我们可以放入电视柜、茶几、餐桌、床等等。这一系列的家具就叫做产品族。产品族面临的问题是,当一个产品族切换到另外一个产品族时,如何让代码的修改最小。也就是说如何做到开闭原则。 想把设计好的方案从简约现代切换到欧式风格家具,怎么才能做到修改最小?如果采用简单工厂,那么每种产品都对应一个工厂,工厂负责产出不同风格的产品。设计方案中用到 n 种产品就要修改 n 处代码。这显然不是最佳的方法。此时,我们需要抽象工厂模式来解决这个问题。抽象工厂模式中,每个工厂的实现负责生产自己产品族的产品。示意图如下: 1. 实现抽象工厂为了便于理解和展示,我们假设只有两种家具----椅子和桌子。 首先定义每种家具的接口,只有一个方法用来获取家具说明。 椅子: public interface Chair { void getChairIntroduction(); } 代码块123
桌子: public interface Desk { void getDeskIntroduction(); } 代码块123
以椅子为例,我们分别实现简约现代和欧式两种风格。简约现代风格椅子: public class ModernStyleChair implements Chair { @Override public void getChairIntroduction() { System.out.println("这是一个现代简约风格的椅子"); } } 代码块123456
欧式风格椅子: public class EuropeanStyleChair implements Chair { @Override public void getChairIntroduction() { System.out.println("这是一个欧式风格的椅子"); } } 代码块123456
桌子也有两种实现,代码这里省略。 产品我们已经编写完成。接下来我们来看看工厂的代码。 首先我们定义一个家具工厂接口,可以生产椅子和桌子: public interface FurnitureFactory { Chair createChair(); Desk createDesk(); } 代码块1234
由于我们支持两种不同的风格,所以我们编写两个实现类。 简约风格家具工厂: public class ModernFurnitureFactory implements FurnitureFactory{ @Override public Chair createChair() { return new ModernStyleChair(); }
@Override public Desk createDesk() { return new ModernStyleDesk(); } } 代码块1234567891011
欧式风格家具工厂: public class EuropeanFurnitureFactory implements FurnitureFactory{ @Override public Chair createChair() { return new EuropeanStyleChair(); }
@Override public Desk createDesk() { return new EuropeanStyleDesk(); } }
上面的代码中,每种工厂各自实现如何生产两种不同的家具。 客户端代码如下: public class Client { public static void main(String[] args) { FurnitureFactory furnitureFactory = new EuropeanFurnitureFactory(); Chair chair = furnitureFactory.createChair(); Desk desk = furnitureFactory.createDesk();
chair.getChairIntroduction(); desk.getDeskIntroduction(); } }
客户端代码中,我们实例化的是欧式家具工厂,那么所生产的椅子和桌子应该是欧式风格。执行后输出如下: 这是一个欧式风格的椅子 这是一个欧式风格的桌子 代码块12
和我们的预期相符。如果想要更换产品族,从现代简约切换到欧式,我们只需要修改一处代码。 FurnitureFactory furnitureFactory = new ModernFurnitureFactory(); 代码块1
仅通过更换抽象工厂的实现即可实现。修改后执行结果如下: 这是一个现代简约风格的椅子 这是一个现代简约风格的桌子 代码块12
可以看到已经切换到简约风格的产品族。这个过程中并不需要改任何产品使用的代码。 如果增加别的风格产品族,只需要新建新风格的产品族产品,增加新风格产品族的工厂实现即可。类图: 2. 抽象工厂优缺点2.1 优点分离了产品类和客户端类:客户端只依赖抽象的产品接口。此外,如何生产产品被封装在工厂内部; 方便切换产品族:客户端代码只需要初始化一次工厂实现。这意味着在切换产品族的时候,只需要修改一行代码,换一个工厂实现即可; 保证产品的一致性:使用抽象工厂,可以保证你从相同工厂生产的产品都属于同一个产品族。不会出现椅子是现代简约风格,而桌子是欧式风格的情况。
2.2 缺点添加新的产品时,改动较多。例子从两个维度定义产品,一是不同产品,比如桌子、椅子。另外是不同族,例如现代简约和欧式。使用抽象工厂,优化了产品族,也就是第二个维度变化的难度。但是当添加新的产品时改动就会比较多。比如我们要添加一个新的产品是电视柜。那么需要修改抽象工厂,添加生产电视柜的方法。此外,有几种工厂的实现,我们就需要修改几个类,添加具体的生产实现。 3. 抽象工厂适用场景你的系统中,需要使用不同产品族中的某一个产品族来操作。 比如说DB源。如果想切换DB,只需要切换DB源即可,其他代码基本上不需要改动; 你的系统中,需要保证某些产品的一致性。 比如操作系统的外观,当切换到夜间模式时,所有的组件都会换为夜间模式风格。
4. 小结抽象工厂可以做到一组产品的使用和生产相分离。通过抽象工厂模式,我们切换一组产品族的,只需要更换抽象工厂实现即可。由于产品生产被分离出去,所以添加新的产品族完全通过扩展来实现的。很好的实现了开闭原则。如果你要生产的产品很多,而且是一个产品族。并且面临不同产品族切换的情况。那么可以考虑通过抽象工厂来实现。
|