设计模式(三)-结构型模式

1.基本概念

结构型模式是设计模式中的一类,它主要关注如何将类或对象结合在一起形成更大的结构,以满足系统的需求。结构型模式通常涉及到对象的组合,以达到更复杂的功能或结构。

它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。

结构型模式包括:代理模式、适配器模式 、装饰者模式、桥接模式、外观模式、组合模式  、享元模式 。

 

2.代理模式

代理模式是一种结构型设计模式,它允许你提供一个代理对象,以控制对其他对象的访问。代理通常充当客户端和实际对象之间的中介,通过代理对象可以控制对实际对象的访问,同时隐藏实际对象的细节。适用于访问对象不适合或者不能直接引用目标对象。

Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。

代理(Proxy)模式分为三种角色:

  • 抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  • 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  • 代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。代理对象通常在调用真实主题对象之前或之后执行一些额外的操作,例如权限验证、缓存、延迟加载等。

 

静态代理

静态代理是代理模式的一种形式,它在编译时就已经确定了代理对象和真实对象之间的关系。在静态代理中,代理类和真实类都实现同一个接口或继承同一个父类,并且代理类持有对真实类的引用,在代理类中对真实类的方法进行包装或增强。

package com.dreams.pattern.proxy;

// 定义抽象主题接口
interface Subject {
    void request();
}

// 定义真实主题类
class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("RealSubject: Handling request...");
    }
}

// 定义代理类
class Proxy implements Subject {
    private RealSubject realSubject;

    public Proxy() {
        this.realSubject = new RealSubject();
    }

    @Override
    public void request() {
        // 在调用真实主题之前可以执行一些额外的操作
        System.out.println("Proxy: Performing additional operations before request...");
        
        // 调用真实主题的方法
        realSubject.request();

        // 在调用真实主题之后可以执行一些额外的操作
        System.out.println("Proxy: Performing additional operations after request...");
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        // 创建代理对象
        Proxy proxy = new Proxy();
        
        // 通过代理对象调用真实主题的方法
        proxy.request();
    }
}

Subject 是抽象主题接口,定义了一个 request() 方法。RealSubject 是真实主题类,实现了 Subject 接口,提供了真实的业务逻辑。Proxy 是代理类,也实现了 Subject 接口,并持有对RealSubject 对象的引用。在 Proxy 类中,对 request() 方法进行了增强,添加了额外的操作。在客户端代码中,通过代理对象 proxy 调用了 request() 方法,实际上是由代理对象去调用真实对象的方法,并在前后添加了额外的操作。这样,通过代理对象 Proxy,我们可以在不修改真实对象 RealSubject 的情况下,对其方法进行增强或包装。

 

JDK动态代理

动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。

如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题

package com.dreams.pattern.proxy.JDKProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 定义接口
interface Subject {
    void request();
}

// 具体实现类
class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("RealSubject: Handling request.");
    }
}

// InvocationHandler 实现类
class MyInvocationHandler implements InvocationHandler {
    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在调用目标对象方法前执行额外逻辑
        System.out.println("Before invoking " + method.getName());

        // 通过反射调用目标对象的方法
        Object result = method.invoke(target, args);

        // 在调用目标对象方法后执行额外逻辑
        System.out.println("After invoking " + method.getName());

        return result;
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        // 创建真实主题对象
        RealSubject realSubject = new RealSubject();

        // 创建 InvocationHandler 实现类对象
        MyInvocationHandler handler = new MyInvocationHandler(realSubject);

        // 使用 Proxy 类的静态方法动态生成代理对象
        Subject proxySubject = (Subject) Proxy.newProxyInstance(
            realSubject.getClass().getClassLoader(),
            realSubject.getClass().getInterfaces(),
            handler
        );

        // 通过代理对象调用方法
        proxySubject.request();
    }
}

如果我们希望在调用 request() 方法前后添加日志记录或权限验证等功能,只需在 MyInvocationHandler 的 invoke() 方法中添加相应的代码即可,而无需修改 RealSubject 的代码。这种灵活性和扩展性正是代理模式的优势所在。

当然也可以将main函数里的生成代理对象提取到一个工厂类里通过工厂调用。

 

CGLIB动态代理

