设计模式(三)结构型模式介绍及实例

  本系列文章共分为六篇:
    设计模式(一)设计模式的分类与区别
    设计模式(二)创建型模式介绍及实例
    设计模式(三)结构型模式介绍及实例
    设计模式(四)行为型模式介绍及实例(上)
    设计模式(五)行为型模式介绍及实例(下)
    设计模式(六)设计模式的常见应用

  上篇文件介绍了创建型模式,本篇将介绍结构型模式。结构型模式的主要关注点是“如何将类或对象按某种布局组成更大的结构”。

一、适配器模式

  将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式对象结构型模式两种,前者之间的耦合度比后者高,且要求开发者了解现有组件库中的相关组件的内部结构,所以应用相对较少些。即:在开发者调用的接口和现有接口不一致时,增加一个"转换接口"。

  • 适配器模式角色
     1、目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
     2、适配者(Adaptee)类:它是被访问和适配的现存的组件接口
     3、适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

  注意事项:详细设计阶段不要考虑使用适配器模式,使用主要场景为扩展应用中。

  • 适配器模式的优点
     1)客户端通过适配器可以透明地调用目标接口。
     2)复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
     3)将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
  • 适配器模式的缺点
     过多地使用适配器,会让系统非常零乱,不易整体进行把握。

1.1 适配器模式实现方式

  适配器模式的实现方式较多,主要方式如下:

1.1.1 类适配器模式

  该模式是通过类的继承来实现适配器功能,是较为简单的一种适配器模式。简单来说,创建新类,继承源类,并实现新接口:

	class adapter extends oldClass implements newFunc{
	}

  以要充电转接口为例,某个电子产品的原始充电器是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.1.2 对象适配器模式

  对象适配器目标接口和适配者接口与类适配器模式中的相同,不同的是适配器类和测试类。在适配器类中,不再继承适配者接口,而是从调用方传一个适配者接口对象的参数。
  简单来说,创建新类持源类的实例,并实现新接口:

	class adapter implements newFunc { 
		private oldClass oldInstance;
	}

  其差异代码示例:

/*适配器类,充当转接口*/
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.2 适配器模式应用场景

 1)以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
 2)使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同

二、桥接模式

  桥接模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
  常见的方式是:用抽象类和接口两种方式来定义不同维度,然后用他们的继承类(或实现类)来进行组合,从而达到多种组合的目的。

  • 桥接模式主要角色
     1、抽象(Abstraction): 该类持有一个对实现角色的引用,抽象角色中的方法需要实现角色来实现,抽象角色一般为抽象类(构造函数规定子类要传入一个实现对象);
     2、修正抽象(RefinedAbstraction): Abstraction的具体实现,对Abstraction的方法进行完善和扩展;
     3、实现(Implementor): 确定实现维度的基本操作,提供给Abstraction使用。该类一般为接口或者抽象类;
     4、具体实现(ConcreteImplementor): Implementor的具体实现。

  • 桥接模式的优点
     1)由于抽象与实现分离,所以扩展能力强;
     2)其实现细节对客户透明。

  • 桥接模式的缺点
     聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,增加了系统的理解与设计难度。

2.1 桥接模式实现方式

  桥接模式的意义在于将系统中不同维度的功能分开来实现,降低耦合性。
  创建抽象Abstraction:

/**
 * 抽象 Abstraction 
 */
public abstract class Abstraction {
    protected IImplementor mImplementor;

    public Abstraction(IImplementor implementor) {
        this.mImplementor = implementor;
    }

    public void operation() {
        this.mImplementor.operationImpl();
    } 
}

  创建修正抽象角色RefinedAbstraction:

/**
 * 修正抽象 RefinedAbstraction
 */
public class RefinedAbstraction extends Abstraction {
    public RefinedAbstraction(IImplementor implementor) {
        super(implementor);
    }

    @Override
    public void operation() {
        super.operation();
        System.out.println("refined operation");
    }
}

  创建实现角色Implementor:

public interface IImplementor {
    void operationImpl();
}

  创建具体实现角色ConcreteImplementorA:

public class ConcreteImplementorA implements IImplementor {
    public void operationImpl() {
        System.out.println("I'm ConcreteImplementor A");
    }
}

  测试类:

public class Test {
    public static void main(String[] args) {
        // 实现角色A
        IImplementor imp = new ConcreteImplementorA();
        // 抽象角色,聚合实现
        Abstraction abs = new RefinedAbstraction(imp);
        // 执行操作
        abs.operation();
    }
}

  测试结果:

