23种设计模式--结构型模式(适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式)

来源:互联网 发布:css布局 知乎 编辑:程序博客网 时间:2024/06/09 14:23

  总体来说设计模式分为三大类:
  创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
  结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
  行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
  其实还有两类:并发型模式和线程池模式。
  这章开始,讲下7种结构型模式:适配器模式(Adapter)、桥接模式(Bridge)、组合模式(Composite)、装饰模式(Decorator)、外观模式(Facade)、享元模式(Flyweight)、代理模式(Proxy)。其中对象的适配器模式是各种模式的起源,我们看下面的图:

1、适配器模式

  适配器模式(Adapter)将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。
  

1.1、类的适配器模式

  也是适配器模式的通常代码:
  1.目标角色

public interface Target {    // 目标角色有自己的方法    void request();}

  2.目标角色的实现类

public class ConcreteTarget implements Target {    public void request() {         // 实现    }}

  3.源角色

public class Adaptee {    // 原有的业务逻辑    public void doSomething(){    }}

  4.适配器角色:最核心

public class Adapter extends Adaptee implements Target{    public void request() {        super.doSomething();    }}

  场景类

    @Test    public void test() {        // 原有的业务逻辑        Target target = new ConcreteTarget();        target.request();        // 现在增加了适配器角色的业务逻辑        Target target2 = new Adapter();        target2.request();    }

1.2、对象的适配器模式

  基本思路和类的适配器模式相同,只是将Adapter类作修改,这次不继承Adaptee类,而是持有Adaptee类的实例,以达到解决兼容性的问题。如果要适配多个对象,就要用这种

public class Adapter implements Target{    private Adaptee adaptee1;    private Adaptee adaptee2;    public Adapter(Adaptee adaptee12 private Adaptee adaptee2){  // 持有实例        super();          this.adaptee1 = adaptee1;          this.adaptee2 = adaptee2;     }      public void request() {        super.doSomething();    }}

1.3、接口的适配器模式

  第三种适配器模式是接口的适配器模式,接口的适配器是这样的:有时我们写的一个接口中有多个抽象方法,当我们写该接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要某一些,此处为了解决这个问题,我们引入了接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行。看一下类图:

public abstract class Adapter implements Target{    public void method1(){}    public void method2(){}}

总结:
  的适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。
  对象的适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper类的方法中,调用实例的方法就行。
  接口的适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。

优点:
  1).适配器模式可以让两个没有任何关系的类在一起运行
  2).增加了类的透明性
  3).提高了类的复用度,灵活性非常好

注意事项:适配器模式最好在详细设计阶段不要考虑它,它不是为了解决还处在开发阶段中接口时,而是解决正在服役的项目问题,该模式使用的主要场景是扩展应用中。
  再次提醒,项目一定要遵守依赖倒置原则和里氏替换原则。


2、桥接模式(Bridge)

  桥接模式将抽象化与实现化解耦,使得二者可以独立变化。桥梁模式的重点是在“解耦”上,如何让它们两者解耦是我们要了解的重点。
  桥接模式是一个非常简单的模式,它只是使用了类间的聚合关系、继承、覆写等常用功能 。
  像我们常用的JDBC桥DriverManager一样,JDBC进行连接数据库的时候,在各个数据库之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是JDBC提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了。我们来看看关系图:

记住一句话:抽象角色引用实现角色,或者说抽象角色的部分实现是由实现角色完成的。
1.Implementor: 实现化角色
  它是接口或抽象类,定义角色必需的行为和属性

public interface Implementor {    // 基本方法    void doSomething();    void doAnything();}

2.具体实现化角色

public class ConcreteImplementor implements Implementor {    public void doSomething() {        // 业务逻辑处理    }    public void doAnything() {    }}

3.抽象化角色
  定义出该角色的行为,同时保存一个对实现化角色的引用,该角色一般是抽象类。
  为什么要增加一个构造函数?是为了提醒子类,你必须做这项工作,指定实现者,特别是已经明确了实现者,则尽量清晰明确地定论出来。如果没有写,实现化角色有很多子接口,下面就会是一堆子实现。

public abstract class Abstraction {    private Implementor implementor;    // 约束子类必须实现该构造函数    public Abstraction(Implementor implementor) {        this.implementor = implementor;    }    // 自身的行为和属性    public void request(){        this.implementor.doSomething();    }    public Implementor getImplementor() {        return implementor;    }}

4.具体抽象化角色
  它引用实现化角色对抽象化角色进行修正

public class RefinedAbstraction extends  Abstraction {    public RefinedAbstraction(Implementor implementor) {        super(implementor);    }    // 修正父类的行为    @Override    public void request() {        super.request();        super.getImplementor().doAnything();    }}

5.场景类