如果没有定义接口,只定义了类。JDK代理是无法使用的,因为JDK动态代理要求必须定义接口,对接口进行代理。

CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。

使用需要加入依赖:

<dependencies>
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>2.2.2</version>
    </dependency>
</dependencies>

代码:

package com.dreams.pattern.proxy.CGLIBProxy;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

// 目标类
class TargetClass {
    public void doSomething() {
        System.out.println("目标类的方法被调用");
    }
}

// MethodInterceptor 实现类,用于处理方法调用
class CustomInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("在目标方法调用之前,添加额外逻辑");
        
        // 调用目标对象的方法
        Object result = proxy.invokeSuper(obj, args);
        
        System.out.println("在目标方法调用之后,添加额外逻辑");
        
        return result;
    }
}

public class CglibProxyExample {
    public static void main(String[] args) {
        // 创建 Enhancer 对象,用于生成代理类
        Enhancer enhancer = new Enhancer();
        
        // 设置目标类的父类,即被代理的类
        enhancer.setSuperclass(TargetClass.class);
        
        // 设置回调,即拦截器
        enhancer.setCallback(new CustomInterceptor());
        
        // 生成代理对象
        TargetClass proxy = (TargetClass) enhancer.create();
        
        // 调用代理对象的方法
        proxy.doSomething();
    }
}

在这个示例中,TargetClass 是我们的目标类,它有一个名为 doSomething() 的方法。CustomInterceptor 是我们实现的拦截器,用于在方法调用前后添加额外逻辑。在 main 方法中,我们使用 Enhancer 来创建代理对象,并将目标类和拦截器传入,最终生成代理对象并调用其方法。在执行过程中,拦截器会在目标方法调用前后添加额外逻辑。

同理,当然也可以将main函数里的生成代理对象提取到一个工厂类里通过工厂调用。

使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。

在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。

 

 

3.适配器模式

适配器模式 (Adapter Pattern):用于将一个类的接口转换成客户端所期望的另一个接口。它允许原本不兼容的接口之间进行合作。

适配器模式角色:

  • 目标接口(Target): 定义客户端使用的接口,客户端通过目标接口与适配器进行交互。它可以是抽象类或接口。
  • 适配器(Adapter): 实现了目标接口,并且持有一个对适配者对象的引用。适配器将客户端的请求转发给适配者对象,并将适配者对象的结果转换成符合目标接口的形式返回给客户端。
  • 适配者(Adaptee): 需要被适配的类,它定义了客户端无法直接使用的接口。

适配器模式可以分为两种不同的实现方式:

  • 类适配器模式(Class Adapter Pattern): 在类适配器模式中,适配器类通过多重继承同时实现了目标接口和适配者接口,从而将适配者接口转换成目标接口。
  • 对象适配器模式(Object Adapter Pattern): 在对象适配器模式中,适配器类持有一个适配者对象的引用,并实现了目标接口。适配器类将客户端的请求委托给适配者对象,并将其结果转换成符合目标接口的形式返回给客户端。

类适配器模式

定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件

package com.dreams.pattern.adapter.classAdaper;

// 目标接口
interface Target {
    void request();
}

// 适配者类
class Adaptee {
    void specificRequest() {
        System.out.println("Adaptee's specific request");
    }
}

// 适配器类(类适配器)
class Adapter extends Adaptee implements Target {
    public void request() {
        specificRequest(); // 调用适配者类的方法
        System.out.println("Adapter's request");
    }
}

// 客户端类
public class Client {
    public static void main(String[] args) {
        Target adapter = new Adapter(); // 使用适配器

        adapter.request(); // 调用适配器的方法
    }
}

在这个示例中,Target 是目标接口,Adaptee 是适配者类,Adapter 是适配器类,采用了类适配器模式。适配器类 Adapter 继承了适配者类 Adaptee 并实现了目标接口 Target,在 request 方法中调用了适配者类的 specificRequest 方法,并添加了额外的功能。客户端通过目标接口 Target 与适配器进行交互,调用适配器的方法request(),实际上调用了适配者类的 specificRequest 方法。将一个类的接口转换成客户端所期望的另一个接口。

注意:

