结构型设计模式包括:适配器模式、桥接模式、组合模式、装饰者模式、外观模式、享元模式、代理模式。
1、适配器模式
当需要使用一个现存的类,但它提供的接口与我们系统的接口不兼容,而我们还不能修改它时,我们可以将目标类用一个新类包装一下,使新类的接口保留原接口模式,但实际上使用的是目标类的接口。
比如我们系统中原来的日志接口为MyFactory,现在要使用新的日志库NewFactory,其写日志的接口与我们原来的接口不同,但我们无法修改新日志库的代码,所以可以包装一下新的日志库类来使用:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//原来的日志接口 public interface MyFactory { void log(String tag, String message); } //新的日志接口 public interface NewLogger { void debug(int priority, String message, Object ... obj); } public class NewLoggerImp implements NewLogger { @Override public void debug(int priority, String message) { } } //日志适配器类 public class LogAdapter implements MyFactory { private NewLogger nLogger; public LogAdapter() { this.nLogger = new NewLoggerImp(); } @Override public void log(String tag, String message) { Objects.requireNonNull(nLogger); nLogger.debug(1, message); } }
2、桥接模式
现在有一个形状类Shape,其子类有圆形Circle和方形
Square,如果我们想要扩展子类使其包含颜色的话,可以增加红色圆形孙子类、蓝色圆形孙子类和红色方形孙子类、蓝色方形孙子类,如下图所示。当然,对于C++来说,因为可以多重继承,所以可以将颜色单拎出来为一个类系,红色圆形就是从红色类+圆形类派生而来。
如果再增加一个三角形的话就要增加三个类:三角形、红色三角形、蓝色三角形,如果颜色再增加的话要增加的类就更多。问题的根本原因是我们试图在两个独立的维度——形状与颜色——上使用继承来扩展类。更好的方式就是把颜色单独拿出来作为一个父类来派生出红色、蓝色等颜色后,在形状类增加一个颜色类的成员变量来表示当前形状的颜色。现在, 形状类可以将所有与颜色相关的工作委派给连入的颜色对象。 这样的引用就成为了 形状
和 颜色
之间的桥梁。这种设计模式就属于桥接模式,桥是用来将两个独立的结构联系起来,而这两个被联系起来的结构可以独立的变化而不影响对方。对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
再比如,现在要开发一个咖啡制作系统,咖啡容量有大杯、中杯、小杯,咖啡口味有原味、加糖,直接设计类的话就会出现大杯原味和大杯加糖、中杯原味和中杯加糖、小杯原味和小杯加糖六个类,如果以后再增加加奶口味的话这样类的个数就直线上升。我们可以使用上面说过的桥接模式,咖啡的容量和口味是两个维度的类,咖啡原始类(咖啡容量的虚基类)中包含一个咖啡口味的成员,这样即使加上一个加奶口味的话也就有大杯咖啡、中杯咖啡、小杯咖啡、加糖口味、加奶口味五个类(咖啡类中口味成员为空即为原味咖啡,所以不用添加原味口味)。
3、组合模式
如果业务的核心模型能用树状结构表示, 那么使用组合模式才有价值。比如,现在有个业务对象,可以是产品或盒子,盒子中可以再包含盒子或产品,以此类推。现在设计获得对象价格的方法,对于产品来说可以直接返回其价格,对于盒子的话遍历盒子中的所有项目, 询问每个项目的价格, 然后返回该盒子的总价格, 如果其中某个项目是小一号的盒子, 那么当前盒子也会遍历其中的所有项目, 以此类推。组合模式的优点在于调用方无需了解构成树状结构的对象的具体类, 也无需了解对象是简单的产品还是复杂的盒子。 你只需调用通用接口以相同的方式对其进行处理即可, 当你调用该方法后, 对象会将请求沿着树结构传递下去。
组合模式一般包含个体与组合的通用接口类、个体类、组合类,当然个体类和组合类也可以为一个类:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//个体和组合通用的接口类 abstract class Node { private String name; public Node(String name) { this.name = name; } public String getName() { return name; } public abstract void add(Node organization); public abstract Node getChild(String orgName); public abstract int getAttrCount(); } //个体类 class Unit extends Node { public Unit(String name) { super(name); } @Override public int getAttrCount() { return 5; } @Override public void add(Node organization) { throw new UnsupportedOperationException(this.getName()+"已经是最基本部门,无法增加下属部门"); } @Override public Node getChild(String orgName) { if(getName().equals(orgName)){ return this; } return null; } } //组合类 class OrganizationComposite extends Node { //很关键,这体现了组合的思想 private List<Node> nods = new ArrayList<>(); public OrganizationComposite(String name) { super(name); } @Override public void add(Node organization) { nods.add(organization); } @Override public Node getChild(String orgName) { for (Node org : nods) { Node targetOrg = org.getChild(orgName); if (targetOrg != null) { return targetOrg; } } return null; } @Override public int getAttrCount() { int count = 0; for (Node organization : nods) { count += organization.getAttrCount(); } return count; } }
4、装饰者模式
对于一个咖啡制造系统,咖啡的口味有原味、加奶、加糖,加奶也加糖,那么对于即加奶又加糖的口味,我们想到的是从加奶和加糖二者继承出来的一个类,但Java中不支持多重继承怎么办?而且有的客户要求先加奶后加糖,有的客户要求先加糖后加奶,这就相当于是要动态的给咖啡增加口味。当你需要更改一个对象的行为时, 第一个跳入脑海的想法就是扩展它所属的类, 但是, 你不能忽视继承可能引发的两个严重问题:一个就是继承是静态的,你无法在运行时更改已有对象的行为,另一个问题就是很多语言比如Java的子类只能有一个父类。
我们可以使用聚合(对象A包含对象B,B可以独立于A的存在)或组合(对象A由对象B组成,A负责管理B的声明周期)来避免继承带来的问题,二者的工作方式几乎一模一样:一个对象包含指向另一个对象的引用, 并将部分工作委派给引用对象。聚合 或组合是许多设计模式背后的关键原则 ,包括装饰模式在内。
装饰者模式适合:1、需要在运行时动态的给一个对象增加额外职责的时候。2、需要给一个现有的类扩展功能,增加职责,但是又不想通过继承的方式来实现的时候(应该优先使用组合而非继承),或者通过继承的方式不现实的时候(不支持多重继承)。装饰器包含与目标对象相同的一系列方法,因为装饰器一般是继承目标对象所属抽象类, 它会将所有接收到的请求委派给目标对象, 但装饰器可以在将请求委派给目标前后对其进行处理,即进行装饰。如下实现了装饰者模式,可以看到,你无需创建新子类即可扩展对象的行为:将对象放到装饰者类中进行装饰。你也可以在运行时动态的添加对象的功能:加奶、加糖。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//咖啡接口类 interface ICoffee { void makeCoffee(); } //原味咖啡 class OriginalCoffee implements ICoffee { @Override public void makeCoffee() { System.out.print("原味咖啡 "); } } //咖啡装饰者抽象类 abstract class CoffeeDecorator implements ICoffee { protected ICoffee coffee; public CoffeeDecorator(ICoffee coffee){ this.coffee=coffee; } } //咖啡装饰者——加奶咖啡 class MilkDecorator extends CoffeeDecorator { private int milkML = 10; //10毫升的奶 public MilkDecorator(ICoffee coffee) { super(coffee); } @Override public void makeCoffee() { coffee.makeCoffee(); addMilk(); } private void addMilk(){ System.out.print("加奶 "); } } //咖啡装饰者——加糖咖啡 class SugarDecorator extends CoffeeDecorator { private int sugarG = 10; //10克的糖 public SugarDecorator(ICoffee coffee) { super(coffee); } @Override public void makeCoffee() { coffee.makeCoffee(); addSugar(); } private void addSugar(){ System.out.print("加糖"); } } public class Main { public static void main(String[] args)throws CloneNotSupportedException{ //制作各类咖啡的过程 ICoffee coffee = new OriginalCoffee(); //原味咖啡 coffee.makeCoffee(); coffee = new MilkDecorator(coffee); //加奶咖啡 coffee.makeCoffee(); coffee = new SugarDecorator(coffee); //加奶后加糖的咖啡 coffee.makeCoffee(); } }
乍一看,装饰模式使用了桥接模式,但其实不然,桥接模式一般是在A类中包含另一个类B类的引用成员,然后通过这个成员来调用B类(一般为B类的实现类的对象)的方法,A和B是两个不同维度、不同功能的类。上面的装饰者类是从咖啡类继承的(因为装饰者需要提供与被装饰者相同的方法),所以它属于咖啡,能够提供咖啡的方法,然后它包含的是咖啡类的引用成员(即被装饰者),这样就可以对传入的原咖啡对象进行加糖、加奶等装饰操作,这种方式避免了从咖啡类继承来进行操作。
5、外观模式
如果你的程序需要与包含几十种功能的复杂库整合, 但你只需使用这个库中非常少的功能的时候,你可以提供一个只包含需要用到的功能接口的类。另一种情况,当前有很多个模块,或者说子系统,你可以给用户提供一个统一操作的类,而不是让用户为了实现某个功能需要分别与这些模块一个一个的进行调用或交互,当然这种方式不会限制客户端直接使用子系统。
上面这种方式就叫外观模式:如果你需要一个指向复杂子系统的直接接口, 且该接口的功能有限, 则可以使用外观模式。一般外观仅提供简单的接口, 它会将绝大部分工作委派给其他类。 通常情况下, 外观管理着其所使用的对象的完整生命周期。
6、享元模式
当你开发一个射击游戏的时候,有可能需要成千上万个子弹在游戏中展示, 子弹对象中包含名称、大小、贴图、位置和速度,这样就会消耗很多内存。我们可以把对象中的名称、大小、和贴图这些不会改变的属性(不易改变的属性又称内在状态)与位置、速度这些经常改变的属性(经常改变的属性又称外在状态)从一个类分开来,子弹对象的名称、大小、和贴图是固定的,只需要一份对应的数据就可以了,我们把保存子弹内在状态的类称为“享元”,代码设计如下所示:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import java.util.ArrayList; import java.util.List; //享元类 class BulletShare{ int size; Image img; BulletShare(int size, Image img){} } //产品类 class Bullet{ int x, y; int speed; BulletShare type; //享元类的引用 Bullet(int x, int y, int speed, BulletShare type){} } public class Main { public static void main(String[] args){ BulletShare bs = new BulletShare(100, Image()); //不管创建多少个子弹对象,只需要一个BulletShare对象 List l = new ArrayList<Bullet>(); for(int i = 0; i < 1000; ++i){ l.add(new Bullet(i, 5, 100, bs)); } } }
子弹也可能有好几种类型,比如有普通子弹、榴弹、导弹三种类型,所以可以增加一个工厂类来获得不同类型的子弹:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; //享元类 class BulletType{ String name; int size; Image img; BulletType(String name, int size, Image img){} } //产品类 class Bullet{ int x, y; int speed; BulletType type; //享元类的引用 Bullet(int x, int y, int speed, BulletType type){} } class BulletFactor{ private static final Map<String, BulletType> _map = new HashMap<>(); public static BulletType getBullet(String name) { BulletType bt = _map.get(name); if (bt == null) { int size = 0; if(name == "子弹") size = 10; else if(name == "榴弹") size = 100; else if(name == "导弹") size = 1000; bt = new BulletType(name, size, Image); _map.put(name, bt); } return bt; } } public class Main { public static void main(String[] args){ BulletType bs = BulletFactor.getBullet("子弹"); //子弹 //创建1000个子弹对象,只需要一个子弹对象 List l1 = new ArrayList<Bullet>(); for(int i = 0; i < 1000; ++i){ l1.add(new Bullet(i, 5, 100, bs)); } BulletType bt = BulletFactor.getBullet("导弹"); //导弹 //创建10个导弹对象,只需要一个导弹对象 List l2 = new ArrayList<Bullet>(); for(int i = 0; i < 10; ++i){ l2.add(new Bullet(i, 5, 100, bt)); } } }
7、代理模式
代理对象提供原服务的全部或部分接口功能,相当于是原服务的替代品,它控制着对于原对象的访问。使用代理的好处:
①、可以扩展原对象的接口功能,比如在原接口调用前增加日志写入,条件判断等,当然这种情况一般是原服务是第三方,我们无法修改其代码,或者我们不想破坏原服务接口明确的功能性这种情况。
②、对访问进行控制以保护目标对象,比如只希望特定客户端使用服务,因为服务会操作系统中非常重要的部分。
③、缓存请求的结果,即缓存客户请求结果并对缓存生命周期进行管理。
④、接口业务发生扩展的时候,方便集中管理。这个可以参考《JAVA之反射》这篇文章中的动态代理部分。