结构型模式是为解决怎样组装现有的类,设计它们的交互方式,从而达到实现一定的功能目的。结构型模式包容了对很多问题的解决。例如:扩展性(外观、组成、代理、装饰)、封装(适配器、桥接)。
在解决了对象的创建问题之后,对象的组成以及对象之间的依赖关系就成了开发人员关注的焦点,因为如何设计对象的结构、继承和依赖关系会影响到后续程序的维护性、代码的健壮性、耦合性等。
适配器模式#
适配器模式(Adapter Pattern)是一种结构型设计模式,其核心作用是将一个类的接口转换为客户端期望的另一种接口,从而解决因接口不兼容而无法协同工作的类之间的协作问题。它类似于现实中的 “电源转换器”,通过中间层接口转换,让原本不匹配的组件能够协同工作。
主要组成部分#
- 目标接口(Target):客户端期望的接口,定义了需要调用的方法。
- 被适配者(Adaptee):需要适配的接口,已存在的类或接口,但其接口与目标不兼容。
- 适配器(Adapter):连接目标与被适配者的中间层,通过继承或组合实现接口转换。
实现#
-
类适配器(继承方式):适配器继承被适配者类,并实现目标接口。灵活性较低
// 目标接口 interface Target { void request(); } // 被适配者 class Adaptee { void specificRequest() { /*...*/ } } // 适配器 class Adapter extends Adaptee implements Target { @Override void request() { specificRequest(); } }
-
对象适配器(组合方式):适配器持有被适配者的对象实例,通过委托调用方法。
class Adapter implements Target { private Adaptee adaptee; public Adapter(Adaptee adaptee) { this.adaptee = adaptee; } @Override void request() { adaptee.specificRequest(); } }
-
接口适配器(缺省适配器):解决目标接口方法过多而客户端仅需部分实现的问题。它通过抽象类作为中间层,为接口方法提供默认实现(通常为空或简单逻辑),允许子类按需重写特定方法,从而避免强制实现所有接口方法的冗余。
// 目标接口 public interface MultiFunction { void print(); void scan(); void copy(); } // 抽象适配器类 public abstract class DefaultAdapter implements MultiFunction { @Override public void print() {} // 默认空实现 @Override public void scan() {} @Override public void copy() {} } // 具体适配器类 public class PrinterAdapter extends DefaultAdapter { @Override public void print() { System.out.println("打印功能已启用"); } // scan()和copy()无需重写,直接使用默认空逻辑 } // 调用 public class Client { public static void main(String[] args) { MultiFunction device = new PrinterAdapter(); device.print(); // 输出:打印功能已启用 } }
外观模式#
外观模式(Facade Pattern)是一种结构型设计模式,其核心目标是为复杂的子系统提供一个统一的、简化的接口,从而隐藏系统内部的复杂性,降低客户端与子系统之间的耦合度,提升系统的易用性和可维护性。
主要组成部分#
通过一个门面类(Facade) 封装多个子系统的交互逻辑,对外提供高层次的接口。客户端只需与门面类交互,无需直接调用复杂的子系统组件。
- 门面角色(Facade):统一协调子系统的调用,对外提供简化接口。
- 子系统角色(Subsystem Classes):实现具体功能的模块。
- 客户端(Client)、
桥接模式#
2. 桥接模式 — Graphic Design Patterns
设想如果要绘制矩形、圆形、椭圆、正方形,我们至少需要 4 个形状类,但是如果绘制的图形需要具有不同的颜色,如红色、绿色、蓝色等,此时至少有如下两种设计方案:
- 第一种设计方案是为每一种形状都提供一套各种颜色的版本。
- 第二种设计方案是根据实际需要对形状和颜色进行组合
桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。将抽象部分与它的实现部分分离,使它们都可以独立地变化。
桥接模式的结构#
实现案例#
桥接模式(Bridge Pattern)-(最通俗易懂的案例)_桥接器模式实例 - CSDN 博客
组合模式#
将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些对象即可以是容器对象也可以是叶子对象。但是由于容器对象和叶子对象在功能上的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户,它希望能够一致的对待容器对象和叶子对象。这就是组合模式的设计动机:组合模式定义了如何将容器对象和叶子对象进行递归组合,使得客户在使用的过程中无需区分,可以对它们进行一致的处理。
组合模式(Composite Pattern) 也称为 整体 - 部分(Part-Whole)模式,它的宗旨是通过将单个对象(叶子节点)和组合对象(树枝节点)用相同的接口进行表示,使得客户对单个对象和组合对象的使用具有一致性。
组合模式结构#
组合模式分为透明式的组合模式和安全式的组合模式。在该方式中,由于抽象构件声明了所有子类中的全部方法,所以客户端无须区别树叶对象和树枝对象,对客户端来说是透明的。但其缺点是:树叶构件本来没有 Add ()、Remove () 及 GetChild () 方法,却要实现它们(空实现或抛异常),这样会带来一些安全性问题。
透明模式的实现#
public class CompositePattern {
public static void main(String[] args) {
Component c0 = new Composite();
Component c1 = new Composite();
Component leaf1 = new Leaf("1");
Component leaf2 = new Leaf("2");
Component leaf3 = new Leaf("3");
c0.add(leaf1);
c0.add(c1);
c1.add(leaf2);
c1.add(leaf3);
c0.operation();
}
}
//抽象构件
interface Component {
public void add(Component c);
public void remove(Component c);
public Component getChild(int i);
public void operation();
}
//树叶构件
class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
public void add(Component c) {
}
public void remove(Component c) {
}
public Component getChild(int i) {
return null;
}
public void operation() {
System.out.println("树叶" + name + ":被访问!");
}
}
//树枝构件
class Composite implements Component {
private ArrayList<Component> children = new ArrayList<Component>();
public void add(Component c) {
children.add(c);
}
public void remove(Component c) {
children.remove(c);
}
public Component getChild(int i) {
return children.get(i);
}
public void operation() {
for (Object obj : children) {
((Component) obj).operation();
}
}
}
安全模式实现#
由于叶子和分支有不同的接口,客户端在调用时要知道树叶对象和树枝对象的存在,所以失去了透明性。
Leaf 类不需要实现那些对于它来说无用的方法,如 add、remove 和 getChild,这使得设计更加合理且易于理解。同时,这样的设计也避免了潜在的错误
代理模式#
控制对一个对象的访问,通过引入一个代理对象来管理这个访问。代理模式允许在不改变原对象接口的情况下,增加额外的功能或控制访问方式。
主要组成部分#
- Subject(目标接口):
- 定义了代理类和真实主题类的共同接口。
- 这使得客户端可以在不改变代码的情况下,使用代理或者真实主题对象。
- Real Subject(真实主题):
- 实现了
Subject
接口的具体业务逻辑。 - 这是客户端真正需要访问的对象。
- 实现了
- Proxy(代理):
- 也实现了
Subject
接口,并且维护一个对Real Subject
的引用。 - 代理类负责在客户端和真实主题之间进行协调,可能在请求前后执行一些额外的操作。
- 也实现了
静态代理#
一个代理只能服务于一个特定的业务实现类
1. 定义目标接口
首先,定义一个接口,该接口声明了需要被代理的方法。
JAVApublic interface Subject {
void request();
}
2. 实现真实主题类
创建一个实现了上述接口的具体类,该类包含实际的业务逻辑。
JAVApublic class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject: 处理请求");
}
}
3. 创建代理类
代理类也实现相同的接口,并持有一个对真实主题对象的引用。代理类可以在调用真实主题的方法之前或之后添加额外的功能。
JAVApublic class Proxy implements Subject {
private RealSubject realSubject;
public Proxy(RealSubject realSubject) {
this.realSubject = realSubject;
}
@Override
public void request() {
// 执行预处理操作
System.out.println("Proxy: 执行预处理操作");
realSubject.request();
// 执行后处理操作
System.out.println("Proxy: 执行后处理操作");
}
}
4. 编写客户端代码
客户端通过接口与代理类或真实主题对象交互,无需知道是直接调用还是通过代理。
JAVApublic class Client {
public static void main(String[] args) {
// 创建真实主题对象
Subject realSubject = new RealSubject();
// 创建代理对象,并将真实主题传递给代理
Subject proxy = new Proxy(realSubject);
// 调用代理的方法
proxy.request();
}
}
动态代理#
动态代理采用反射的机制,在运行时创建一个接口类的实例。
//业务接口
interface DateService {
void add();
void del();
}
// 具体业务类
class DateServiceImplA implements DateService {
@Override
public void add() {
System.out.println("成功添加!");
}
@Override
public void del() {
System.out.println("成功删除!");
}
}
// 代理
class ProxyInvocationHandler implements InvocationHandler {
private DateService service;
public ProxyInvocationHandler(DateService service) {
this.service = service;
}
// this.getClass().getClassLoader() 第一个是类加载器,用来加载代理类。
// service.getClass().getInterfaces() 被代理类实现的所有接口,使得代理对象能够提供与目标对象相同的接口视图。
// this 当前实例(即实现了 InvocationHandler 接口的对象),用于处理对代理对象的方法调用。
public Object getDateServiceProxy() {
return Proxy.newProxyInstance(this.getClass().getClassLoader(), service.getClass().getInterfaces(), this);
}
/* invoke 方法:
重写的 invoke 方法会在代理对象上调用任何方法时触发。它接收三个参数:
Object proxy:代表代理对象本身。
Method method:表示正在被调用的目标方法。
Object[] args:传递给目标方法的参数列表。
在方法体内,首先通过 method.invoke(service, args); 调用了目标对象的实际方法,并将结果保存到 result 变量中。
然后打印出一条日志信息,表明代理对象执行了某个方法,并记录了方法名和返回值。
最后返回目标方法的执行结果。
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
var result = method.invoke(service, args); // 让service调用方法,方法返回值
System.out.println(proxy.getClass().getName() + "代理类执行" + method.getName() + "方法,返回" + result + ",记录日志!");
return result;
}
}
//客户端
public class Test {
public static void main(String[] args) {
DateService serviceA = new DateServiceImplA();
DateService serviceProxy = (DateService) new ProxyInvocationHandler(serviceA).getDateServiceProxy();
serviceProxy.add();
serviceProxy.del();
}
}
装饰模式#
装饰模式(Decorator Pattern)-(最通俗易懂的案例)_装饰模式使用例子 - CSDN 博客
装饰模式(Decorator), 动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。装饰模式属于结构型模式,它是作为现有的类的一个包装。
装饰模式的主要角色包括:
-
Component(抽象组件):定义了被装饰对象和装饰器共同的接口。定义一个对象接口,可以给这些对象动态地添加职责。
-
ConcreteComponent(具体组件):实现了 Component 接口,是被装饰的对象。定义了一个具体的对象,也可以给这个对象添加一些职责。
-
Decorator(抽象装饰器):持有一个 Component 对象的引用,并定义了一个与 Component 具有相同接口的方法,以保持一致性。
-
ConcreteDecorator(具体装饰器):实现了 Decorator 定义的方法,在调用被装饰对象的方法前后可以执行一些额外的操作。
具体实现#
// 抽象组件
public interface INoodles {
public void cook();
}
// 具体组件
public class Noodles implements INoodles {
@Override
public void cook() {
System.out.println("的面条");
}
}
// 抽象装饰类
public abstract class NoodlesDecorator implements INoodles {
private INoodles noodles; //添加一个INoodles 的引用
public NoodlesDecorator(INoodles noodles){ //通过构造器来设置INoodles
this.noodles = noodles;
}
@Override
public void cook() {
if (noodles!=null){
noodles.cook();
}
}
}
// 具体装饰类
public class EggDecorator extends NoodlesDecorator {
public EggDecorator(INoodles noodles) {
super(noodles);
}
/**
* 重写父类的cook方法,并添加自己的实现,调用父类的cook方法,此cook方法是通过本类的构造器
* EggDecorator(INoodles noodles)传入的noodles的cook操作
*/
@Override
public void cook() {
System.out.println("加了一个荷包蛋");
super.cook();
}
}
// 具体装饰类
public class BeefDecorator extends NoodlesDecorator {
public BeefDecorator(INoodles noodles) {
super(noodles);
}
@Override
public void cook() {
System.out.println("加了一斤牛肉");
super.cook();
}
}
public class Client {
public static void main(String[] args) {
INoodles noodles1 = new Noodles();
INoodles noodles1WithEgg = new EggDecorator(noodles1);
noodles1WithEgg.cook();
}
}
装饰模式和代理模式的区别#
- 意图不同:装饰模式关注的是增强对象的功能,而代理模式更注重于控制对对象的访问。
- 实现细节:在装饰模式中,装饰器和被装饰的对象必须实现相同的接口,这样才能保证装饰链中的对象能够相互替换。而在代理模式中,代理类和目标对象也常常实现相同的接口,但这主要是为了保持一致性,并不是必须的条件。
- 设计考量:使用装饰模式时,设计者更多考虑的是如何在不修改现有代码的情况下扩展功能;而使用代理模式时,则更多地考虑如何保护、管理对资源的访问。