类适配器模式违背了合成复用原则。合成复用原则强调的是通过组合(composition)而不是继承(inheritance)来实现代码复用,这样可以降低类与类之间的耦合度,提高系统的灵活性。在类适配器模式中,适配器类通过继承适配者类来实现目标接口,这意味着适配器类与适配者类之间存在着强耦合关系。

类适配器是客户类有一个接口规范的情况下可用,反之不可用。

 

对象适配器模式

package com.dreams.pattern.adapter.objectAdapter;

// 目标接口
interface Target {
    void request();
}

// 适配者类
class Adaptee {
    void specificRequest() {
        System.out.println("Adaptee's specific request");
    }
}

// 适配器类(对象适配器)
class Adapter implements Target {
    private Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    public void request() {
        adaptee.specificRequest(); // 调用适配者类的方法
        System.out.println("Adapter's request");
    }
}

// 客户端类
public class Client {
    public static void main(String[] args) {
        Adaptee adaptee = new Adaptee();
        Target adapter = new Adapter(adaptee); // 使用适配器
        
        adapter.request(); // 调用适配器的方法
    }
}

适配器类 Adapter 通过组合(composition)的方式持有一个适配者对象 Adaptee,并实现了目标接口 Target。适配器类的 request 方法中,调用了适配者对象的 specificRequest 方法,并添加了额外的功能。这样的设计更符合合成复用原则,通过组合而不是继承来实现代码复用,降低了类与类之间的耦合度。同时,对象适配器模式也更灵活,可以适配任意具有特定接口的适配者类。

Reader(字符流)、InputStream(字节流)的适配使用的是InputStreamReader。

InputStreamReader继承自java.io包中的Reader,对他其中的抽象的未实现的方法给出实现。

InputStreamReader是对同样实现了Reader的StreamDecoder的封装。

StreamDecoder不是Java SE API中的内容,是Sun JDK给出的自身实现。他们对构造方法中的字节流类(InputStream)进行封装,并通过该类进行了字节流和字符流之间的解码转换。

 

4.装饰者模式

装饰者模式 (Decorator Pattern):装饰者模式是一种结构型设计模式,用于动态地给一个对象添加一些额外的职责,而不需要继承子类。通过将对象封装在装饰器对象中,可以在运行时动态地添加新功能。

装饰者模式的角色结构:

  • Component(组件):定义了一个对象接口,可以给这些对象动态地添加职责。在装饰者模式中,通常是一个抽象类或接口,定义了被装饰者和装饰者共同的接口。
  • ConcreteComponent(具体组件):实现了Component接口的具体对象,是被装饰者。
  • Decorator(装饰者):持有一个Component对象的引用,并实现了Component接口。通常是一个抽象类,其子类可以为具体组件添加功能。
  • ConcreteDecorator(具体装饰者):实现了Decorator接口的具体装饰者对象,负责给具体组件添加额外的职责。

代码:

// 定义咖啡接口
interface Coffee {
    double getCost(); // 获取咖啡价格
    String getDescription(); // 获取咖啡描述
}

// 具体咖啡类
class SimpleCoffee implements Coffee {
    @Override
    public double getCost() {
        return 1.0; // 简单咖啡价格为1.0
    }

    @Override
    public String getDescription() {
        return "普通咖啡"; // 返回咖啡描述
    }
}

// 装饰者抽象类
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee; // 持有一个Coffee对象的引用

    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee; // 构造方法接收被装饰对象
    }

    @Override
    public double getCost() {
        return decoratedCoffee.getCost(); // 委托给被装饰对象
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription(); // 委托给被装饰对象
    }
}

// 具体装饰者类:牛奶装饰者
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee); // 调用父类构造方法
    }

    @Override
    public double getCost() {
        return super.getCost() + 0.5; // 基于被装饰对象的价格增加牛奶价格
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ",加牛奶"; // 返回咖啡描述,并添加牛奶
    }
}