2.2 桥接模式应用场景

 1)当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
 2)当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
 3)当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。
 4)不希望或不适用使用继承的场景。
 5)接口或抽象类不稳定的场景。
 6)重用性要求较高的场景。

  注意:发现类的继承有N层时,可以考虑使用桥梁模式。桥梁模式主要考虑如何拆分抽象和实现。

三、组合模式

  组合模式:又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性。即:定义一个接口,让树枝对象和树叶对象都实现该接口,并具有不同的实现,进而表现整体与部分的关系。

  • 组合模式主要角色
     1、抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
     2、树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
     3、叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

  在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式。

  Component为组合中的对象声明接口,在适当情况下,实现所有类共有接口的默认行为;Left表示叶对象节点,叶结点没有子节点;Composite定义枝节点行为,用来存储子节点。
  上述方式实现的组合模式称为,透明方式中,在Component中声明了所有管理子对象的方法,其中包括add、remove等。这样实现Component接口的所有子对象都具备了add和remove,这样做的好处是叶结点也枝节点对于外界没有区别,他们具备完全一致的行为接口。但是问题也很明显,因为Left类不具备add、remove方法的功能,所以实现他们也是没有意义的。
  要克服上面的问题,可以采用安全方式,也就是在Component接口中不声明add和remove方法。不过由于不够透明,所以树叶和树枝具有不同的接口,客户端的调用需要做相应的判断,会带来一些不便。

  当发现需求中是体现部分与整体层次的结构时,以及希望用户可以忽略组合对象和单个对象的不同,统一地使用组合结构中的所有对象时,就可以考虑使用组合模式。

  • 组合模式的优点
     1、组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
     2、更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”。
  • 组合模式的缺点
     1、设计较复杂,客户端需要花更多时间理清类之间的层次关系;
     2、不容易限制容器中的构件;
     3、不容易用继承的方法来增加构件的新功能。

3.1 组合模式实现方式

3.1.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.1.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.2 组合模式应用场景

  1、在需要表示一个对象整体与部分的层次结构的场合,如树形菜单、文件和文件夹管理。
  2、要求对用户隐藏组合对象与单个对象的不同,用户可以用统一的接口使用组合结构中的所有对象的场合。
  3、只要是树形结构,就考虑使用组合模式。
  4、从一个整体中能够独立出部分模块或功能的场景。

四、装饰器模式

  装饰器模式:在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。即:不改变现有对象的结构,动态地增加一些功能。
  就增加功能来说,装饰器模式相比生成子类更为灵活 。

  • 装饰器模式角色
     1、抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
     2、具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
     3、抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
     4、具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
  • 装饰器模式的优点
     1、采用装饰模式扩展对象的功能比采用继承方式更加灵活
     2、可以设计出多个不同的具体装饰类,创造出多个不同行为的组合。
  • 装饰器模式的缺点
     增加了许多子类,如果过度使用会使程序变得很复杂。

4.1 装饰器模式实现方式

  以学生为例,进行体育运动时穿运动装,上课时转校服。学生为抽象构件,抽象装饰为角色,具体的装饰是运动者和上课者,这两种具体的装饰赋予了学生不同的行为。示例:

/*抽象构件角色*/
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.2 装饰器模式应用场景

  1、当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。
  2、当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰模式却很好实现。
  3、当对象的功能要求可以动态地添加,也可以再动态地撤销时。

五、外观模式

  外观模式:一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。即:在复杂的系统上,提供一个对外的一致性接口(外观)。

  • 外观模式主要角色
     1、Facade(外观角色):在客户端可以调用这个角色的方法,在外观角色中可以知道相关的一个或多个子系统的功能和责任,正常情况下将来自客户端的请求委派到对应的子系统中去,传递给相应的子系统对象处理
     2、SubSystem(子系统角色):每一个子系统是一个单独的类,也可以是一个类的集合,实现子系统的功能。每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求,子系统并不知道外观类的存在,对于子系统而已,外观角色仅仅是另一个客户端

  注意:

  1. 一个子系统可以有多个外观;
  2. 外观不参与子系统内的业务逻辑。
  • 外观模式的优点
     1、降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
     2、为一个复杂子系统提供一个简单接口,对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
     3、降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。

  • 外观模式的缺点
     1、不能很好地限制客户使用子系统类。
     2、增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。

5.1 外观模式实现方式

  以上班族为例,一个人早上起床后,去上班前要做一系列的事情:穿衣、洗漱、坐公交等。示例代码:

