系统架构设计师:设计模式——结构型设计模式
一、结构型设计模式
结构型设计模式涉及如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口或实现。一个简单的例子是采用多重继承方法将两个以上的类组合成一个类,结果这个类包含了所有父类的性质。
这一模式尤其有助于多个独立开发的类库协同工作。其中一个例子是类形式的 Adapter 模式。一般来说,适配器使得一个接口与其他接口兼容,从而给出了多个不同接口的统一抽象。为此,类适配器对一个 adaptee 类进行私有继承。这样,适配器就可以用 adaptee 的接口表示它的接口。
结构型对象模式不是对接口和实现进行组合,而是描述了如何对一些对象进行组合,从而实现新功能的一些方法。因为可以在运行时刻改变对象组合关系,所以对象组合方式具有更大的灵活性,而这种机制用静态类组合是不可能实现的。
Composite 模式是结构型对象模式的一个实例。它描述了如何构造一个类层次式结构,这一结构由两种类型的对象所对应的类构成。其中的组合对象使得用户可以组合基元对象以及其他的组合对象,从而形成任意复杂的结构。
在 Proxy 模式中,proxy 对象作为其他对象的一个方便的替代或占位符。它的使用可以有多种形式,例如可以在局部空间中代表一个远程地址空间中的对象,也可以表示一个要求被加载的较大的对象,还可以用来保护对敏感对象的访问。
Proxy 模式还提供了对对象的一些特有性质的一定程度上的间接访问,从而可以限制、增强或修改这些性质。
Flyweight 模式为了共享对象定义了一个结构。至少有两个原因要求对象共享:效率和一致性。Flyweight 的对象共享机制主要强调对象的空间效率。使用很多对象的应用必须考虑每一个对象的开销。使用对象共享而不是进行对象复制,可以节省大量的空间资源。但是,仅当这些对象没有定义与上下文相关的状态时,它们才可以被共享。
Flyweight 的对象没有这样的状态。任何执行任务时需要的其他一些信息仅当需要时才传递过去。由于不存在与上下文相关的状态,因此 Flyweight 对象可以被自由地共享。
如果说 Flyweight 模式说明了如何生成很多较小的对象,那么 Facade 模式则描述了如何用单个对象表示整个子系统。模式中的 facade 用来表示一组对象,facade 的职责是将消息转发给它所表示的对象。Bridge 模式将对象的抽象和其实现分离,从而可以独立地改变它们。
Decorator 模式描述了如何动态地为对象添加职责。Decorator 模式是一种结构型模式,这一模式采用递归方式组合对象,从而允许添加任意多的对象职责。
例如,一个包含用户界面组件的 Decorator 对象可以将边框或阴影这样的装饰添加到该组件中,或者它可以将窗口滚动和缩放这样的功能添加到组件中。
可以将一个 Decorator 对象嵌套在另外一个对象中,就可以很简单地增加两个装饰,添加其他的装饰也是如此。因此,每个 Decorator 对象必须与其组件的接口兼容并且保证将消息传递给它。Decorator 模式在转发一条信息之前或之后都可以完成它的工作(例如绘制组件的边框)。许多结构型模式在某种程度上具有相关性。
(一)Adapter模式
1. 模式名称
适配器模式(Adapter Pattern)
2. 意图解决的问题
适配器模式旨在解决不兼容接口之间的协作问题。具体来说,它允许原本由于接口不匹配而无法一起工作的类能够协同工作。适配器模式通常用于以下场景:
- 第三方库的集成:当你需要使用一个已经存在的类,但其接口与你的需求不匹配时。
- 系统升级或重构:在对旧系统的改造过程中,可能需要让新代码与遗留代码共存,这时就可以利用适配器来桥接两者之间的差异。
- 实现透明的互操作性:当两个不同的系统或者组件需要交换数据或者服务调用,但是它们的接口定义不同,适配器可以提供一种统一的方式来访问这些资源。
适配器模式的主要目的是通过转换接口,使得原本由于接口不兼容而不能合作的类能够一起工作,从而提高了代码的复用性和灵活性。
3. 模式描述
适配器模式有两种主要的形式:类适配器和对象适配器。尽管两种形式的核心思想相同,但其实现方式略有差异。
类适配器(通过多重继承实现)
类适配器使用的是多重继承的方式,即适配器类同时继承目标接口和被适配的类,从而实现接口的转换。然而,在Java等不支持多重继承的语言中,这种方式并不适用。
对象适配器(通过组合实现)
对象适配器则采用组合的方法,适配器类包含了一个对被适配者类型的引用,通过该引用来调用被适配者的功能,并将结果适配为目标接口所需的格式。
角色说明:
- Target(目标抽象类):定义客户端所期待使用的接口。
- Adaptee(适配者类):已有的、功能正确的但接口不符合要求的类。
- Adapter(适配器类):负责将Adaptee的接口转换为Target接口,使得原本因接口不一致而无法合作的类能够一起工作。
示例代码
// 目标接口
public interface Target {void request();
}// 已有类,其接口与目标接口不兼容
public class Adaptee {public void specificRequest() {System.out.println("Called specificRequest()");}
}// 适配器类,实现了Target接口并持有Adaptee的一个实例
public class Adapter implements Target {private Adaptee adaptee;public Adapter(Adaptee adaptee) {this.adaptee = adaptee;}@Overridepublic void request() {// 调用现有类中的方法,并进行必要的适配adaptee.specificRequest();}
}// 使用示例
public class Main {public static void main(String[] args) {Adaptee adaptee = new Adaptee();Target target = new Adapter(adaptee);target.request(); // 输出: Called specificRequest()}
}
4.应用场景
(1)场景描述
假设你正在开发一个电子商务平台,需要支持多种在线支付方式,比如支付宝、微信支付和PayPal。每个支付服务提供商都有其独特的API接口和调用方式。你的目标是为用户提供统一的支付体验,即用户不需要知道他们实际使用的是哪个支付服务。
(2)遇到的困难:
- 接口不一致:每个支付服务提供商提供的API接口都不相同,这意味着你需要编写不同的代码来与每个支付服务进行交互。
- 复杂性增加:随着业务的发展,可能需要接入更多的支付服务。每次接入新的支付服务都需要修改现有的代码,这不仅增加了工作量,还提高了出错的风险。
- 维护成本高:由于不同支付服务的接口差异较大,维护这些接口的代码会变得非常复杂,尤其是当某个支付服务更新其API时,可能需要对整个系统进行调整。
(3)Adapter模式如何解决问题:
通过应用适配器模式(Adapter Pattern),我们可以将不同的支付服务API适配为统一的接口,从而解决上述问题。
- 提供统一接口:定义一个统一的目标接口(Target),所有具体的支付操作都通过这个接口来进行。这样,无论底层使用的是哪种支付服务,对于客户端来说都是透明的。
- 实现适配器类:针对每一个具体的支付服务(Adaptee),实现一个对应的适配器(Adapter)。适配器负责将支付请求转换成特定于该支付服务的调用,并将结果返回给客户端。
- 简化扩展流程:如果需要添加新的支付服务,只需创建一个新的适配器类,而无需修改现有的代码,符合开闭原则(对扩展开放,对修改关闭)。
(4)示例代码实现
// 目标接口,定义了客户端期望使用的支付方法
public interface PaymentGateway {void processPayment(double amount);
}// 已有类,代表支付宝支付服务的具体实现
public class AlipayService {public void sendPayment(double amount) {System.out.println("Processing payment of " + amount + " via Alipay.");}
}// 已有类,代表PayPal支付服务的具体实现
public class PayPalService {public void makePayment(double amount) {System.out.println("Processing payment of " + amount + " via PayPal.");}
}// 适配器类,将AlipayService适配为目标接口PaymentGateway
public class AlipayAdapter implements PaymentGateway {private AlipayService alipayService;public AlipayAdapter(AlipayService alipayService) {this.alipayService = alipayService;}@Overridepublic void processPayment(double amount) {alipayService.sendPayment(amount);}
}// 适配器类,将PayPalService适配为目标接口PaymentGateway
public class PayPalAdapter implements PaymentGateway {private PayPalService payPalService;public PayPalAdapter(PayPalService payPalService) {this.payPalService = payPalService;}@Overridepublic void processPayment(double amount) {payPalService.makePayment(amount);}
}// 客户端代码
public class Main {public static void main(String[] args) {// 创建支付服务实例AlipayService alipay = new AlipayService();PayPalService paypal = new PayPalService();// 使用适配器将支付服务适配为统一的支付网关接口PaymentGateway alipayAdapter = new AlipayAdapter(alipay);PaymentGateway paypalAdapter = new PayPalAdapter(paypal);// 处理支付请求alipayAdapter.processPayment(100); // 输出: Processing payment of 100.0 via Alipay.paypalAdapter.processPayment(200); // 输出: Processing payment of 200.0 via PayPal.}
}
通过使用适配器模式,我们能够有效地解决因接口不一致而导致的问题,使得不同的支付服务可以无缝地集成到我们的电子商务平台中。这种方式不仅简化了系统的结构,降低了维护成本,还提高了系统的灵活性和可扩展性。当我们需要添加新的支付服务时,只需要为该服务创建一个新的适配器即可,而无需改动现有代码。
5.效果
Adapter模式适用于以下情况:
- 想使用一个已经存在的类,而它的接口不符合要求。
- 想创建一个可以服用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
- (仅适用于对象Adapter)想使用一个已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。
(二)桥接模式
1. 模式名称
桥接模式(Bridge Pattern)
2. 意图解决的问题
桥接模式主要用于解决系统中由于抽象和实现之间的强耦合而导致的复杂性和僵化问题。具体来说,它试图解决以下问题:
- 避免庞大的继承结构:在某些情况下,为了适应不同维度的变化,可能需要创建大量的子类。例如,如果一个系统既有不同的设备类型,又有不同的操作系统版本,那么传统的做法可能会导致需要为每种组合创建一个具体的子类。这会导致继承层次迅速膨胀。
- 分离接口与实现:允许抽象部分和其实现部分可以独立地变化。通过将接口和实现分离,可以使它们各自独立地扩展,而不会相互影响。
- 增强系统的灵活性:通过动态地关联不同的实现对象,可以在运行时改变实现的方式,从而提供更大的灵活性。
3. 模式描述
桥接模式的核心思想是将抽象部分与其实现部分分离,使它们可以独立变化。它涉及到四个主要角色:
- Abstraction(抽象类):定义了对用户公开的高级操作,通常包含了一个对Implementor(实现者)类型的引用。
- RefinedAbstraction(扩充抽象类):扩展了Abstraction定义的功能,提供了更具体的业务逻辑,并通过其构造函数或方法调用传入对应的Implementor实例。
- Implementor(实现者接口):定义了实现部分的基本操作,但并不强制要求所有实现都必须使用这些操作。它是被Abstraction使用的接口。
- ConcreteImplementor(具体实现者):实现了Implementor接口,提供了特定的具体实现。
(1)示例代码
// Implementor接口
public interface Implementor {void operationImpl();
}// ConcreteImplementorA 和 ConcreteImplementorB 是 Implementor 的具体实现
public class ConcreteImplementorA implements Implementor {@Overridepublic void operationImpl() {System.out.println("ConcreteImplementorA operation");}
}public class ConcreteImplementorB implements Implementor {@Overridepublic void operationImpl() {System.out.println("ConcreteImplementorB operation");}
}// Abstraction 抽象类
public abstract class Abstraction {protected Implementor implementor;public Abstraction(Implementor implementor) {this.implementor = implementor;}public abstract void operation();
}// RefinedAbstraction 扩充抽象类
public class RefinedAbstraction extends Abstraction {public RefinedAbstraction(Implementor implementor) {super(implementor);}@Overridepublic void operation() {System.out.print("RefinedAbstraction operation invokes ");implementor.operationImpl();}
}// 使用示例
public class Main {public static void main(String[] args) {Implementor implA = new ConcreteImplementorA();Implementor implB = new ConcreteImplementorB();Abstraction abstractionA = new RefinedAbstraction(implA);abstractionA.operation(); // 输出: RefinedAbstraction operation invokes ConcreteImplementorA operationAbstraction abstractionB = new RefinedAbstraction(implB);abstractionB.operation(); // 输出: RefinedAbstraction operation invokes ConcreteImplementorB operation}
}
4.应用场景
(1)场景描述
假设你正在开发一个图形渲染库,这个库可以支持多种形状(如圆形、矩形等)的绘制,并且需要在多个平台上运行,比如 Windows、Linux 和 macOS。每种形状都可以使用不同的渲染方式,例如抗锯齿和非抗锯齿渲染。
(2)遇到的困难:
类爆炸(组合爆炸):
如果不采用桥接模式,传统的做法可能是通过多重继承的方式来实现功能扩展。例如:
- 创建一个 Shape 抽象类。
- 然后为每一种具体的形状(如 Circle、Rectangle)创建子类。
- 同时,针对每个平台的具体渲染方式,再为每一个形状创建一个平台相关的子类(如 WindowsCircle、LinuxCircle、WindowsRectangle、LinuxRectangle 等)。
这样会导致类的数量随着形状和平台的增加而呈指数级增长,难以维护。如果增加一个新的形状或新的平台,则需要大量新增代码并修改已有逻辑。
耦合度高:
形状与平台实现之间紧密耦合,使得修改或扩展变得复杂。例如,如果要添加一个新的渲染特性(如抗锯齿),则可能需要修改所有相关平台的实现。
灵活性差:
由于代码结构僵化,无法在运行时动态切换平台或其他渲染特性。
(3)Bridge 模式如何解决这个问题
桥接模式将抽象部分(如形状)和其实现部分(如平台渲染方式)分离,使它们可以独立变化。这样就可以避免类数量的指数级增长,并提高系统的灵活性和可扩展性。
定义实现者接口(Implementor):
这个接口用于封装底层平台的渲染能力,例如不同操作系统的绘图 API。
public interface Renderer {void renderCircle(float radius, float x, float y);
}
实现具体实现者(Concrete Implementors):
每个平台对应一个实现者,实现具体的渲染细节。
// Windows 渲染器
public class WindowsRenderer implements Renderer {@Overridepublic void renderCircle(float radius, float x, float y) {System.out.println("Render circle with Windows renderer: radius=" + radius + " at (" + x + "," + y + ")");}
}// Linux 渲染器
public class LinuxRenderer implements Renderer {@Overridepublic void renderCircle(float radius, float x, float y) {System.out.println("Render circle with Linux renderer: radius=" + radius + " at (" + x + "," + y + ")");}
}
定义抽象类(Abstraction):
定义上层逻辑(如 Shape 的通用行为),并通过引用指向 Renderer 实现者。
public abstract class Shape {protected Renderer renderer;public Shape(Renderer renderer) {this.renderer = renderer;}public abstract void draw();
}
扩展抽象类(Refined Abstractions):
具体的形状(如圆形、矩形)作为抽象类的子类,依赖于 Renderer 接口完成实际的绘制。
public class Circle extends Shape {private float radius;private float x;private float y;public Circle(Renderer renderer, float radius, float x, float y) {super(renderer);this.radius = radius;this.x = x;this.y = y;}@Overridepublic void draw() {renderer.renderCircle(radius, x, y);}
}
客户端调用:
public class Main {public static void main(String[] args) {// 创建 Windows 渲染器Renderer windowsRenderer = new WindowsRenderer();// 创建 Linux 渲染器Renderer linuxRenderer = new LinuxRenderer();// 使用 Windows 渲染器绘制一个圆Shape circleOnWindows = new Circle(windowsRenderer, 5.0f, 10.0f, 10.0f);circleOnWindows.draw(); // 输出: Render circle with Windows renderer// 使用 Linux 渲染器绘制一个圆Shape circleOnLinux = new Circle(linuxRenderer, 7.0f, 15.0f, 15.0f);circleOnLinux.draw(); // 输出: Render circle with Linux renderer}
}
5.效果
桥接模式通过将抽象部分和实现部分分离,解决了传统继承方式导致的类爆炸问题,提高了系统的可扩展性和灵活性。它特别适用于那些需要多维度组合变化的场景,例如跨平台软件开发、数据库连接驱动适配、图形界面框架设计等领域。
Bridge 模式带来的优势:
- 解耦抽象与实现:
- 形状(抽象部分)和渲染方式(实现部分)完全解耦,可以分别独立扩展。
- 添加新的形状或新的渲染方式仅需新增类,无需修改原有类。
- 避免类爆炸:
- 不需要为每个形状和每个平台的组合都创建一个单独的类。
- 每个维度只需创建有限的类,通过组合的方式完成各种情况。
- 运行时灵活切换:
- 可以在运行时动态地改变对象使用的实现方式。例如,一个绘图工具可以在运行时切换不同的渲染引擎。
- 符合开闭原则:
- 对扩展开放:新增形状或渲染方式时,不需要修改已有的代码。
- 对修改关闭:不会因为新增功能而影响现有逻辑。
Bridge模式适用于以下情况:
- 不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如,这种情况可能是因为,在程序运行时刻实现部分应可以被选择或者切换。
- 类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这是Bridge模式使得开发者可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。
- 对一个抽象的实现部分的修改应对客户不产生影响,即客户代码不必重新编译。
- (C++)想对客户完全隐藏抽象的实现部分。
- 有许多类要生成的类层次结构。
- 想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点。
(三)Composite组合模式
1. 模式名称
组合模式(Composite Pattern)
2. 意图解决的问题
组合模式主要用于处理树形结构的数据,使得用户可以一致地对待单个对象和对象组合。具体来说,它试图解决以下问题:
- 复杂树形结构的管理:在处理具有层次结构的对象集合时(如文件系统中的目录和文件),需要一种方法来简化这些结构的操作,例如遍历、添加或删除节点。
- 统一接口的需求:希望提供一种方式,让客户端代码能够一致地处理简单元素(如单个文件)和复杂元素(如文件夹,其中可能包含其他文件或文件夹),而无需关心它们之间的区别。
- 增强系统的可扩展性:通过定义一个递归的结构,使得新的组件类型可以很容易地加入到系统中,而不影响现有的代码。
3. 模式描述
组合模式允许你将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得客户端对单个对象和组合对象的使用具有一致性。它涉及到三个主要角色:
- Component(构件):这是组合中的抽象基类,声明了所有公共的方法,包括操作自身的方法以及操作子部件的方法(如add、remove和getChild等)。这些方法通常会有默认实现,但在叶子节点上可能会有不同的实现。
- Leaf(叶子):表示组合中的叶节点对象,叶节点没有子节点。它实现了在Component中定义的行为,但通常会把那些涉及子节点的操作置为空或者抛出异常,因为叶子节点不包含子节点。
- Composite(组合):定义了有子部件的那些行为,并存储其子部件。除了在Component中定义的方法外,还实现了与子部件相关的操作,比如添加、删除或访问子部件。
(1)示例代码
// Component 抽象类
public abstract class Component {protected String name;public Component(String name) {this.name = name;}public abstract void add(Component c);public abstract void remove(Component c);public abstract void display(int depth);// 其他通用方法...
}// Leaf 类
public class Leaf extends Component {public Leaf(String name) {super(name);}@Overridepublic void add(Component c) {System.out.println("Cannot add to a leaf");}@Overridepublic void remove(Component c) {System.out.println("Cannot remove from a leaf");}@Overridepublic void display(int depth) {for (int i = 0; i < depth; i++) System.out.print("-");System.out.println(name);}
}// Composite 类
public class Composite extends Component {private List<Component> children = new ArrayList<>();public Composite(String name) {super(name);}@Overridepublic void add(Component component) {children.add(component);}@Overridepublic void remove(Component component) {children.remove(component);}@Overridepublic void display(int depth) {for (int i = 0; i < depth; i++) System.out.print("-");System.out.println(name);for (Component component : children) {component.display(depth + 2);}}
}// 使用示例
public class Main {public static void main(String[] args) {Composite root = new Composite("root");root.add(new Leaf("Leaf A"));Composite comp = new Composite("Composite X");comp.add(new Leaf("Leaf XA"));comp.add(new Leaf("Leaf XB"));root.add(comp);root.add(new Leaf("Leaf B"));root.display(1);}
}
4.应用场景
(1)场景描述
设想你需要设计一个文件管理系统,该系统需要支持对文件和目录的操作。用户可以创建、删除、重命名文件或目录,并且能够查看某个目录下的所有内容(包括文件和其他子目录)。此外,还需要提供功能来计算某个目录及其所有子目录的总大小。
(2)遇到的困难
处理不同类型的对象:
在这个场景中,你有两类主要的对象:文件和目录。目录可以包含其他文件和目录,而文件则是最底层的单元,不能再包含其他对象。这意味着你需要处理两种不同类型的对象,但又希望以统一的方式来操作它们。
递归结构的复杂性:
目录可以嵌套多个层级,形成一个复杂的树形结构。例如,一个目录可能包含多个子目录,每个子目录又可能包含更多的文件和子目录。在这种情况下,遍历整个目录结构并执行操作(如计算总大小)变得非常复杂。
代码重复与维护难题:
如果直接针对文件和目录分别编写操作逻辑,会导致大量相似的代码,增加维护成本。比如,如果要添加一个新的操作(如复制),就需要为文件和目录分别实现这一操作,增加了工作量和出错的可能性。
(3)Composite模式如何解决这个问题:
Composite模式通过将对象组织成树形结构,使得客户端可以一致地对待单个对象(叶子节点)和对象组合(复合节点),从而解决了上述问题。
具体实现步骤:
定义Component接口:
创建一个抽象类或接口Component,它声明了所有组件(文件和目录)共有的方法,如getName()、getSize()等。同时,还应包括一些操作子组件的方法,如add(Component component)、remove(Component component)和getChild(int index)。
public abstract class Component {protected String name;public Component(String name) {this.name = name;}public abstract void add(Component component);public abstract void remove(Component component);public abstract Component getChild(int index);public abstract int getSize(); // 计算大小的方法public abstract void display();
}
实现Leaf类:
Leaf类代表树中的叶子节点,即那些没有子节点的对象,在此场景下对应的是文件。
public class File extends Component {private int size;public File(String name, int size) {super(name);this.size = size;}@Overridepublic void add(Component component) {System.out.println("Cannot add to a file");}@Overridepublic void remove(Component component) {System.out.println("Cannot remove from a file");}@Overridepublic Component getChild(int index) {return null;}@Overridepublic int getSize() {return size;}@Overridepublic void display() {System.out.println(name + " (" + size + " bytes)");}
}
实现Composite类:
Composite类代表那些包含子节点的对象,在此场景下对应的是目录。它实现了Component中定义的所有方法,并提供了管理子组件的功能。
import java.util.ArrayList;
import java.util.List;public class Directory extends Component {private List<Component> children = new ArrayList<>();public Directory(String name) {super(name);}@Overridepublic void add(Component component) {children.add(component);}@Overridepublic void remove(Component component) {children.remove(component);}@Overridepublic Component getChild(int index) {return children.get(index);}@Overridepublic int getSize() {int totalSize = 0;for (Component child : children) {totalSize += child.getSize();}return totalSize;}@Overridepublic void display() {System.out.println(name);for (Component child : children) {for (int i = 0; i < 2; i++) System.out.print(" "); // 缩进显示child.display();}}
}
客户端使用:
public class Main {public static void main(String[] args) {Directory root = new Directory("root");Directory folderA = new Directory("Folder A");Directory folderB = new Directory("Folder B");File file1 = new File("File 1", 100);File file2 = new File("File 2", 200);File file3 = new File("File 3", 300);folderA.add(file1);folderB.add(file2);folderB.add(file3);root.add(folderA);root.add(folderB);root.display(); // 显示整个目录结构System.out.println("Total Size: " + root.getSize() + " bytes"); // 计算总大小}
}
解决方案带来的优势:
- 统一的接口:无论是文件还是目录,都可以通过相同的接口进行操作,极大地简化了客户端代码。
- 易于扩展:如果将来需要添加新的类型(如符号链接),只需创建一个新的类实现Component接口即可,无需修改现有代码。
- 简化递归操作:通过递归调用getSize()等方法,轻松实现了对整个目录树的操作,而不需要手动处理每个层级的细节。
综上所述,Composite模式非常适合用于处理具有层次结构的数据集合,它不仅简化了代码,提高了可读性和可维护性,同时也增强了系统的灵活性和可扩展性。在本例中,它帮助我们构建了一个简单但功能强大的文件管理系统。
5.效果
Composite模式适用于:
- 想表示对象的部分—整体层次结构。
- 希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
(四)Decorator 模式
1. 模式名称
装饰者模式(Decorator Pattern)
2. 意图解决的问题
装饰者模式主要用于在不改变对象自身的基础上,在运行时为对象动态地添加职责。它提供了一种比继承更灵活的解决方案来扩展对象的功能,特别是在需要频繁修改或增加功能的情况下。
具体来说,装饰者模式试图解决以下问题:
- 静态继承的局限性:传统的通过继承来扩展功能的方式缺乏灵活性,一旦类层次结构确定下来,新增加的行为就需要创建新的子类,这会导致类的数量迅速膨胀。
- 代码复用与灵活性的需求:希望能够在不影响现有对象结构的前提下,灵活地给某个对象而不是整个类添加一些临时性的功能或行为。
- 避免“胖接口”问题:有时候一个类可能因为要支持多种功能而变得非常庞大,导致接口变得复杂且难以维护。装饰者模式可以帮助分解这些功能,使得每个装饰器只关注于特定的行为增强。
3. 模式描述
装饰者模式的核心思想是使用组合而非继承来实现功能的动态添加。它涉及到四个主要角色:
- Component(组件):定义了被装饰的对象的接口,可以是一个具体的类或者抽象类。
- ConcreteComponent(具体组件):实现了Component接口,提供了基本的行为,但不一定提供所有的装饰行为。
- Decorator(装饰者):持有一个对Component类型的引用,并定义了一个与Component接口一致的接口。这样,装饰者可以替代其所装饰的对象。
- ConcreteDecorator(具体装饰者):实现了Decorator定义的接口,并在调用被装饰对象的方法之前或之后添加额外的行为。
(1)示例代码
// 定义Component接口
public interface Component {void Operation();
}
// 实现ConcreteComponent类
public class ConcreteComponent implements Component {@Overridepublic void Operation() {System.out.println("执行具体组件的操作");}
}
//定义Decorator抽象类
public abstract class Decorator implements Component {protected Component component;public Decorator(Component component) {this.component = component;}@Overridepublic void Operation() {if (component != null) {component.Operation();}}
}
//实现ConcreteDecoratorA类
public class ConcreteDecoratorA extends Decorator {private String addedState;public ConcreteDecoratorA(Component component) {super(component);// 初始化addedStateaddedState = "新增状态A";}@Overridepublic void Operation() {super.Operation();addedBehavior();}private void addedBehavior() {System.out.println("添加行为A:" + addedState);}
}
// 实现ConcreteDecoratorB类
public class ConcreteDecoratorB extends Decorator {public ConcreteDecoratorB(Component component) {super(component);}@Overridepublic void Operation() {super.Operation();addedBehavior();}private void addedBehavior() {System.out.println("添加行为B");}
}
// 客户端调用
public class Main {public static void main(String[] args) {Component component = new ConcreteComponent();component = new ConcreteDecoratorA(component);component = new ConcreteDecoratorB(component);component.Operation();}
}
这段代码实现了装饰器模式的基本结构。Component 接口定义了基本操作 Operation(),ConcreteComponent 是具体的组件实现,Decorator 是装饰器的基类,而 ConcreteDecoratorA 和 ConcreteDecoratorB 则是具体的装饰器,它们在调用 Operation() 方法时会添加额外的行为。最后,在 Main 类中展示了如何使用这些类来动态地为对象添加功能。
4.应用场景
(1)场景背景:
设想你正在开发一个咖啡店的订单管理系统,该系统需要支持多种类型的咖啡饮品(如浓缩咖啡、拿铁等),并且顾客可以为他们的咖啡选择不同的调料(如牛奶、摩卡、奶泡等)。每种咖啡和调料都有自己的价格,而且同一种咖啡可以选择添加多个不同种类的调料。
(2)遇到的困难:
静态继承导致的类爆炸:
如果采用传统的继承方式来实现这个需求,你需要为每一种可能的咖啡-调料组合创建一个新的子类。例如,如果有一种咖啡和三种调料,则至少需要七个子类(一种原味咖啡 + 三种单调料咖啡 + 三种双调料咖啡 + 一种三调料咖啡)。随着咖啡种类和调料选项的增加,这种方案会导致类的数量呈指数级增长,难以维护。
功能扩展复杂:
当引入新的咖啡种类或调料时,必须修改现有代码以适应这些变化。这不仅增加了工作量,还违背了开闭原则(对扩展开放,对修改关闭)。
灵活性不足:
不同顾客可能会有不同的口味偏好,这意味着同一杯咖啡可以根据个人喜好添加不同的调料。使用静态继承的方法很难动态地根据用户的选择调整咖啡的配置。
(4)Decorator模式如何解决这个问题:
装饰者模式通过在运行时动态地为对象添加行为,而不是通过继承来扩展功能,解决了上述问题。它允许我们为每个咖啡类型定义一个基本的价格和描述,并通过装饰器为这些基础咖啡动态添加调料,而不需要为每种可能的组合创建单独的类。
//定义一个Beverage接口,表示所有饮料的基础接口,包括获取描述和计算价格的方法。
public abstract class Beverage {public abstract String getDescription();public abstract double cost();
}
// 实现具体的咖啡类型,比如浓缩咖啡(Espresso)。
public class Espresso extends Beverage {@Overridepublic String getDescription() {return "Espresso";}@Overridepublic double cost() {return 1.99;}
}
// 创建一个抽象装饰者类CondimentDecorator,它同样实现了Beverage接口,并持有一个对Beverage的引用。
public abstract class CondimentDecorator extends Beverage {protected Beverage beverage;public CondimentDecorator(Beverage beverage) {this.beverage = beverage;}
}
// 根据实际需要创建具体的调料装饰器,比如Mocha(摩卡)、Whip(奶泡)等。
public class Mocha extends CondimentDecorator {public Mocha(Beverage beverage) {super(beverage);}@Overridepublic String getDescription() {return beverage.getDescription() + ", Mocha";}@Overridepublic double cost() {return 0.20 + beverage.cost(); // 添加Mocha的成本}
}public class Whip extends CondimentDecorator {public Whip(Beverage beverage) {super(beverage);}@Overridepublic String getDescription() {return beverage.getDescription() + ", Whip";}@Overridepublic double cost() {return 0.10 + beverage.cost(); // 添加Whip的成本}
}
// 客户端使用
public class Main {public static void main(String[] args) {Beverage beverage = new Espresso();System.out.println(beverage.getDescription() + " $" + beverage.cost());beverage = new Mocha(beverage);System.out.println(beverage.getDescription() + " $" + beverage.cost());beverage = new Whip(beverage);System.out.println(beverage.getDescription() + " $" + beverage.cost());}
}
解决方案带来的优势:
- 避免类爆炸:通过使用装饰者模式,无需为每种咖啡-调料组合创建单独的类。只需定义基本的咖啡类型和调料装饰器即可。
- 增强灵活性:可以在运行时动态地为咖啡添加调料,满足不同客户的需求。同时,新增加的咖啡种类或调料只需要实现相应的组件或装饰器类,不会影响现有代码。
- 遵循开闭原则:当需要扩展新功能时(如添加新的咖啡或调料),可以通过添加新的类来完成,而不需要修改现有的代码。
综上所述,装饰者模式非常适合用于需要动态地、透明地给某个对象添加职责的场景。在这个咖啡订单系统的例子中,它帮助我们构建了一个灵活且易于扩展的系统,能够有效地处理复杂的咖啡与调料组合需求。
5.效果
Decorator模式适用于以下情况:
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
处理那些可以撤销的职责。
当不能采用生成子类的方式进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是,由于类定义被隐藏,或类定义不能用于生成子类。
(五)Facade(外观)
1. 模式名称
外观模式(Facade Pattern)
2. 意图解决的问题
外观模式旨在为复杂的子系统提供一个统一的高层接口,使这个子系统的使用更加简单。它特别适用于以下情况:
- 简化复杂系统的使用:当有一个或多个复杂的子系统时,客户代码需要与这些子系统的众多接口交互,这增加了使用的复杂度和难度。
- 提高子系统的独立性:通过引入外观类来作为与子系统进行沟通的中介,降低了客户代码与子系统之间的耦合度,使得子系统内部的变化对客户端的影响降到最低。
- 促进分层设计:在具有多层次结构的应用中,外观模式可以用来定义每一层的入口点,这样一层内的组件就可以只与该层的外观进行交互,而不需要直接访问其他层次的组件。
3. 模式描述
外观模式的核心在于提供了一个单一的接口来简化对一个或多个子系统的访问。它涉及到三个主要角色:
- Facade(外观角色):为调用方提供了一组简化的接口,隐藏了复杂的实现细节。外观类通常负责将调用者的请求转发给适当的子系统对象,并处理结果返回给调用者。
- SubSystem classes(子系统类):实现了子系统的功能,但每个子系统都是独立工作的,不知道有Facade的存在。子系统之间也可能相互调用。
- Client(客户端):通过外观类提供的简化接口来访问子系统中的功能,无需了解子系统的具体实现细节。
(1)示例代码
// 子系统类A
public class SubSystemOne {public void methodOne() {System.out.println("子系统方法一");}
}// 子系统类B
public class SubSystemTwo {public void methodTwo() {System.out.println("子系统方法二");}
}// 子系统类C
public class SubSystemThree {public void methodThree() {System.out.println("子系统方法三");}
}// 外观类
public class Facade {private SubSystemOne subSystemOne;private SubSystemTwo subSystemTwo;private SubSystemThree subSystemThree;public Facade() {subSystemOne = new SubSystemOne();subSystemTwo = new SubSystemTwo();subSystemThree = new SubSystemThree();}public void operation() {System.out.println("外观模式统一接口");subSystemOne.methodOne();subSystemTwo.methodTwo();subSystemThree.methodThree();}
}// 客户端代码
public class Client {public static void main(String[] args) {Facade facade = new Facade();facade.operation(); // 通过外观类来访问子系统的功能}
}
在这个例子中,Facade 类提供了 operation() 方法,它封装了对 SubSystemOne、SubSystemTwo 和 SubSystemThree 的调用逻辑。客户端通过调用 Facade 的 operation() 方法即可完成原本需要分别调用各个子系统方法才能完成的操作,从而大大简化了客户端的代码。
4.应用场景
(1)场景背景:
设想你正在设计一个家庭影院系统,该系统包括多个子系统或组件,如DVD播放器、投影仪、音响系统和屏幕等。为了观看电影,用户需要依次执行一系列操作,比如打开投影仪、调整音响音量、启动DVD播放器并播放影片。
(2)遇到的困难:
复杂性高:
每个组件都有自己的接口和操作方式,用户需要记住并正确执行每个步骤,才能享受完整的观影体验。对于普通用户来说,这可能过于复杂且容易出错。
使用不便:
如果用户想要快速开始看电影,他们不得不分别操作各个设备,这不仅耗时而且降低了用户体验。
难以维护:
如果任何一个组件的接口发生改变(例如,新的投影仪型号引入了不同的控制方法),则所有依赖于该组件的操作流程都需要相应调整,增加了维护成本。
(3)Facade模式如何解决这个问题:
Facade模式通过提供一个统一的高层接口来简化对一个或多个子系统的访问,解决了上述问题。在这个例子中,可以创建一个“家庭影院外观”类,它将所有必要的操作封装在一个简单的方法中,用户只需调用这个方法即可完成整个观影准备过程。
定义子系统类:
为每个子系统定义具体的类,这些类代表家庭影院中的不同组件,例如DVD播放器、投影仪、音响系统等。
public class DVDPlayer {public void on() { System.out.println("打开DVD播放器"); }public void play(String movie) { System.out.println("播放电影:" + movie); }
}public class Projector {public void on() { System.out.println("打开投影仪"); }public void wideScreenMode() { System.out.println("设置宽屏模式"); }
}public class SoundSystem {public void on() { System.out.println("打开音响系统"); }public void setVolume(int volume) { System.out.println("设置音量:" + volume); }
}
创建Facade类:
创建一个HomeTheaterFacade类,它包含了所有子系统的实例,并提供了简化版的方法来完成复杂的操作序列。
public class HomeTheaterFacade {private DVDPlayer dvdPlayer;private Projector projector;private SoundSystem soundSystem;public HomeTheaterFacade(DVDPlayer dvdPlayer, Projector projector, SoundSystem soundSystem) {this.dvdPlayer = dvdPlayer;this.projector = projector;this.soundSystem = soundSystem;}public void watchMovie(String movie) {System.out.println("准备观看电影...");projector.on();projector.wideScreenMode();soundSystem.on();soundSystem.setVolume(10);dvdPlayer.on();dvdPlayer.play(movie);}public void endMovie() {System.out.println("结束电影...");// 关闭所有设备}
}
客户端代码:
public class Client {public static void main(String[] args) {DVDPlayer dvdPlayer = new DVDPlayer();Projector projector = new Projector();SoundSystem soundSystem = new SoundSystem();HomeTheaterFacade homeTheaterFacade = new HomeTheaterFacade(dvdPlayer, projector, soundSystem);homeTheaterFacade.watchMovie("星球大战");}
}
解决方案带来的优势:
- 简化用户交互:通过HomeTheaterFacade提供的简化接口,用户无需了解各个子系统的具体操作细节,只需调用一个方法即可完成观影准备。
- 降低耦合度:外观模式减少了客户端与子系统之间的直接依赖关系,使得子系统的内部变化不会影响到客户端代码,提高了系统的可维护性和灵活性。
- 促进模块化设计:每个子系统都可以独立开发和测试,而不需要考虑其他子系统的存在。这样不仅简化了开发过程,还增强了系统的可扩展性。
综上所述,Facade模式非常适合用于简化复杂的系统结构,特别是在需要隐藏系统内部复杂性以提高易用性的场合。在本例的家庭影院系统中,它帮助我们构建了一个既功能丰富又易于使用的解决方案。
5.效果
Facade模式适用于以下情况:
要为一个复杂子系统提供一个简单接口时,子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类,这使得子系统更具有可重用性,也更容易对子系统进行定制,但也给那些不需要定制子系统的用户带来一些使用上的困难。Facade可以提供一个简单的默认视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过Facade层。
客户程序与抽象类的实现部分之间存在着很大的依赖性。引入Facade将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
当需要构建一个层次结构的子系统时,使用Facade模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,则可以让它们仅通过Facade进行通信,从而简化了它们之间的依赖关系。
(六)Flyweight(享元)
1. 模式名称
享元模式(Flyweight Pattern)
2. 意图解决的问题
享元模式主要用于减少创建对象的数量,以减少内存占用和提高性能。它通过共享多个类似对象的公共状态来实现这一点,从而避免因大量细粒度对象而造成的资源浪费。
具体来说,享元模式适用于以下场景:
- 大量相似对象:当一个应用程序使用了大量的相似对象时,这些对象可能共享相同的属性或状态。
- 对象创建成本高:如果对象的创建和销毁非常耗费资源,那么通过共享对象可以显著降低系统资源的消耗。
- 需要高效利用内存:在处理大规模数据集或运行于资源受限环境中的应用中,有效管理内存是至关重要的。享元模式可以帮助减少内存占用,使得程序更加高效。
3. 模式描述
享元模式的核心在于区分对象的内部状态和外部状态,并将它们分开处理。内部状态是可以共享的部分,通常不会随环境变化;而外部状态则依赖于具体的上下文,不能被共享。该模式涉及以下几个关键角色:
- Flyweight(抽象享元类):定义了所有具体享元类共有的方法,并声明了一个接口用于操作内部状态。
- ConcreteFlyweight(具体享元类):实现了抽象享元类的方法,存储了可共享的内部状态。每个具体享元对象都代表一种特定的状态组合。
- UnsharedConcreteFlyweight(非共享具体享元类):并非所有对象都需要被共享,有时某些对象可能包含无法或不适合共享的数据,这类对象即为非共享具体享元类。
- FlyweightFactory(享元工厂类):负责创建和管理享元对象,确保相同状态的对象被复用而不是重复创建。它维护着一个享元池,用于存储已创建的享元实例。
(1)示例代码
//定义抽象享元类(Flyweight)
public interface Flyweight {void operation(String extrinsicState);
}
// 实现具体享元类(ConcreteFlyweight)
public class ConcreteFlyweight implements Flyweight {private String intrinsicState;public ConcreteFlyweight(String intrinsicState) {this.intrinsicState = intrinsicState;}@Overridepublic void operation(String extrinsicState) {System.out.println("Intrinsic State: " + intrinsicState + ", Extrinsic State: " + extrinsicState);}
}
// 实现非共享具体享元类(UnsharedConcreteFlyweight)
public class UnsharedConcreteFlyweight implements Flyweight {private String allState;public UnsharedConcreteFlyweight(String allState) {this.allState = allState;}@Overridepublic void operation(String extrinsicState) {System.out.println("All State: " + allState + ", Extrinsic State: " + extrinsicState);}
}
// 创建享元工厂类(FlyweightFactory)
public class FlyweightFactory {private Map<String, Flyweight> flyweights = new HashMap<>();public Flyweight getFlyweight(String key) {if (flyweights.containsKey(key)) {return flyweights.get(key);} else {Flyweight flyweight = new ConcreteFlyweight(key);flyweights.put(key, flyweight);return flyweight;}}
}
// 客户端代码(Client)
public class Client {public static void main(String[] args) {FlyweightFactory factory = new FlyweightFactory();// 使用享元对象Flyweight flyweightA = factory.getFlyweight("A");flyweightA.operation("Extrinsic State A");Flyweight flyweightB = factory.getFlyweight("B");flyweightB.operation("Extrinsic State B");// 再次获取 'A' 字符,应该返回同一个实例Flyweight flyweightA2 = factory.getFlyweight("A");flyweightA2.operation("Extrinsic State A2");}
}
思考过程:
- 定义接口:首先定义了 Flyweight 接口,它包含一个 operation 方法,用于处理外部状态。
- 实现具体享元类:创建了 ConcreteFlyweight 类,实现了 Flyweight 接口,并添加了内部状态 intrinsicState。
- 实现非共享具体享元类:创建了 UnsharedConcreteFlyweight 类,同样实现了 Flyweight 接口,但包含了所有状态 allState。
- 创建享元工厂:设计了 FlyweightFactory 类,用于管理享元对象的创建和复用。通过检查键值是否存在来决定是返回现有对象还是创建新对象。
- 客户端使用:在 Client 类中演示了如何使用享元工厂获取享元对象并调用其方法。
以上代码完整地实现了享元模式,并展示了如何通过享元工厂管理和复用对象以节省内存资源。
4.应用场景
(1)场景背景:
设想你正在开发一个文本编辑器,该编辑器需要处理大量的文本数据。在大多数情况下,文档中的许多字符是相同的(如空格、字母和数字等)。如果为每个字符都创建一个新的对象实例,将会消耗大量的内存资源,尤其是在处理大规模文档时。
(2)遇到的困难:
内存占用大:
如果对文档中的每一个字符都创建独立的对象实例,那么对于长文档来说,这将导致非常高的内存使用率,因为即使是重复出现的字符也会各自占用一份内存空间。
性能问题:
创建大量对象不仅会增加内存负担,还会拖慢程序的运行速度,特别是在初始化或加载大型文档时表现尤为明显。
难以维护:
当文档内容发生变化时,如果需要频繁地创建和销毁对象,这将进一步加剧性能瓶颈,并且增加了代码的复杂性和维护成本。
(3)Flyweight模式如何解决这个问题:
Flyweight模式通过共享具有相同内部状态的对象来减少内存占用,解决了上述问题。它将对象的状态分为内部状态(可以共享的部分)和外部状态(依赖于具体上下文的部分)。在这个例子中,字符本身(如’A’、'B’等)被认为是内部状态,而它们的位置或格式(如字体大小、颜色等)则是外部状态。
// 定义一个 Character 接口作为所有字符的基类,声明了一个方法用于操作字符。
public interface Character {void display(Context context);
}
// 实现具体的字符类 ConcreteCharacter,它实现了 Character 接口并持有字符本身的内部状态。
public class ConcreteCharacter implements Character {private char character;public ConcreteCharacter(char character) {this.character = character;}@Overridepublic void display(Context context) {System.out.println("字符: " + character + ", 字号: " + context.getFontSize() + ", 颜色: " + context.getFontColor());}
}
// 创建一个 Context 类来保存字符的外部状态,如字号和颜色。
public class Context {private String fontSize;private String fontColor;public Context(String fontSize, String fontColor) {this.fontSize = fontSize;this.fontColor = fontColor;}public String getFontSize() { return fontSize; }public String getFontColor() { return fontColor; }
}
// 设计一个 CharacterFactory 工厂类,用于创建和管理享元对象,确保相同字符只创建一次,并在后续请求时复用已有的实例。
public class CharacterFactory {private static HashMap<char, Character> characters = new HashMap<>();public static Character getCharacter(char key) {Character character = characters.get(key);if (character == null) {character = new ConcreteCharacter(key);characters.put(key, character);}return character;}
}
// 客户端调用:
public class Main {public static void main(String[] args) {Context context1 = new Context("12px", "red");Context context2 = new Context("14px", "blue");Character characterA = CharacterFactory.getCharacter('A');characterA.display(context1);Character characterB = CharacterFactory.getCharacter('B');characterB.display(context2);// 再次获取 'A' 字符,应该返回同一个实例Character characterA2 = CharacterFactory.getCharacter('A');characterA2.display(context2);}
}
5.效果
Flyweight模式适用于以下情况:
- 一个应用程序使用了大量的对象。
- 完全由于使用大量的对象,造成很大的存储开销。
- 对象的大多数状态都可变为外部状态。
- 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
- 应用程序不依赖于对象标识。由于Flyweight对象可以被共享,所以对于概念上明显有别的对象,标识测试将返回真值。
(七)Proxy代理模式
1. 模式名称
Proxy(代理)模式
2. 意图解决的问题
Proxy模式主要用于解决直接访问对象可能带来的复杂性或不必要的资源消耗问题。它通过提供一个替代对象(即代理对象),来控制对原对象的访问,从而可以在不改变原始对象的情况下添加额外的功能或者逻辑。具体应用场景包括但不限于:
- 延迟初始化:在创建开销较大的对象时,使用代理延迟实际对象的创建直到真正需要。
- 访问控制:为不同的用户提供不同级别的访问权限。
- 远程代理:代表远程服务器上的对象执行操作,隐藏网络通信的细节。
- 虚拟代理:用于表示大型对象,如图像等,以提高性能。
- 智能引用:提供对目标对象的额外操作,如计数访问次数、事务处理等。
3. 模式描述
Proxy模式是一种结构型设计模式,它为其他对象提供一种代理以控制对该对象的访问。在Proxy模式中,通常涉及三个主要角色:
- 抽象主题角色(Subject):声明了真实主题和代理主题的共同接口,这样任何使用真实主题的地方都可以使用代理主题而无需修改代码。
- 真实主题角色(Real Subject):定义了代理角色所代表的真实对象,并实现了抽象主题角色所声明的接口,是最终要引用的对象。
- 代理主题角色(Proxy):持有对真实主题角色的引用,并提供与真实主题角色相同的接口。除了将请求转发给真实主题之外,代理角色还可以在请求前后执行一些额外的操作,如权限检查、日志记录、延迟加载等。
通过这种模式,Proxy可以有效地管理客户端对真实主题的访问,既能够保护真实主题免受不必要的复杂性影响,又能为客户端提供更加灵活的服务。例如,在进行远程方法调用时,客户端可以直接调用本地的代理对象,而代理对象则负责处理所有的网络通信细节并将结果返回给客户端,从而简化了客户端的实现。
(1)示例代码
// 1. 抽象主题角色(Subject)
public interface Subject {void request();
}// 2. 真实主题角色(RealSubject)
public class RealSubject implements Subject {@Overridepublic void request() {System.out.println("真实主题请求");}
}// 3. 代理主题角色(Proxy)
public class Proxy implements Subject {private RealSubject realSubject;public Proxy(RealSubject realSubject) {this.realSubject = realSubject;}@Overridepublic void request() {// 在调用真实主题的request方法之前可以添加一些额外的操作System.out.println("代理前的操作");// 调用真实主题的方法if (realSubject != null) {realSubject.request();}// 在调用真实主题的request方法之后也可以添加一些额外的操作System.out.println("代理后的操作");}
}// 4. 客户端代码
public class Client {public static void main(String[] args) {// 创建真实主题对象RealSubject realSubject = new RealSubject();// 创建代理对象,并将真实主题对象传递给它Proxy proxy = new Proxy(realSubject);// 通过代理对象调用真实主题的方法proxy.request();}
}
思考过程:
- 抽象主题角色(Subject):定义了一个接口Subject,其中包含一个request()方法。这个接口是真实主题和代理主题共同遵守的规范。
- 真实主题角色(RealSubject):实现了Subject接口,提供了具体业务逻辑的实现。
- 代理主题角色(Proxy):也实现了Subject接口,并持有一个对真实主题的引用。在调用真实主题的request()方法前后,代理可以执行额外的操作。
- 客户端代码:创建了真实主题对象和代理对象,并通过代理对象来间接访问真实主题的功能。
解题过程:
- 首先定义了抽象主题角色Subject接口,确保真实主题和代理主题有相同的接口。
- 然后分别实现了真实主题RealSubject和代理主题Proxy。
- 最后在客户端代码中,通过代理对象来调用真实主题的方法,展示了代理模式的基本使用方式。
4.应用场景
(1)场景背景:
设想你正在开发一个支持远程文件访问的系统,用户可以通过客户端软件浏览、下载和上传存储在服务器上的文件。为了简化设计并提高性能,你希望客户端直接与本地代理进行交互,而不是每次都直接与远程服务器通信。
(2)遇到的困难:
网络延迟和带宽限制:
直接与远程服务器通信可能会导致较高的网络延迟,尤其是在处理大文件时,这不仅影响用户体验,还可能消耗大量的带宽资源。
安全性问题:
每次请求都直接到达服务器,增加了暴露敏感数据的风险。此外,还需要考虑如何对每个请求进行身份验证和授权。
复杂性增加:
如果客户端直接与服务器通信,则需要处理诸如错误重试、断点续传等复杂逻辑,增加了客户端的实现难度。
(3)Proxy模式如何解决这个问题:
通过引入代理对象(Proxy),可以在客户端与远程服务器之间建立一个中间层,使得客户端可以像操作本地文件一样轻松地访问远程文件,同时解决了上述问题。
//定义一个 FileAccess 接口作为所有文件访问操作的基础接口,包括打开、读取、写入和关闭文件等方法。
public interface FileAccess {void open(String path);byte[] read();void write(byte[] data);void close();
}
// 创建一个 RemoteFileAccess 类实现了 FileAccess 接口,并提供了实际与远程服务器通信的功能。
public class RemoteFileAccess implements FileAccess {@Overridepublic void open(String path) {System.out.println("打开远程路径: " + path);// 远程调用代码...}@Overridepublic byte[] read() {System.out.println("从远程读取数据");return new byte[0]; // 远程调用代码...}@Overridepublic void write(byte[] data) {System.out.println("向远程写入数据");// 远程调用代码...}@Overridepublic void close() {System.out.println("关闭远程连接");// 远程调用代码...}
}
// 实现一个 LocalFileProxy 代理类,它同样实现了 FileAccess 接口,并持有一个对 RemoteFileAccess 的引用。代理类可以在转发请求之前或之后执行额外的操作,如缓存数据、日志记录等。
public class LocalFileProxy implements FileAccess {private RemoteFileAccess remoteFileAccess;public LocalFileProxy(RemoteFileAccess remoteFileAccess) {this.remoteFileAccess = remoteFileAccess;}@Overridepublic void open(String path) {System.out.println("代理前的操作 - 检查权限...");remoteFileAccess.open(path);}@Overridepublic byte[] read() {System.out.println("代理前的操作 - 记录日志...");byte[] data = remoteFileAccess.read();System.out.println("代理后的操作 - 缓存数据...");return data;}@Overridepublic void write(byte[] data) {System.out.println("代理前的操作 - 校验数据...");remoteFileAccess.write(data);}@Overridepublic void close() {remoteFileAccess.close();System.out.println("代理后的操作 - 清理临时资源...");}
}
//客户端代码
public class Client {public static void main(String[] args) {RemoteFileAccess realSubject = new RemoteFileAccess();LocalFileProxy proxy = new LocalFileProxy(realSubject);proxy.open("/path/to/file");proxy.read();proxy.write(new byte[]{});proxy.close();}
}
解决方案带来的优势:
优化性能:
代理可以在转发请求前后执行一些优化措施,例如缓存数据以减少重复的网络请求,从而提高了整体性能。
增强安全性:
在代理中可以加入身份验证和授权机制,确保只有合法用户才能访问远程资源,保护了系统的安全。
简化客户端实现:
客户端只需要知道如何与代理交互即可,而不需要关心复杂的网络通信细节,降低了开发成本和维护难度。
5.效果
Proxy 模式适用于在需要比较通用和复杂的对象指针代替简单的指针的时候,常见情况有:
- 远程代理(Remote Proxy)为一个对象在不同地址空间提供据不代表。
- 虚代理(Virtual Proxy)根据需要创建开销很大的对象。
- 保护代理(Protection Proxy)控制对原始对象的访问,用于对象应该有不同的访问权限的时候。
- 只能指引(Smart Reference)取代了简单的指针,它在访问对象时执行一些附加操作。典型用途包括:对指向实际对象的引用计数,这样当该对象没有引用时,可以被自动释放;当第一次引用一个持久对象时,将它装入内存;在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。
相关文章:
系统架构设计师:设计模式——结构型设计模式
一、结构型设计模式 结构型设计模式涉及如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口或实现。一个简单的例子是采用多重继承方法将两个以上的类组合成一个类,结果这个类包含了所有父类的性质。 这一模式尤其有助于多个独立开发的类库协…...
接口测试实战指南:从入门到精通的质量保障之道
为什么接口测试如此重要? 在当今快速迭代的软件开发环境中,接口测试已成为质量保障体系中不可或缺的一环。据统计,有效的接口测试可以发现约70%的系统缺陷,同时能将测试效率提升3-5倍。本指南将从实战角度出发,系统性…...
对第三方软件开展安全测评,如何保障其安全使用?
对第三方软件开展安全测评,能够精准找出软件存在的各类安全隐患,进而为软件的安全使用给予保障。此次会从漏洞发现、风险评估、测试环境等多个方面进行具体说明。 漏洞发现情况 在测评过程中,我们借助专业技术与工具,对第三方软…...
计算方法实验四 解线性方程组的间接方法
【实验性质】 综合性实验。 【实验目的】 掌握迭代法求解线性方程组。 【实验内容】 应用雅可比迭代法和Gauss-Sediel迭代法求解下方程组: 【理论基础】 线性方程组的数值解法分直接算法和迭代算法。迭代法将方程组的求解转化为构造一个向量序列&…...
Qt 中基于 QTableView + QSqlTableModel 的分页搜索与数据管理实现
Qt 中基于 QTableView QSqlTableModel 的分页搜索与数据管理实现 一、组件说明 QTableView:一个基于模型的表格视图控件,支持排序、选择、委托自定义。QSqlTableModel:与数据库表直接绑定的模型类,可用于展示和编辑数据库表数据…...
云计算-容器云-服务网格Bookinfo
服务网格:创建 Ingress Gateway 将 Bookinfo 应用部署到 default 命名空间下,请为 Bookinfo 应用创建一个网 关,使外部可以访问 Bookinfo 应用。 上传ServiceMesh.tar.gz包 [rootk8s-master-node1 ~]# tar -zxvf ServiceMesh.tar.gz [rootk…...
PostgreSQL自定义函数
自定义函数 基本语法 //建一个名字为function_name的自定义函数create or replace function function_name() returns data_type as //returns 返回一个data_type数据类型的结果;data_type 是返回的字段的类型;$$ //固定写法......//方法体$$ LANGUAGE …...
学习记录:DAY22
我的重生开发之旅:优化DI容器,git提交规范,AOP处理器,锁与并发安全 前言 我重生了,重生到了五一开始的一天。上一世,我天天摆烂,最后惨遭实习生优化。这一世,我要好好内卷… 今天的…...
HarmonyOS NEXT第一课——HarmonyOS介绍
一、什么是HarmonyOS 万物互联时代应用开发的机遇、挑战和趋势 随着万物互联时代的开启,应用的设备底座将从几十亿手机扩展到数百亿IoT设备。全新的全场景设备体验,正深入改变消费者的使用习惯。 同时应用开发者也面临设备底座从手机单设备到全场景多设…...
数据库系统概论|第五章:数据库完整性—课程笔记1
前言 在前文介绍完数据库标准语言SQL之后,大家已经基本上掌握了关于数据库编程的基本操作,那我们今天将顺承介绍关于数据库完整性的介绍,数据库的完整性是指数据的正确性和相容性。数据的完整性是为了防止数据库中存在不符合语义的数据&…...
开源无人机地面站QGroundControl安卓界面美化与逻辑优化实战
QGroundControl作为开源无人机地面站软件,其安卓客户端界面美化与逻辑优化是提升用户体验的重要工程。 通过Qt框架的界面重构和代码逻辑优化,可以实现视觉升级与性能提升的双重目标。本文将系统讲解QGC安卓客户端的二次开发全流程,包括开发环境搭建、界面视觉升级、多分辨率…...
工作记录 2017-12-12 + 在IIS下发布wordpress
工作记录 2017-12-12 序号 工作 相关人员 1 修改邮件上的问题。 更新RD服务器。 在IIS下发布wordpress。 郝 服务器更新 RD服务器更新了,更新的文件放在190的D:\Temp\CHTeam\fnehr_update_20171212\下了。 数据库更新: 数据库没有更新 更新的文件…...
BBR 之 ProbeRTT 新改
早在 1981 年,Jaffe 在 Flow Control Power is Nondecentralizable 中就给出过论证,测量 maxbw 必然引入队列,而获得 minrtt 时带宽必然欠载,这确定了后面 30 年的拥塞控制算法基调,但 BBR 在 35 年后非常聪明地在两者…...
[创业之路-354]:农业文明到智能纪元:四次工业革命下的人类迁徙与价值重构
农业文明到智能纪元:四次工业革命下的人类迁徙与价值重构 从游牧到定居,从蒸汽轰鸣到算法洪流,人类文明的每一次跨越都伴随着生产关系的剧烈震荡。四次工业革命的浪潮不仅重塑了物质世界的生产方式,更将人类推向了身份认同与存在…...
敏感词 v0.25.0 新特性之 wordCheck 策略支持用户自定义
开源项目 敏感词核心 https://github.com/houbb/sensitive-word 敏感词控台 https://github.com/houbb/sensitive-word-admin 版本特性 大家好,我是老马。 敏感词一开始了内置了多种检验策略,但是很多用户在使用的过程中希望可以自定义策略。 所以 v0…...
从0到上线,CodeBuddy 如何帮我快速构建旅游 App?
引言 腾讯云AI代码助手之前就改成了CodeBuddy我相信这也是在为后期做准备。那么这篇文章会对CodeBuddy进行比较详细的介绍,并一起来上手实战,感受一下实际开发中这款插件能带给我们多少的便利。本篇文章是一边写一边进行测试,并不是测试完之…...
微信小程序 自定义组件 标签管理
环境 小程序环境: 微信开发者工具:RC 1.06.2503281 win32-x64 基础运行库:3.8.1 概述 基础功能 标签增删改查:支持添加/删除单个标签、批量删除、重置默认标签 数据展示:通过对话框展示结构化数据并支持复制 动…...
从 Eclipse Papyrus / XText 转向.NET —— SCADE MBD技术的演化
从KPN[1]的萌芽开始,到SCADE的推出[2],再到Scade 6的技术更迭[3],SCADE 基于模型的开发技术已经历许多。现在,Scade One 已开启全新的探索 —— 从 Eclipse Papyrus / XText 转向.NET 8跨平台应用。 [1]: KPN, Kahn进程网络 (197…...
【学习笔记】机器学习(Machine Learning) | 第五章(2)| 分类与逻辑回归
机器学习(Machine Learning) 简要声明 基于吴恩达教授(Andrew Ng)课程视频 BiliBili课程资源 文章目录 机器学习(Machine Learning)简要声明 二、决策边界决策边界的数学表达线性决策边界示例非线性决策边界非线性决策边界的示例…...
python 常用web开发框架及使用示例
Python常用Web开发框架及使用示例 Python拥有丰富的Web开发框架生态系统,以下是主流框架及其使用示例: 一、Flask - 轻量级框架 安装 pip install flask 基础示例 from flask import Flask, request, jsonifyapp Flask(__name__)app.route(/) def…...
[ Qt ] | 第一个Qt程序
1. 创建Qt项目 我们打开Qt Create工具,左上角“文件”,新建文件。 --- --- --- --- 这个是我们的APP“走出国门”的时候,要关注的,这里就不说了。 后面这两个直接默认,下一步就行~~。 2. 项目默认内容 下面就是Qt C…...
react + antd 实现后台管理系统
文章目录 完整路由搭建Layout 和 Aside组件引入 AntdAside组件实现 项目效果图 项目完整代码地址 https://gitee.com/lyh1999/react-back-management 项目完整代码地址 react依赖安装 最好采用yarn 安装 react-router 安装依赖 配置路由 history模式 / // src/router/…...
vue3+ts项目 配置vue-router
安装vue-router pnpm install vue-router配置 1.src/router/index.ts文件下的内容 import type { App } from vue import type { RouteRecordRaw } from vue-router import { createRouter, createWebHistory } from vue-router import remainingRouter from ./modules/remai…...
MySQL基本查询(二)
文章目录 UpdateDelete插入查询结果(select insert)聚合函数分组聚合统计 Update 1. 语法: set后面加列属性或者表达式 UPDATE table_name SET column expr [, column expr …][WHERE …] [ORDER BY …] [LIMIT …] 案例 将孙悟空同学的…...
MySQL:联合查询
目录 一、笛卡尔积 二、内连接 三、外连接 (1)左外连接 (2)右外连接 (3)全外连接 四、自连接 五、子查询 (1)单行子查询 (2)多行子查询 &…...
[算法学习]——通过RMQ与dfs序实现O(1)求LCA(含封装板子)
每周五篇博客:(3/5) 碎碎念 其实不是我想多水一篇博客,本来这篇是欧拉序的博客,结果dfs序也是可以O1求lca的,而且常数更优,结果就变成这样了。。。 前置知识 [算法学习]——dfs序 思想 分…...
复刻低成本机械臂 SO-ARM100 舵机配置篇(WSL)
视频讲解: 复刻低成本机械臂 SO-ARM100 舵机配置篇(WSL) 飞特舵机 组装之前需要配置舵机的ID,如下的网址为舵机的资料,实际上用不到,但可以mark在这里 Software-深圳飞特模型有限公司 User Guide里面可以…...
聊一聊接口测试更侧重于哪方面的验证
目录 一、功能性验证 输入与输出正确性 参数校验 业务逻辑覆盖 二、数据一致性验证 数据格式规范 数据完整性 数据类型与范围 三、异常场景验证 容错能力测试 边界条件覆盖 错误码与信息清晰度 四、安全与权限验证 身份认证 数据安全 防攻击能力 五、性能与可…...
【网络安全实验】SSL协议的应用
目录 一、SSL协议介绍 2.功能与特点 1)数据加密 2)身份验证 3)数据完整性校验 3.SSL的工作流程(握手过程) 1)客户端问候(ClientHello) 2)服务器响应(…...
测试——用例篇
目录 1. 测试用例 1.1 概念 2. 设计测试用例的万能公式 2.1 常规思考逆向思维发散性思维 2.2 万能公式 3. 设计测试用例例的方法 3.1 基于需求的设计方法 编辑 3.2 具体的设计方法 3.2.1 等价类 3.2.2 边界值 3.2.3 正交法 3.2.4 判定表法 3.2.5 场景法 3.2.6…...
计算机视觉技术的发展历程
计算机视觉技术的发展历程可以分为以下几个阶段: 早期探索阶段(1960s-1980s) 1960年代:计算机视觉的概念开始形成,研究者尝试让计算机识别和理解图像,主要集中在基础的图像处理,如边缘检测和特…...
docker 官方:在 alpine 上安装 python 的方法
在 alpine 上安装 python 的方法在 alpine 上安装 python 的方法: # alpine 官方 apk add python3 # docker 官方 docker pull python:3.11-alpine # 第三方 docker run --rm frolvlad/alpine-python3 python3 -c print("Hello World") # 编译安装 略 要点…...
mescroll.js 是在 H5端 运行的下拉刷新和上拉加载插件
1. mescroll的uni版本, 是专门用在uni-app的下拉刷新和上拉加载的组件, 支持一套代码编译到iOS、Android、H5、小程序等多个平台 2. mescroll的uni版本, 继承了mescroll.js的实用功能: 自动处理分页, 自动控制无数据, 空布局提示, 回到顶部按钮 .. 3. mescroll的uni版本, 丰富的…...
openEuler 22.03 安装 Mysql 5.7,RPM 在线安装
目录 一、检查系统是否安装其他版本Mariadb数据库二、安装 MySQL三、配置 MySQL四、修改默认存储路径五、开放防火墙端口六、数据备份七、生产环境优化八、常用命令 一、检查系统是否安装其他版本Mariadb数据库 # 查看已安装的 Mariadb 数据库版本 [rootopeneuler ~]# rpm -qa…...
云原生后端架构的挑战与应对策略
📝个人主页🌹:慌ZHANG-CSDN博客 🌹🌹期待您的关注 🌹🌹 随着云计算、容器化以及微服务等技术的快速发展,云原生架构已经成为现代软件开发和运维的主流趋势。企业通过构建云原生后端系统,能够实现灵活的资源管理、快速的应用迭代和高效的系统扩展。然而,尽管云原…...
第十六届蓝桥杯 2025 C/C++组 客流量上限
目录 题目: 题目描述: 题目链接: 思路: 打表找规律: 核心思路: 思路详解: 得到答案的方式: 按计算器: 暴力求解代码: 快速幂代码: 位运…...
LeetCode算法题 (移除链表元素)Day15!!!C/C++
https://leetcode.cn/problems/remove-linked-list-elements/description/ 一、题目分析 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val val 的节点,并返回 新的头节点 。 今天的题目非常好理解,也就是要删除…...
stm32 HAI库 SPI(一)原理
基本特点 通信方式:同步、串行(串行、并行、并发,别再傻傻分不清了!_串行和并行的区别-CSDN博客)、全双工 (也可以选择半双工)速率:50MHZ以下数据格式:8位/16位传输顺序…...
仿腾讯会议——主界面设计创建房间加入房间客户端实现
1、实现腾讯会议主界面 2、添加Qt类WeChatDialog 3、定义创建会议和加入会议的函数 4、实现显示名字、头像的函数 调用函数 5、在中间者类中绑定函数 6、实现创建房间的槽函数 7、实现加入房间的槽函数 8、设置界面标题 9、服务器定义创建和进入房间函数 10、服务器实现创建房间…...
在pycharm profession 2020.3上安装使用xlwings
之前写了一篇文章在win7和python3.8上安装xlwings-CSDN博客 今天安装了pycharm profession 2020.3,自带Terminal,所以试一下安装xlwings。 一、新建一个python项目 二、安装xlwings 三、输入安装命令 pip3.exe install -i https://pypi.tuna.tsinghu…...
Mybatis学习笔记
介绍 MyBatis 是一款优秀的持久层开发框架,它在 Java 开发中被广泛应用,以下是对它的详细介绍: 概述 MyBatis 最初是 Apache 的一个开源项目 iBatis,2010 年这个项目由 Apache Software Foundation 迁移到了 Google Code&#…...
「Mac畅玩AIGC与多模态13」开发篇09 - 基于多插件协同开发智能体应用(天气+名言查询助手)
一、概述 本篇介绍如何在 macOS 环境下,同时接入多个自定义 OpenAPI 插件,实现智能体根据用户请求自动分析,调用天气查询或名言查询服务,完成多功能协同应用开发。 二、环境准备 1. 确认本地开发环境 macOS 系统Dify 平台已部署并可访问可正常访问外部 API 服务2. 准备天…...
C++--入门基础
C入门基础 1. C的第一个程序 C继承C语言许多大多数的语法,所以以C语言实现的hello world也可以运行,C中需要把文件定义为.cpp,vs编译器看是.cpp就会调用C编译器编译,linux下要用g编译,不再是gcc。 // test.cpp #inc…...
Ubuntu环境下如何管理系统中的用户:创建用户、删除用户、修改密码、切换用户、用户组管理
管理用户的操作需要root权限,在执行命令时需要加sudo,关于sudo命令可以看这篇:Linux_sudo命令的使用与机制 1、添加用户 使用命令: adduser 用户名,主要是按提示输入密码和用户信息(可直接回车使用默认配置…...
广告事件聚合系统设计
需求背景 广告事件需要进行统计,计费,分析等。所以我们需要由数据接入,数据处理,数据存储,数据查询等多个服务模块去支持我们的广告系统 规模上 10000 0000个点击(10000 00000 / 100k 1wQPS) …...
PDF智能解析与知识挖掘:基于pdfminer.six的全栈实现
前言 在数字化信息爆炸的时代,PDF(便携式文档格式)作为一种通用的电子文档标准,承载着海量的结构化与非结构化知识。然而,PDF格式的设计初衷是用于展示而非数据提取,这使得从PDF中挖掘有价值的信息成为数据…...
VGG网络模型
VGG网络模型 诞生背景 VGGNet是牛津大学计算机视觉组核谷歌DeepMind一起研究出来的深度卷积神经网络。VGG是一种被广泛使用的卷积神经网络结构,其在2014年的ImageNet大规模视觉识别挑战中获得亚军。 通常所说的VGG是指VGG-16(13层卷积层3层全连接层)。具有规律的…...
开闭原则与依赖倒置原则区别:原类不变,新增类(功能)vs 接口类不变,原实现类可变
好,我来用最通俗的方式,用角色扮演 场景对话,不讲术语,让你彻底明白「依赖倒置原则」和「开闭原则」的区别。 🎭 场景:你是老板(高层),你要雇人做事 一、【依赖倒置原则…...
【AI面试准备】Azure DevOps沙箱实验全流程详解
介绍动手实验:通过 Azure DevOps 沙箱环境实操,体验从代码提交到测试筛选的全流程。如何快速掌握,以及在实际工作中如何运用。 通过 Azure DevOps 沙箱环境进行动手实验,是快速掌握 DevOps 全流程(从代码提交到测试筛选…...
大数据面试问答-数据湖
1. 概念 数据湖(Data Lake): 以原始格式(如Parquet、JSON等)存储海量原始数据的存储库,支持结构化、半结构化和非结构化数据(如文本、图像)。采用Schema-on-Read模式,数…...