// 具体装饰者类:糖装饰者
class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee); // 调用父类构造方法
    }

    @Override
    public double getCost() {
        return super.getCost() + 0.2; // 基于被装饰对象的价格增加糖价格
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ",加糖"; // 返回咖啡描述,并添加糖
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        // 创建一个普通咖啡对象
        Coffee simpleCoffee = new SimpleCoffee();
        System.out.println("价格:" + simpleCoffee.getCost() + "元;描述:" + simpleCoffee.getDescription());

        // 为普通咖啡添加牛奶装饰者
        Coffee milkCoffee = new MilkDecorator(simpleCoffee);
        System.out.println("价格:" + milkCoffee.getCost() + "元;描述:" + milkCoffee.getDescription());

        // 为加牛奶的咖啡再添加糖装饰者
        Coffee sugarMilkCoffee = new SugarDecorator(milkCoffee);
        System.out.println("价格:" + sugarMilkCoffee.getCost() + "元;描述:" + sugarMilkCoffee.getDescription());
    }
}

Coffee 接口定义了咖啡的基本行为,SimpleCoffee 是一个具体的咖啡实现。CoffeeDecorator 是一个装饰者抽象类,MilkDecorator 和 SugarDecorator 是具体的装饰者,它们分别添加了牛奶和糖的功能。

好处:

  • 饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则,继承是静态的附加责任,装饰者则是动态的附加责任。
  • 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责

当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时可以使用装饰者模式。

不能采用继承的情况主要有两类:

  • 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
  • 第二类是因为类定义不能继承(如final类)

IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter。

BufferedWriter使用装饰者模式对Writer子实现类进行了增强,添加了缓冲区,提高了写数据的效率。

 

 

5.桥接模式

桥接模式 (Bridge Pattern):将抽象部分与实现部分分离,使它们可以独立地变化。通过对象组合的方式,将抽象和实现解耦,从而实现解耦的目的。

桥接(Bridge)模式包含以下主要角色:

  • 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
  • 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
  • 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
  • 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。
package com.dreams.pattern.bridge;

// 实现部分接口
interface Color {
    void applyColor();
}

// 具体实现部分:红色
class RedColor implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying red color");
    }
}

// 具体实现部分:蓝色
class BlueColor implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying blue color");
    }
}

// 抽象部分
abstract class Shape {
    protected Color color;

    public Shape(Color color) {
        this.color = color;
    }

    abstract void applyColor();
}

// 具体形状:圆形
class Circle extends Shape {
    public Circle(Color color) {
        super(color);
    }

    @Override
    void applyColor() {
        System.out.print("Circle filled with ");
        color.applyColor();
    }
}

// 具体形状:正方形
class Square extends Shape {
    public Square(Color color) {
        super(color);
    }

    @Override
    void applyColor() {
        System.out.print("Square filled with ");
        color.applyColor();
    }
}

public class BridgePatternExample {
    public static void main(String[] args) {
        Color red = new RedColor();
        Color blue = new BlueColor();

        Shape redCircle = new Circle(red);
        redCircle.applyColor();

        Shape blueSquare = new Square(blue);
        blueSquare.applyColor();
    }
}

在这个例子中有两个维度:形状和颜色。形状是抽象部分,而颜色是实现部分。通过桥接模式,我们可以使形状和颜色的变化彼此独立,从而可以轻松地组合它们。

 

使用场景

  • 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
  • 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
  • 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。

 

6.外观模式

外观模式 (Facade Pattern):提供了一个统一的接口,用于访问子系统中的一群接口。它定义了一个高层接口,使得子系统更容易使用。

又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。

外观(Facade)模式是“迪米特法则”的典型应用

外观模式的组成部分:

  • 外观(Facade):提供了一个高层次的接口,这个接口使用了子系统中的一群接口,以便更容易地使用这个子系统。外观模式的客户端通过外观来与子系统交互,而不是直接与子系统的类交互。
  • 子系统(Subsystems):由多个类组成的一组类,每个类都实现了子系统的一部分功能。这些类处理系统的底层工作,但客户端不直接与它们交互,而是通过外观来访问它们。

代码:

package com.dreams.pattern.facad;

// 子系统 CPU
class CPU {
    public void start() {
        System.out.println("CPU 启动");
    }

    public void shutdown() {
        System.out.println("CPU 关闭");
    }
}

// 子系统 内存
class Memory {
    public void load() {
        System.out.println("内存加载数据");
    }

    public void free() {
        System.out.println("内存释放数据");
    }
}

// 子系统 硬盘
class HardDrive {
    public void read() {
        System.out.println("硬盘读取数据");
    }

    public void write() {
        System.out.println("硬盘写入数据");
    }
}