/*子系统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.2 外观模式应用场景

  1、对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
  2、当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
  3、当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。

  • 从系统维护的角度考虑
      首先,在设计初期阶段,应该要有意识地将不同的两个层分离,比如经典的三层架构,就需要考虑在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观Facade,使得降低耦合性。
      其次,在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,大多数的模式适用也都会产生很多很小的类,这本是好事,但也给外部调用它们的用户程序带来了使用上的困难,增加外观Facade可以提供一个简单的接口,减少它们之间的依赖
      第三,在维护一个遗留的大型系统时,可能这个系统以及非常难维护和扩展了,但因为它包含非常重要的功能,新的需求开发必须要依赖它,此时也可以使用外观模式。比如为新系统开发一个外观Facade类,来提供设计粗糙或高度复杂的遗留代码的比较简单清晰的接口,让新系统和Facade对象交互,Facade与遗留代码交互所有复杂的工作。

六、享元模式

  享元模式:运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。即:复用需要大量使用的、功能较为简单的对象。

  • 享元模式角色
     1、抽象享元角色(FlyWeight):享元对象抽象基类或者接口,同时定义出对象的外部状态和内部状态的接口或实现;
     2、具体享元角色(ConcreteFlyWeight):实现抽象享元类中的方法,是需要共享的对象类。
     3、享元工厂(FlyWeightFactory):维护一个享元对象的池,内部使用一个 Map 存储已经创建的享元对象。

  • 享元模式的优点
     相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。

  • 享元模式的缺点
     1、为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
     2、读取享元模式的外部状态会使得运行时间稍微变长。

6.1 享元模式实现方式

  此处以学生写字为例,有时需要用蓝色笔写字,有时需要用黑色笔写字,第一次写某颜色的字时,在文具盒中找出相应的笔,下次不用再从哪个文具盒中找,直接使用即可。示例代码:

/*非享元角色*/
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.2 享元模式应用场景

  1、系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
  2、大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态(外部状态相对独立,不影响内部状态)。
  3、由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。
  4、细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
  5、需要缓冲池的场景。

  注意:

  1)享元模式是线程不安全的,只有依靠经验,在需要的地方考虑一下线程安全,在大部分场景下不用考虑。对象池中的享元对象尽量多,多到足够满足为止。
  2)性能安全:外部状态最好以Java的基本类型作为标志,如String、int,可以提高效率。因为String和Integer有池,可以优化性能。

七、代理模式*

  代理模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。即:用代理对象来控制对原有对象的访问权限。

  • 代理模式角色
     1、Subjet类,定义被代理的类和代理类的通用接口,这样使用被代理类的地方就可以使用代理类。
     2、RealSubject类,被代理的类,实现原有的业务逻辑。
     3、Proxy类,代理类,持有一个RealSubject的引用,并且实现了Subject接口,这样就可以替代被代理的类。
  • 代理模式的优点
     1、代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
     2、代理对象可以扩展目标对象的功能
     3、代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度。
  • 代理模式的缺点
     1、在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢。
     2、增加了系统的复杂度。

7.1 代理模式实现方式

  代码模式的实现方式有两类:静态代理动态代理

7.1.1 静态代理

  静态代理可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口(常见),或者是共同继承某个类

//抽象主题接口
public interface Subject {
	//定义一个抽象方法
	void request();
}

//真实主题类
public class RealSubject implements Subject {
	//实现抽象方法
	@Override
	public void request() {
		//真实的业务逻辑
		System.out.println("RealSubject is doing something...");
	}
}

//代理主题类
public class Proxy implements Subject {
	//持有一个真实主题的引用
	private RealSubject realSubject;

	//构造方法,传入一个真实主题对象
	public Proxy(RealSubject realSubject) {
		this.realSubject = realSubject;
	}

	//实现抽象方法
	@Override
	public void request() {
		//在调用真实主题之前,可以执行一些额外操作
		System.out.println("Proxy is doing something before...");
		//调用真实主题的方法
		realSubject.request();
		//在调用真实主题之后,可以执行一些额外操作
		System.out.println("Proxy is doing something after...");
	}
}

  运行结果:

Proxy is doing something before…
RealSubject is doing something…
Proxy is doing something after…

  可以看出,代理对象在调用真实对象的方法之前和之后,都执行了一些额外的操作,从而对真实对象进行了增强或控制。