    @Test    public void test(){        // 定义一个实现化角色        Implementor implementor = new ConcreteImplementor();        // 定义一个抽象化角色        Abstraction abstraction = new RefinedAbstraction(implementor);        abstraction.request();    }

优点
  1).抽象和实现的分离
  这也是桥梁模式的主要特点,它完全是了为解决继承的缺点而提出的设计模式,在该模式下,实现可以不受抽象的约束,不用再绑定在一个固定的抽象层次上。
  2).优秀的扩充能力
  3).实现细节对客户透明,客户不用关心细节的实现,因为已经封装了。

使用场景:
  1).不希望或不适用使用继承的场景
  2).接口或抽象类不稳定
  明知道接口不稳定还想通过实现或继承来实现业务需求,那是得不偿失的。
  3).重要性要求较高的场景

注意:
  继承是有缺点,就是强侵入,父类有一个方法,子类也会有这个方法,这样会导致不能随便修改父类。对于比较明确不发生变化的,可以通过继承来实现;若不能确定是否会发生变化的,那就认为是发生变化,则通过桥梁模式来解决。


3、组合模式(Composite)

  组合模式有时又叫部分-整体模式在处理类似树形结构的问题时比较方便,看看关系图:

public class TreeNode {      private String name;      private TreeNode parent;      private Vector<TreeNode> children = new Vector<TreeNode>();      public TreeNode(String name){          this.name = name;      }      public String getName() {          return name;      }      public void setName(String name) {          this.name = name;      }      public TreeNode getParent() {          return parent;      }      public void setParent(TreeNode parent) {          this.parent = parent;      }      //添加孩子节点      public void add(TreeNode node){          children.add(node);      }      //删除孩子节点      public void remove(TreeNode node){          children.remove(node);      }      //取得孩子节点      public Enumeration<TreeNode> getChildren(){          return children.elements();      }  }  public class Tree {      TreeNode root = null;      public Tree(String name) {          root = new TreeNode(name);      }      public static void main(String[] args) {          Tree tree = new Tree("A");          TreeNode nodeB = new TreeNode("B");          TreeNode nodeC = new TreeNode("C");          nodeB.add(nodeC);          tree.root.add(nodeB);          System.out.println("build the tree finished!");      }  }  

使用场景:将多个对象组合在一起进行操作,常用于表示树形结构中,例如二叉树,数等。

4、装饰模式(Decorator)

  装饰模式:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更加灵活,是对继承的有力补充。要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例,关系图如下:
  

在装饰模式中,必然有一个最基本、最核心、最原始的接口或抽象类充当Component抽象构件

抽象构件
  是一个接口或是抽象类,就是定义我们最核心的对象,也就是最原始的对象。

public abstract class Component {    public abstract void operate();}

具体构件
  最原始、最基本、最核心的接口或抽象类的实现,你要装饰的就是它

public class ConcreteComponent extends Component {    // 具体实现    @Override    public void operate() {    }}

抽象装饰者
  一般是一个抽象类,主要是实现接口或抽象方法,它里面不一定有抽象的方法,在它的属性里必然有一个private变量指向Conponent抽象构件。

public abstract class Decorator extends Component {    private Component component = null;    // 通过构造函数传递被修饰者    public Decorator(Component component) {        this.component = component;    }    // 委托给被修改者执行    @Override    public void operate() {        this.component.operate();    }}

具体的装饰类
  把最基本的东西装饰成其他的东西。

public class ConcreteDecorator extends Decorator {    public ConcreteDecorator(Component component) {        super(component);    }    // 定义自己的装饰方法    public void method1() {        System.out.println("method1装饰");    }    // 重写父类的方法,这里的顺序是固定的,可以通过重载实现多种顺序    public void operate() {        this.method1();        super.operate();    }}

优点:
  1).装饰类和被装饰类可以独立发展,而不会相互耦合。也就是说Component类无须知道Decorator类,Decorator类是从外部来扩展Component类的功能,而它也不用知道具体的构件。
  2).装饰模式是继承关系的一个替代方案。看装饰类Decorator,不管装饰多少层,返回的对象还是Component,实现的还是is-a的关系。
  3).装饰模式可以动态地扩展一个实现类的功能。
缺点:
  多层的装饰模式是比较复杂的,就像剥洋葱一样,剥到最后才发现是最里层的装饰出现了问题,工作量会很多,也会产生过多相似的对象,不易排错。因此尽量减少装饰类的数量,以降低系统的复杂度。

应用场景:
  1).需要扩展一个类的功能,或给类增加附加功能。
  2).动态的为一个对象增加功能,而且还能动态撤销。(继承不能做到这一点,继承的功能是静态的,不能动态增删。)
  3).需要为一批的兄弟类进行改装或加装功能,首选装饰模式。


5、外观/门面模式(Facade)

   门面模式也叫外观模式,是一种比较常用的封装模式:要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。
   门面模式注意“统一的对象”,也就是提供一个访问子系统的接口,除了这个接口不允许有任何访问子系统的行为发生。其通用类图如下:
       