// 外观类 ComputerFacade
class ComputerFacade {
    private CPU cpu;
    private Memory memory;
    private HardDrive hardDrive;

    public ComputerFacade() {
        this.cpu = new CPU();
        this.memory = new Memory();
        this.hardDrive = new HardDrive();
    }

    public void start() {
        cpu.start();
        memory.load();
        hardDrive.read();
        System.out.println("电脑启动完成");
    }

    public void shutdown() {
        hardDrive.write();
        memory.free();
        cpu.shutdown();
        System.out.println("电脑关闭完成");
    }
}

// 客户端代码
public class FacadePatternExample {
    public static void main(String[] args) {
        ComputerFacade computer = new ComputerFacade();

        // 启动电脑
        System.out.println("----- 开始启动电脑 -----");
        computer.start();
        System.out.println("----- 电脑已启动 -----");

        System.out.println();

        // 关闭电脑
        System.out.println("----- 开始关闭电脑 -----");
        computer.shutdown();
        System.out.println("----- 电脑已关闭 -----");
    }
}

CPU、Memory 和 HardDrive 分别代表了三个子系统,ComputerFacade 是外观类,封装了这些子系统的操作。客户端只需要与 ComputerFacade 类交互,而不需要了解具体的子系统细节。通过 ComputerFacade 提供的 start 和 shutdown 方法,客户端可以启动和关闭电脑,而无需关心内部是如何实现的。

 

源码使用

使用tomcat作为web容器时,接收浏览器发送过来的请求,tomcat会将请求信息封装成ServletRequest对象,ServletRequest是一个接口,它还有一个子接口HttpServletRequest,该request对象肯定是一个HttpServletRequest对象的子实现类对象,是一个名为RequestFacade的类的对象。定义 RequestFacade 类,分别实现 ServletRequest ,同时定义私有成员变量 Request ,并且方法的实现调用 Request 的实现。然后,将 RequestFacade上转为 ServletRequest 传给 servlet 的 service 方法,这样即使在 servlet 中被下转为 RequestFacade ,也不能访问私有成员变量对象中的方法。既用了 Request ,又能防止其中方法被不合理的访问。

 

 

7.组合模式

组合模式 (Composite Pattern):将对象组合成树形结构以表示“部分-整体”的层次结构。它使得客户端对单个对象和组合对象的使用具有一致性。

组合模式的组成部分:

  • 组件(Component):定义了组合中的对象的通用接口,可以是抽象类或接口,它声明了组件对象的一些通用操作,包括增加、删除子组件以及获取子组件的方法。
  • 叶子(Leaf):表示组合中的叶子节点对象,叶子节点没有子节点,它实现了组件接口,但不包含子组件。是系统层次遍历的最小单位。
  • 容器(Composite):表示组合中的容器节点对象,容器节点可以包含子节点,它实现了组件接口,并提供了增加、删除、获取子组件的方法。

组合模式客户端可以统一对待单个对象和组合对象,无需关心是叶子节点还是容器节点,从而简化了客户端代码。

又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

package com.dreams.pattern.composite;

import java.util.ArrayList;
import java.util.List;

// 组件接口
interface FileSystemComponent {
    void printName();
}

// 叶子节点
class File implements FileSystemComponent {
    private String name;

    public File(String name) {
        this.name = name;
    }

    @Override
    public void printName() {
        System.out.println("File: " + name);
    }
}

// 容器节点
class Directory implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> components;

    public Directory(String name) {
        this.name = name;
        this.components = new ArrayList<>();
    }

    public void addComponent(FileSystemComponent component) {
        components.add(component);
    }

    public void removeComponent(FileSystemComponent component) {
        components.remove(component);
    }

    @Override
    public void printName() {
        System.out.println("Directory: " + name);
        for (FileSystemComponent component : components) {
            component.printName();
        }
    }
}

// 客户端代码
public class CompositePatternExample {
    public static void main(String[] args) {
        Directory root = new Directory("Root");

        Directory documents = new Directory("Documents");
        documents.addComponent(new File("Resume.docx"));
        documents.addComponent(new File("Presentation.pptx"));

        Directory pictures = new Directory("Pictures");
        pictures.addComponent(new File("Vacation.jpg"));
        pictures.addComponent(new File("FamilyPhoto.jpg"));

        root.addComponent(documents);
        root.addComponent(pictures);

        root.printName();
    }
}

