本系列文章共分为六篇:
设计模式的分类与区别
创建型模式介绍及实例
结构型模式介绍及实例
行为型模式介绍及实例(上)
行为型模式介绍及实例(下)
上篇文件介绍了创建型模式,本篇将介绍结构型模式。结构型模式的主要关注点是“如何将类或对象按某种布局组成更大的结构”。
一、适配器模式
1.1 适配器模式定义
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者之间的耦合度比后者高,且要求开发者了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
1.2 适配器模式特点
优点:
1>客户端通过适配器可以透明地调用目标接口。
2>复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
3>将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
缺点:
1>过多地使用适配器,会让系统非常零乱,不易整体进行把握。
1.3 适配器模式主要角色
目标接口:当前所期待业务功能实现的接口,它可以是抽象类或接口。
适配者类:它是现存组件库中的组件接口。
适配器类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
1.4 适配器模式实现方式
适配器模式的实现方式较多,主要方式如下:
1.4.1 类适配器模式
该模式是通过类的继承来实现适配器功能,是较为简单的一种适配器模式。以要充电转接口为例,某个电子产品的原始充电器是2口的,但电源是3口的,此时就需要一个3口转2口的适配器。以代码示例如下:
/*目标接口,使用二口数据线充电*/
public interface TwoSocket {
public void charge();
}
/*适配者接口,在三口电源座上充电*/
public class ThreeSocket {
public void powerOn(){
System.out.println("在三口电源座上充电");
}
}
/*适配器类,充当转接口*/
public class SocketAdapter extends ThreeSocket implements TwoSocket{
public void charge() {
System.out.println("使用二口电源线连接电子产品,然后用二口电源线连接三口转两口适配器");
powerOn();
}
}
/*测试类*/
public class AdapterTest {
public static void main(String[] args){
TwoSocket twoSocket = new SocketAdapter();
twoSocket.charge();
}
}
1.4.2 对象适配器模式
对象适配器目标接口和适配者接口与类适配器模式中的相同,不同的是适配器类和测试类。在适配器类中,不再继承适配者接口,而是从调用方传一个适配者接口对象的参数,其差异代码示例如下:
/*适配器类,充当转接口*/
public class SocketAdapter implements TwoSocket{
private ThreeSocket threeSocket;
public SocketAdapter(ThreeSocket threeSocket){
this.threeSocket=threeSocket;
}
public void charge(){
System.out.println("使用二口电源线连接电子产品,然后用二口电源线连接三口转两口适配器");
threeSocket.powerOn();
}
}
/*测试类*/
public class AdapterTest {
public static void main(String[] args){
ThreeSocket threeSocket = new ThreeSocket();
TwoSocket target = new SocketAdapter(threeSocket);
target.charge();
}
}
两种适配器模式的输出结果是一样的,如下:
使用二口电源线连接电子产品,然后用二口电源线连接三口转两口适配器
在三口电源座上充电
1.5 适配器模式应用场景
适配器模式通常适用的场景:
1)以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
2)使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
二、桥接模式
2.1 桥接模式定义
将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
2.2 桥接模式特点
优点:
1>由于抽象与实现分离,所以扩展能力强;
2>其实现细节对客户透明。
缺点:
1>聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,增加了系统的理解与设计难度。
2.3 桥接模式主要角色
抽象化角色:定义抽象类,并包含一个对实现化对象的引用。
扩展抽象化角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
实现化角色:定义实现化角色的接口,供扩展抽象化角色调用。
具体实现化角色:给出实现化角色接口的具体实现。
2.4 桥接模式实现方式
桥接模式的意义在于将系统中不同维度的功能分开来实现,降低耦合性,示例代码如下:
/*实现化角色:颜色*/
public interface Color{
String getColor();
}
/*具体实现化角色:绿色*/
class Green implements Color{
public String getColor(){ return "green"; }
}
/*具体实现化角色:红色*/
public class Red implements Color{
public String getColor(){ return "red"; }
}
/*抽象化角色:包*/
public abstract class Bag{
protected Color color;
public void setColor(Color color){
this.color=color;
}
public abstract String getName();
}
/*扩展抽象化角色:双肩包*/
public class Knapsack extends Bag{
public String getName(){
return color.getColor()+" Knapsack";
}
}
/*扩展抽象化角色:钱包*/
public class Wallet extends Bag{
public String getName(){
return color.getColor()+" Wallet";
}
}
/*测试类*/
public class BridgeTest {
public static void main(String[] args){
Color color = new Red();
Bag bag=new Knapsack();
bag.setColor(color);
System.out.println(bag.getName());
color = new Green();
bag=new Knapsack();
bag.setColor(color);
System.out.println(bag.getName());
color = new Red();
bag=new Wallet();
bag.setColor(color);
System.out.println(bag.getName());
color = new Green();
bag=new Wallet();
bag.setColor(color);
System.out.println(bag.getName());
}
}
结果示例如下:
red Knapsack
green Knapsack
red Wallet
green Wallet
2.5 桥接模式应用场景
适配器模式通常适用的场景:
1)当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
2)当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
3)当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。
三、组合模式
3.1 组合模式定义
又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性。
3.2 组合模式特点
优点:
1>组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
2>更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”。
缺点:
1>设计较复杂,客户端需要花更多时间理清类之间的层次关系;
2>不容易限制容器中的构件;
3>不容易用继承的方法来增加构件的新功能。
3.3 组合模式主要角色
抽象构件角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。
树叶构件角色:是组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中 声明的公共接口。
树枝构件角色:是组合中的分支节点对象,它有子节点。它实现了抽象构件角色中声明的接口,它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。
3.4 组合模式实现方式
组合模式分为透明式的组合模式和安全式的组合模式。
3.4.1 透明方式
在该方式中,由于抽象构件声明了所有子类中的全部方法,所以客户端无须区别树叶对象和树枝对象,对客户端来说是透明的。但其缺点是:树叶构件本来没有 Add()、Remove() 及 GetChild() 方法,却要实现它们(空实现或抛异常)。
以检查班级里有多少人为例,老师为树枝节点,学生为叶子节点,老师可以在自己的班级中添加、删减学生。该例子代码如下:
/*抽象接口*/
public interface ClassPeople {
public void add(ClassPeople sp);
public void remove(ClassPeople sp);
public ClassPeople getChild(int i);
public void check();
}
/*老师*/
public class Teacher implements ClassPeople {
private ArrayList<ClassPeople> children=new ArrayList<ClassPeople>();
private String name;
public Teacher(String name){
this.name=name;
}
public void add(ClassPeople sp) {
children.add(sp);
}
public void remove(ClassPeople sp) {
children.remove(sp);
}
public ClassPeople getChild(int i) {
return children.get(i);
}
public void check() {
System.out.println(name+" Teacher");
for(Object obj:children){
((ClassPeople)obj).check();
}
}
}
/*学生*/
public class Student implements ClassPeople {
private String name;
public Student(String name){
this.name=name;
}
public void add(ClassPeople sp) {}
public void remove(ClassPeople sp) {}
public ClassPeople getChild(int i) { return null; }
public void check() {
System.out.println(name+" Student");
}
}
/*测试类*/
public class ComponentTest {
public static void main(String[] args) {
ClassPeople sp1 = new Teacher("Jack");
ClassPeople sp2 = new Student("Marry");
ClassPeople sp3 = new Student("John");
sp1.add(sp2);
sp1.add(sp3);
sp1.check();
}
}
3.4.2 安全方式
在该方式中,将管理子构件的方法移到树枝构件中,抽象构件和树叶构件没有对子对象的管理方法,这样就避免了上一种方式的安全性问题,但由于叶子和分支有不同的接口,客户端在调用时要知道树叶对象和树枝对象的存在,所以失去了透明性。
仍以上面的老师学生为例子,抽象接口中不再具有add、remove、getChild方法,而是在Teacher类中实现,具体代码如下:
/*抽象接口*/
public interface ClassPeople {
public void check();
}
/*老师*/
public class Teacher implements ClassPeople {
private ArrayList<ClassPeople> children=new ArrayList<ClassPeople>();
private String name;
public Teacher(String name){
this.name=name;
}
public void add(ClassPeople sp) {
children.add(sp);
}
public void remove(ClassPeople sp) {
children.remove(sp);
}
public ClassPeople getChild(int i) {
return children.get(i);
}
public void check() {
System.out.println(name+" Teacher");
for(Object obj:children){
((ClassPeople)obj).check();
}
}
}
/*学生*/
public class Student implements ClassPeople {
private String name;
public Student(String name){
this.name=name;
}
public void check() {
System.out.println(name+" Student");
}
}
/*测试类*/
public class ComponentTest {
public static void main(String[] args) {
Teacher sp1 = new Teacher("Jack");
ClassPeople sp2 = new Student("Marry");
ClassPeople sp3 = new Student("John");
sp1.add(sp2);
sp1.add(sp3);
sp1.check();
}
}
以上两种方式输出结果相同,如下:
Jack Teacher
Marry Student
John Student
3.5 组合模式应用场景
组合模式通常适用的场景:
1)在需要表示一个对象整体与部分的层次结构的场合。
2)要求对用户隐藏组合对象与单个对象的不同,用户可以用统一的接口使用组合结构中的所有对象的场合。
四、装饰器模式
4.1 装饰器模式定义
在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。
4.2 装饰器模式特点
优点:
1>采用装饰模式扩展对象的功能比采用继承方式更加灵活。
2>可以设计出多个不同的具体装饰类,创造出多个不同行为的组合。
缺点:
1>增加了许多子类,如果过度使用会使程序变得很复杂。
4.3 装饰器模式主要角色
通常情况下,扩展一个类的功能会使用继承方式来实现。但使用继承方式的话,耦合度高,并且随着扩展功能的增多,子类会变得很臃肿。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标。
装饰器角色如下:
1>抽象构件角色,定义一个抽象接口以规范准备接收附加责任的对象。
2>具体构件角色,实现抽象构件,通过装饰角色为其添加一些职责。
3>抽象装饰角色,继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
4>具体装饰角色,实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
4.4 装饰器模式实现方式
以学生为例,进行体育运动时穿运动装,上课时转校服,示例代码如下:
/*抽象构件角色*/
public interface Student {
public void act();
}
/*具体构件角色*/
public class Jack implements Student{
public Jack(){}
public void act() {
System.out.println("Jack开始做事");
}
}
/*抽象装饰角色*/
public class Decorator implements Student{
private Student student;
public Decorator(Student student){
this.student=student;
}
public void act(){
student.act();
}
}
/*具体装饰角色1:运动员*/
public class Player extends Decorator{
public Player(Student student) {
super(student);
}
public void act(){
super.act();
addedFunction();
}
public void addedFunction(){
System.out.println("要去打篮球,穿运动服");
}
}
/*具体装饰角色2:上课的人*/
public class Learner extends Decorator{
public Learner(Student student) {
super(student);
}
public void act(){
super.act();
addedFunction();
}
public void addedFunction(){
System.out.println("要去上课,穿校服");
}
}
/*测试类*/
public class DecoratorTest {
public static void main(String[] args) {
Student student = new Jack();
Student player = new Player(student);
player.act();
Student learner = new Learner(student);
learner.act();
}
}
测试结果如下:
Jack开始做事
要去打篮球,穿运动服
Jack开始做事
要去上课,穿校服
4.5 装饰器模式应用场景
装饰器模式通常适用的场景:
1)当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。
2)当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰模式却很好实现。
3)当对象的功能要求可以动态地添加,也可以再动态地撤销时。
五、外观模式
5.1 外观模式定义
一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。
5.2 外观模式特点
优点:
1>降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
2>对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
3>降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。
缺点:
1>不能很好地限制客户使用子系统类。
2>增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
5.3 外观模式主要角色
外观角色:为多个子系统对外提供一个共同的接口。
子系统角色:实现系统的部分功能,客户可以通过外观角色访问它。
客户角色:通过一个外观角色访问各个子系统的功能。
5.4 外观模式实现方式
以上班族为例,一个人早上起床后,去上班前要做一系列的事情:穿衣、洗漱、坐公交等,示例代码如下:
/*子系统1*/
public class Step1 {
public void executeStep(){
System.out.println("穿衣服");
}
}
/*子系统2*/
public class Step2 {
public void executeStep(){
System.out.println("洗漱");
}
}
/*子系统3*/
public class Step3 {
public void executeStep(){
System.out.println("坐公交");
}
}
/*外观*/
public class Employee {
public Employee(){
System.out.println("一个人上班的流程:");
}
Step1 step1 = new Step1();
Step2 step2 = new Step2();
Step3 step3 = new Step3();
public void work(){
step1.executeStep();
step2.executeStep();
step3.executeStep();
}
}
/*测试类,也是客户*/
public class FacadeTest {
public static void main(String[] args) {
Employee employee = new Employee();
employee.work();
}
}
结果如下:
一个人上班的流程:
穿衣服
洗漱
坐公交
5.5 外观模式应用场景
外观模式通常适用的场景:
1)对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
2)当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
3)当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
六、享元模式
6.1 享元模式定义
运用共享技术来有効地支持大量细粒度对象的复用。它通过共享已经存在的又橡来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
6.2 享元模式特点
优点:
1>相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
缺点:
1>为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
2>读取享元模式的外部状态会使得运行时间稍微变长。
6.3 享元模式主要角色
抽象享元角色:是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
具体享元角色:实现抽象享元角色中所规定的接口。
非享元角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
享元工厂角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
6.4 享元模式实现方式
此处以学生写字为例,有时需要用蓝色笔写字,有时需要用黑色笔写字,第一次写某颜色的字时,在文具盒中找出相应的笔,下次不用再从哪个文具盒中找,直接使用即可。示例代码如下:
/*非享元角色*/
public class PenInfo {
private String info;
PenInfo(String info){
this.info=info;
}
public String getInfo(){
return info;
}
public void setInfo(String info){
this.info=info;
}
}
/*抽象享元角色*/
public interface Pen {
public void operation(PenInfo state);
}
/*具体享元角色1*/
public class BluePen implements Pen{
private String key;
BluePen(String key){
this.key=key;
System.out.println(key+"颜色的笔从文具盒中找出!");
}
public void operation(PenInfo outState){
System.out.print("具体享元"+key+"被调用,");
System.out.println("非享元信息是:"+outState.getInfo());
}
}
/*具体享元角色2*/
public class BlackPen implements Pen{
private String key;
BlackPen(String key){
this.key=key;
System.out.println(key+"颜色的笔从文具盒中找出!");
}
public void operation(PenInfo outState){
System.out.print("具体享元"+key+"被调用,");
System.out.println("非享元信息是:"+outState.getInfo());
}
}
/*享元工厂角色*/
public class PenFactory {
private HashMap<String, Pen> pens=new HashMap<String, Pen>();
public Pen getPen(String key){
Pen pen=(Pen)pens.get(key);
if(pen!=null){
System.out.println(key+"颜色的笔已经存在,可直接使用!");
}else{
if("blue".equals(key)){
pen=(Pen) new BluePen(key);
}else{
pen=(Pen) new BlackPen(key);
}
pens.put(key, pen);
}
return pen;
}
}
/*测试类*/
public class PenTest {
public static void main(String[] args){
PenFactory factory=new PenFactory();
Pen pen1=factory.getPen("blue");
Pen pen2=factory.getPen("blue");
Pen pen3=factory.getPen("blue");
Pen pen4=factory.getPen("black");
Pen pen5=factory.getPen("black");
pen1.operation(new PenInfo("第1次使用blue笔"));
pen2.operation(new PenInfo("第2次使用blue笔"));
pen3.operation(new PenInfo("第3次使用blue笔"));
pen4.operation(new PenInfo("第1次使用black笔"));
pen5.operation(new PenInfo("第2次使用black笔"));
}
}
测试结果如下:
blue颜色的笔从文具盒中找出!
blue颜色的笔已经存在,可直接使用!
blue颜色的笔已经存在,可直接使用!
black颜色的笔从文具盒中找出!
black颜色的笔已经存在,可直接使用!
具体享元blue被调用,非享元信息是:第1次使用blue笔
具体享元blue被调用,非享元信息是:第2次使用blue笔
具体享元blue被调用,非享元信息是:第3次使用blue笔
具体享元black被调用,非享元信息是:第1次使用black笔
具体享元black被调用,非享元信息是:第2次使用black笔
6.5 享元模式应用场景
享元模式通常适用的场景:
1)系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
2)大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
3)由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。
七、代理模式
7.1 代理模式定义
由于某些原因需要给某对象提供一个代理以控制对该对象的访问。
7.2 代理模式特点
优点:
1>代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用。
2>代理对象可以扩展目标对象的功能。
3>代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度。
缺点:
1>在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢。
2>增加了系统的复杂度。
7.3 代理模式主要角色
抽象主题类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
真实主题类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
代理类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
7.4 代理模式实现方式
代码模式有两种实现方式:静态代理和动态代理。
7.4.1 静态代理
静态代理可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。比如要买电影票,但又不想去电影院买,这时就可以通过购票APP等第三方"代理"来购买。示例代码如下:
/*抽象主题类,电影*/
public interface Movie {
public void buyMovieTicket();
}
/*真实主题类,第三方购票APP*/
public class TaoPiaoPiao implements Movie {
public void buyMovieTicket() {
System.out.println("淘票票帮您购买电影票");
}
}
/*代理类,通过第三方APP购票*/
public class Proxy implements Movie{
private TaoPiaoPiao movie;
public void buyMovieTicket() {
if(movie == null){
movie=new TaoPiaoPiao();
}
System.out.println("登录淘票票");
movie.buyMovieTicket();
System.out.println("淘票票向你推荐爆米花、可乐");
}
}
/*测试类*/
public class ProxyTest {
public static void main(String[] args) {
Proxy proxy=new Proxy();
proxy.buyMovieTicket();
}
}
示例结果如下:
登录淘票票
淘票票帮您购买电影票
淘票票向你推荐爆米花、可乐
7.4.2 动态代理
动态代理代理依靠反射来实现,代理类在JVM运行时动态生成,而不是编译期就能确定。仍以上面的代码为例,区别在于代理类和测试类。示例代码如下:
/*动态代理类*/
public class Proxy1{
private TaoPiaoPiao movie;
public Proxy1(TaoPiaoPiao movie) {
this.movie = movie;
}
public Object getProxyInstance() {
return Proxy.newProxyInstance(movie.getClass().getClassLoader(), movie.getClass().getInterfaces(), new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if(movie == null){
movie=new TaoPiaoPiao();
}
System.out.println("登录淘票票");
movie.buyMovieTicket();
System.out.println("淘票票向你推荐爆米花、可乐");
return null;
}
});
}
}
/*测试类*/
public class ProxyTest {
public static void main(String[] args) {
TaoPiaoPiao movie = new TaoPiaoPiao();
Movie dynamicProxy = (Movie) new Proxy1(movie).getProxyInstance();
dynamicProxy.buyMovieTicket();
}
}
7.5 代理模式应用场景
代理模式通常适用的场景:
1)虚拟代理,这种方式通常用于要创建的目标对象开销很大时。
2)安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
3)智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
参考资料:结构型模式概述