7.1.2 动态代理

  动态代理代理依靠反射来实现,代理类在JVM运行时动态生成,而不是编译期就能确定。
  动态代理技术就是用来产生一个对象的代理对象的。 使用代理模式必须要让代理类和目标类实现相同的接口,客户端通过代理类来调用目标方法,代理类会将所有的方法调用,分派到目标对象上反射执行,还可以在分派过程中添加"前置通知"和后置处理(是在invoke方法中添加,如在调用目标方法前校验权限,在调用完目标方法后打印日志等)等功能。

  要生成某一个对象的代理对象,这个代理对象通常也要编写一个类来生成,所以首先要编写用于生成代理对象的类。在java中如何用程序去生成一个对象的代理对象呢,java在JDK1.5之后提供了一个"java.lang.reflect.Proxy"类,通过"Proxy"类提供的一个newProxyInstance方法用来创建一个对象的代理对象,如下所示:

     static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 

  newProxyInstance方法用来返回一个代理对象,这个方法总共有3个参数:

ClassLoader loader:用来指明生成代理对象使用哪个类装载器
Class<?>[ ] interfaces:用来指明生成哪个对象的代理对象(代理类和目标类实现相同的接口)
InvocationHandler h : 用来指明产生的这个代理对象要做什么事情。(这个InvocationHandler 就是我们自己定义的InvocationHandler 对象,里边有我们编写invok方法)

  newProxyInstance方法生成代理对象过程:

  1. 通过Proxy.getProxyClass(ProxyGenerator.generateProxyClass(proxyName, interfaces);)获得动态代理类的class字节码内容。
  2. 把字节码通过传入的类加载器加载到虚拟机中,然后通过反射机制获得代理类的构造方法(方法签名为getConstructor(InvocationHandler.class)),生成代理类对象。

  示例代码:

//抽象主题接口
public interface Subject {
	//定义一个抽象方法
	void request();
}

//真实主题类
public class RealSubject implements Subject {
	//实现抽象方法
	@Override
	public void request() {
		//真实的业务逻辑
		System.out.println("RealSubject is doing something...");
	}
	}
	
	//日志处理器类,实现了InvocationHandler接口,用于定义代理逻辑
	public class LogHandler implements InvocationHandler {
		//持有一个目标对象的引用
		private Object target;
		//构造方法,传入一个目标对象
		public LogHandler(Object target) {
			this.target = target;
		}

	//实现invoke方法,用于调用目标对象的方法,并在之前和之后执行日志操作
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		//在调用目标对象之前,记录开始时间
		long startTime = System.currentTimeMillis();
		System.out.println("开始执行" + method.getName() + "方法...");
		//调用目标对象的方法,并获取返回值
		Object result = method.invoke(target, args);
		//在调用目标对象之后,记录结束时间和耗时
		long endTime = System.currentTimeMillis();
		long duration = endTime - startTime;
		System.out.println("结束执行" + method.getName() + "方法,耗时" + duration + "毫秒");
		return result;
	}
}

//客户端类
public class Client {
	public static void main(String[] args) {
		//创建一个真实主题对象
		RealSubject realSubject = new RealSubject();
		//创建一个日志处理器对象,并传入真实主题对象
		LogHandler logHandler = new LogHandler(realSubject);
		//使用Proxy类的静态方法newProxyInstance来动态地创建一个代理对象,传入真实主题对象的类加载器、接口和处理器
		Subject proxy = (Subject) Proxy.newProxyInstance(realSubject.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), logHandler);
		//使用代理对象来调用抽象方法
	proxy.request();
	}
}

  运行结果:

开始执行request方法…
RealSubject is doing something…
结束执行request方法,耗时1毫秒

7.2 代理模式应用场景

  按职责来划分,代理模式通常有以下使用场景: 1、远程代理。 2、虚拟代理。 3、Copy-on-Write代理。 4、保护(Protect or Access)代理。 5、Cache 代理。 6、防火墙(Firewall)代理。7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。

  代理模式的应用场景有以下几种:

  • 1、远程代理
      为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在与不同地址空间的事实。
  • 2、虚拟代理
      通过虚拟代理存放实例化需要很长时间的真实对象。
    3、安全代理
      用来控制真实对象访问时的权限,一般用于对象应该有不同的访问权限的时候。
  • 4、智能指引
      指当调用真实的对象时,代理处理另一些事情。如计算真实对象的引用次数,这样当对象没有引用时,可以自动释放它。

  总的来说,代理模式其实就是在访问对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值