在这个示例中,File 类表示叶子节点,Directory 类表示容器节点,客户端通过组合这些类来构建文件系统的树形结构。然后,客户端只需要与 FileSystemComponent 接口交互,无需关心具体的叶子节点和容器节点的实现细节。

组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作。

分类:

组合模式通常可以分为两种不同的实现方式,即安全式(安全性)组合模式和透明式(透明性)组合模式。

1. 安全式组合模式

在安全式组合模式中,将组件对象分为两个不同的类别:叶子节点类和容器节点类。这两种类别的对象有不同的接口,叶子节点对象的接口不包含增加、删除、获取子组件等操作,而容器节点对象的接口包含这些操作。

特点:

  • 明确的界限:叶子节点和容器节点有明确的类别,接口的区分使得叶子节点对象不会包含对子组件的操作。
  • 安全性:客户端无法对叶子节点执行增加、删除子组件等操作,因此更加安全。

注意:

透明组合模式也是组合模式的标准形式。

 

2. 透明式组合模式

在透明式组合模式中,将所有的节点对象都视为组件对象,并在接口中声明所有的操作,包括叶子节点特有的操作。这样,所有的节点对象都具有相同的接口,但对于叶子节点来说,一些操作可能是空的或抛出异常的。

特点:

  • 简单直观:所有的节点对象具有相同的接口,简化了客户端的使用。
  • 透明性:客户端可以对所有的节点执行相同的操作,但对于叶子节点来说,一些操作可能是无意义的。

 

8.享元模式

享元模式 (Flyweight Pattern):通过共享技术有效地支持大量细粒度的对象,它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。

对象被分为两部分:内部状态(Intrinsic State)和外部状态(Extrinsic State)。

  • 内部状态:是对象的固有属性,不会随环境的改变而改变,可以被多个对象共享。
  • 外部状态:是对象的上下文信息,随环境的改变而改变,因此不能被共享,需要在外部进行管理。

享元模式的角色:

  • 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
  • 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
  • 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
  • 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

考虑一个简单的游戏场景,有许多棋子,每个棋子有颜色和形状。颜色是棋子的内部状态,而形状是外部状态。

package com.dreams.pattern.flyweight;

import java.util.HashMap;
import java.util.Map;

// 享元接口
interface ChessPiece {
    void display(String shape);
}

// 具体享元类
class ConcreteChessPiece implements ChessPiece {
    private String color;

    public ConcreteChessPiece(String color) {
        this.color = color;
    }

    @Override
    public void display(String shape) {
        System.out.println("Displaying " + color + " " + shape + " chess piece");
    }
}

// 享元工厂
class ChessPieceFactory {
    private Map<String, ChessPiece> pieces = new HashMap<>();

    public ChessPiece getChessPiece(String color) {
        ChessPiece piece = pieces.get(color);
        if (piece == null) {
            piece = new ConcreteChessPiece(color);
            pieces.put(color, piece);
        }
        return piece;
    }
}

// 客户端
public class Client {
    public static void main(String[] args) {
        ChessPieceFactory factory = new ChessPieceFactory();
        ChessPiece blackPawn1 = factory.getChessPiece("black");
        blackPawn1.display("pawn");
        ChessPiece blackPawn2 = factory.getChessPiece("black");
        blackPawn2.display("pawn");
        // blackPawn1 和 blackPawn2 实际上是同一个对象,因为颜色相同,内部状态相同
        if (blackPawn1 == blackPawn2){
            System.out.println("同一个对象");
        }
    }
}

棋子的颜色是内部状态,可以被共享,而形状是外部状态,需要在外部进行管理。享元工厂负责创建和管理棋子对象,确保相同颜色的棋子实际上是同一个对象,从而节省了内存。

在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

JDK源码使用:Integer 默认先创建并缓存 -128 ~ 127之间数的 Integer 对象,当调用 valueOf 时如果参数在 -128 ~ 127之间则计算下标并从缓存中返回,否则创建一个新的 Integer 对象。

 

参考

黑马设计模式

大话设计模式

图解设计模式

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