   外观模式是为了解决类与类之间的依赖关系的,像spring一样,可以将类和类之间的关系配置到配置文件中,而外观模式就是将他们的关系放在一个Facade类中,降低了类类之间的耦合度,该模式中没有涉及到接口。

subsystem子系统角色
   可以同时有一个或多个子系统。每一个子系统都不是一个单独的类,而是一个类的集合。子系统并不知道门面的存在,对于子系统而言,门面仅仅是另外一个客户端而已。
   下面用电脑的CPU,Memory来代表子系统

    public class CPU {          public void startup(){              System.out.println("cpu startup!");          }          public void shutdown(){              System.out.println("cpu shutdown!");          }      }  
    public class Memory {          public void startup(){              System.out.println("memory startup!");          }          public void shutdown(){              System.out.println("memory shutdown!");          }      }  

Facade门面角色
   客户端可以调用这个角色的方法,此角色知晓子系统的所有功能和责任。一般情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去,也就主该角色没有实际的业务逻辑,只是一个委托类。

    public class Facade {    // 可以当成Computer类去理解        private CPU cpu;          private Memory memory;          private Disk disk;          public Facade(){              cpu = new CPU();              memory = new Memory();              disk = new Disk();          }          // 提供给自问访问的方法        public void startup(){              cpu.startup();              memory.startup();          }          public void shutdown(){              cpu.shutdown();              memory.shutdown();          }      }  

   如果我们没有Facade类(实际可以理解为Computer类),那么,CPU、Memoryk他们之间将会相互持有实例,产生关系,这样会造成严重的依赖,修改一个类,可能会带来其他类的修改,这不是我们想要看到的,有了Facade类,他们之间的关系被放在了Facade类里,这样就起到了解耦的作用,这,就是外观模式!

优点:
  1).减少系统的相互依赖,外界访问不用深入到子系统内部。
  2).提高了灵活性
  3).提高安全性
缺点:
  最大的缺点就是不符合开闭原则,一旦系统出现错误,只能修改门面的代码,风险太大,所以设计的时候慎之又慎。
使用场景:
  1).为了复杂的模式或子系统提供一个供外界访问的接口
  2).子系统相对独立,外界对子系统的访问只要黑箱操作即可


6、享元模式(Flyweight)

  享元模式是池技术的重要实现方式,其定义如下:使用共享对象可有效地支持大量的细粒度的对象。主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。
  享元模式的定义为我们提出两个要求:细粒度的对象和共享对象。分配太多的的对象到应用程序中将有损程序的性能,同时还容易造成内存溢出,那怎么避免?就是享元模式提到的共享技术

内存溢出对Java应用来说实在是太平常了,有以下两种可能
1.内存泄漏
  无意识的代码缺陷,导致内存泄漏,JVM不能获得连续的内存空间
2.对象太多
  代码写的太烂,产生的对象太多,内存被耗尽。

  要求细粒度对象,那么不可避免地使得对象数量多且性质相近,那我们就将这些对象的信息分为两个部分:内部状态(insrinsic)与外部状态(extrinsic)
内部状态
  内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随着环境改变而改变,它们可以作为一个对象的动态附加信息,不必直接储存在具体某个对象中,属于可以共享的部分。
外部状态
  外部状态是对象得以依赖的一个标记,是随着环境改变而改变的、不可共享的状态,它是一批对象的统一标识,是唯一的一个索引值。
  
  FlyWeightFactory负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象,如果有,就返回已经存在的对象,如果没有,则创建一个新对象,FlyWeight是超类。一提到共享池,我们很容易联想到Java里面的JDBC连接池,想想每个连接的特点,我们不难总结出:适用于作共享的一些个对象,他们有一些共有的属性,就拿数据库连接池来说,url、driverClassName、username、password及dbname,这些属性对于每个连接来说都是一样的,所以就适合用享元模式来处理,建一个工厂类,将上述类似属性作为内部数据,其它的作为外部数据,在方法调用时,当做参数传进来,这样就节省了空间,减少了实例的数量。

  享元模式的目的在于运用共享技术,使得一些粒度的对象可以共享,我们的设计确实也应该这样,多使用细粒度的对象,便于重用或重构。下面看通用代码。
抽象享元角色
  简单地说就是一个产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现。
  在该类里,一般需要把外部状态和内部状态(当然了可以没有内部状态,只有行为也是可以的)定义出来,避免子类的随意扩展。
  在对外部状态加上final关键字,防止意外产生,避免获得一个外部状态后无意修改了一下,池就混乱了。

注意:在程序开发中,确认只需要一次赋值的属性则设置为final类型,避免无意修改导致逻辑混乱,特别是Session级的常量或变量

public abstract class Flyweight {    // 内部状态    private String intrinsic;    // 外部状态    protected final String extrinsic;  // final方法必须初始化    // 要求享元角色必须接受外部状态    public Flyweight(String extrinsic) {        this.extrinsic = extrinsic;    }    // 定义业务操作    public abstract void operate();    /*内部状态的getter/setter*/    public String getIntrinsic() {        return intrinsic;    }    public void setIntrinsic(String intrinsic) {        this.intrinsic = intrinsic;    }}

具体享元角色
  具体的一个产品类,实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时修改了外部状态,这是绝对不允许的。

public class ConcreteFlyweight extends Flyweight {    // 接受外部状态    public ConcreteFlyweight(String extrinsic) {        super(extrinsic);    }    // 根据外部状态进行逻辑处理    @Override    public void operate() {    }}

不可共享的享元角色
  不存在外部状态或者安全要求(如线程安全)不能使用共享技术的对象,该对象一般不会出现在享元工厂 中。

享元工厂
  职责非常简单,就是构造一个池容器,同时提供从池中获得对象的方法

public class FlyweightFactory {    // 定义一个池容器    private static HashMap<String ,Flyweight> pool = new HashMap<String, Flyweight>();    // 享元工厂    public static Flyweight getFlyweight(String extrinsic){        Flyweight flyweight;  // 需要返回的对象        if (pool.containsKey(extrinsic)) {            flyweight = pool.get(extrinsic);        } else {            flyweight = new ConcreteFlyweight(extrinsic);            pool.put(extrinsic, flyweight);        }        return flyweight;    }}

看个数据库连接池例子:

        /*公有属性*/          private String url = "jdbc:mysql://localhost:3306/test";          private String username = "root";          private String password = "root";          private String driverClassName = "com.mysql.jdbc.Driver"; 

  通过连接池的管理,实现了数据库连接的共享,不需要每一次都重新创建连接,节省了数据库重新创建的开销,提升了系统的性能!
  
优点:
  享元模式是一个非常简单的模式,它可以大大减少应用程序创建的对象,降低程序内存的占用,增强程序的性能 。
缺点:
  提高了系统的复杂性,需要分享出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则系统的逻辑混乱。
使用场景:
  1).系统中存在大量相似对象
  2).细粒度的对象都具备较近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
  3).需要缓冲池的场景。


7、代理模式(Proxy)

  代理模式:为其他对象提供一种代理以控制对这个对象的访问。
  代理模式也叫做委托模式,它是一项基本设计技巧。许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了代理模式,而且在日常的应用中,代理模式可以提供非常好的访问控制。
  通用类图如下:
  
抽象主题类
  抽象类或接口,是一个最普通的业务类型定义,无特殊要求。

public interface Subject {    void request();}

真实主题类
  也叫被委托角色、被代理角色。业务逻辑的具体执行者。

public class RealSubject implements Subject{    @Override    public void request() {        // 业务逻辑处理    }}

代理类
  也叫委托类、代理类。它负责对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现,并且在真实主题角色处理完毕前后做预处理和善后处理工作。

public class Proxy implements Subject {    // 要代理哪个实现类    private Subject subject;    // 默认被代理者    public Proxy(){        super();        this.subject = new Proxy();    // new代理对象    }    // 你要代理谁就产生该代理的实例,然后把代理者传递进来    public Proxy(Subject subject) {        this.subject = subject;    }    // 实现接口中定义的方法    @Override    public void request() {        this.before();        subject.request();        this.after();    }    private void after() {        System.out.println("after proxy!");    }    private void before() {        System.out.println("before proxy!");    }}

优点:
  1).职责清晰。真实的角色就是实现实际的业务逻辑,不用关心其他非本职的事务。
  2).高扩展性。具体主题角色只要实现了接口,无论它如何发生变化,我们代理类完全在不做任何修改的情况下使用。
  3).智能化。在动态代理中中体现。
使用场景:
  为什么要代理?就跟打官司要找律师一样。最经典的使用就是Spring AOP,这是一个非常典型的动态代理。
  
普通代理与强制代理
在网络上代理服务设置分为透明代理和普通代理。
  透明代理:就是用户不用设置代理服务器地址,就可以直接访问,也就是说代理服务器对用户来说是透明的,不用知道它存在。
  普通代理:需要用户自己设置代理服务器的IP地址,用户必须知道代理的存在。
  强制代理:需要用户必须通过真实角色查找代理角色,否则你不能访问。如你认识一明星,有件事需要她确认,于是你直接打这个明星的电话,但她说太忙,要我找她的经纪人,最后返回的还是她的代理。

7.1.动态代理

  动态代理是在实现阶段不用关心代理谁,而是在运行阶段才指定代理哪一个对象,AOP的核心就是采用了动态代理机制。(到时结合spring源码分析一波)

阅读全文
0 0
原创粉丝点击