结构型模式
结构型设计模式关注于如何将类和对象组合在一起形成更大的结构,同时保证这些结构的灵活、高效和可重用。以下是一些主要的结构型设计模式及其简介:
适配器模式(Adapter Pattern)
目的:使原本不兼容的接口可以协同工作。适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不匹配而无法工作的两个类能够一起工作。
JS示例
假设我们有一个旧的音频播放器类 LegacyAudioPlayer,它只能播放 MP3 文件。现在,我们希望它也能播放 WAV 和 OGG 格式的音频文件。为此,我们可以创建适配器类来包装现有的播放器,使其能够处理其他格式。
// 1. 定义目标接口
class AudioPlayer {
play(audioType, filename) {
throw new Error('Method not implemented.');
}
}
// 2. 实现旧的音频播放器类
class LegacyAudioPlayer {
playMP3(filename) {
console.log(`Playing MP3 file. Name: ${filename}`);
}
}
// 3. 创建适配器类
class AudioPlayerAdapter extends AudioPlayer {
constructor() {
super();
this.legacyAudioPlayer = new LegacyAudioPlayer();
}
play(audioType, filename) {
if (audioType === 'mp3') {
this.legacyAudioPlayer.playMP3(filename);
} else if (audioType === 'wav') {
this.convertAndPlayWAV(filename);
} else if (audioType === 'ogg') {
this.convertAndPlayOGG(filename);
}
}
convertAndPlayWAV(filename) {
console.log(`Converting WAV file and playing. Name: ${filename}`);
}
convertAndPlayOGG(filename) {
console.log(`Converting OGG file and playing. Name: ${filename}`);
}
}
// 4. 使用适配器
const audioPlayer = new AudioPlayerAdapter();
audioPlayer.play('mp3', 'song.mp3'); // Playing MP3 file. Name: song.mp3
audioPlayer.play('wav', 'song.wav'); // Converting WAV file and playing. Name: song.wav
audioPlayer.play('ogg', 'song.ogg'); // Converting OGG file and playing. Name: song.ogg
在这个例子中,AudioPlayer 定义了目标接口,LegacyAudioPlayer 是一个旧的类,它只支持 MP3 格式。AudioPlayerAdapter 是适配器类,它包装了 LegacyAudioPlayer,并实现了 AudioPlayer 接口,使得它可以处理 MP3 以外的其他格式。适配器类通过在内部转换其他格式的音频文件,然后使用 LegacyAudioPlayer 播放 MP3 文件,从而实现了接口的适配。
php示例
假设我们有两个类,一个是旧系统中的LegacyPaymentProcessor,它有一个名为processPayment的方法,另一个是我们新系统中的PaymentGateway接口,它要求实现pay方法。我们的目标是让旧系统的支付处理器能够适应新系统的要求。
// 定义接口
interface PaymentGateway {
public function pay(float $amount);
}
// 实现旧系统中的类
class LegacyPaymentProcessor {
public function processPayment(float $amount) {
echo "Processing payment of $" . $amount . " using legacy system.\n";
}
}
// 创建适配器
class LegacyPaymentAdapter implements PaymentGateway {
private $legacyPaymentProcessor;
public function __construct(LegacyPaymentProcessor $legacyPaymentProcessor) {
$this->legacyPaymentProcessor = $legacyPaymentProcessor;
}
public function pay(float $amount) {
$this->legacyPaymentProcessor->processPayment($amount);
}
}
// 使用适配器
$legacyProcessor = new LegacyPaymentProcessor();
$adapter = new LegacyPaymentAdapter($legacyProcessor);
$adapter->pay(100.0);
应用场景
- 兼容性问题:当你需要在系统中集成一个外部组件或库,但它的接口与你的系统不兼容时。
- 复用代码:你想复用一些已经存在的类,但是它们的接口不符合当前需求。
- 框架或库的更新:当一个你依赖的框架或库更新了API,而你不想修改所有使用该API的地方时,可以使用适配器模式来平滑过渡。
- 多态性:你想在不改变客户端代码的情况下,通过多态性来扩展系统行为。
- 系统升级:在系统升级过程中,新的模块可能需要与旧的模块交互,适配器模式可以帮助解决接口不匹配的问题。
桥接模式(Bridge Pattern)
目的:将抽象部分与其实现部分分离,使它们都可以独立变化。桥接模式通过提供抽象化和实现化之间的桥梁,让两者可以独立演化。
JS示例
假设我们有一个图形库,需要支持不同的形状(如圆形、矩形)以及不同的渲染方式(如屏幕渲染、打印渲染)。这里,形状是抽象部分,渲染方式是实现部分。
// 1. 定义实现接口
class Renderer {
drawCircle(radius) {
throw new Error('Renderer must implement drawCircle method.');
}
}
// 2. 实现具体的渲染器
class ScreenRenderer extends Renderer {
drawCircle(radius) {
console.log(`Drawing circle on screen with radius ${radius}`);
}
}
class PrintRenderer extends Renderer {
drawCircle(radius) {
console.log(`Printing circle with radius ${radius}`);
}
}
// 3. 定义抽象部分
class Shape {
constructor(renderer) {
this.renderer = renderer;
}
}
class Circle extends Shape {
constructor(renderer, radius) {
super(renderer);
this.radius = radius;
}
draw() {
this.renderer.drawCircle(this.radius);
}
}
// 4. 使用桥接模式
const screenRenderer = new ScreenRenderer();
const printRenderer = new PrintRenderer();
const screenCircle = new Circle(screenRenderer, 5);
const printCircle = new Circle(printRenderer, 5);
screenCircle.draw(); // Drawing circle on screen with radius 5
printCircle.draw(); // Printing circle with radius 5
在这个例子中,Shape 类是抽象部分,它持有对实现部分(Renderer)的引用。Circle 类继承自 Shape 并实现了具体的形状绘制逻辑。ScreenRenderer 和 PrintRenderer 分别实现了不同的渲染方式。这样,我们可以独立地扩展形状和渲染方式,而不需要修改现有代码。
PHP示例
假设我们需要构建一个消息通知系统,它可以发送不同类型的消息(普通、警告、紧急)并通过不同的渠道(电子邮件、短信、社交媒体)发送。我们可以使用桥接模式来设计这个系统。
// 步骤 1: 定义抽象消息接口
interface Message {
public function send(string $message);
}
// 步骤 2: 实现具体的发送渠道
class Email implements Message {
public function send(string $message) {
echo "Sending email: " . $message . "\n";
}
}
class SMS implements Message {
public function send(string $message) {
echo "Sending SMS: " . $message . "\n";
}
}
class SocialMedia implements Message {
public function send(string $message) {
echo "Posting to social media: " . $message . "\n";
}
}
// 步骤 3: 定义消息类型抽象类
abstract class Notification {
protected $channel;
public function __construct(Message $channel) {
$this->channel = $channel;
}
abstract public function formatMessage(string $message): string;
abstract public function sendNotification(string $message);
}
// 步骤 4: 实现具体的消息类型
class RegularNotification extends Notification {
public function formatMessage(string $message): string {
return "[Regular] " . $message;
}
public function sendNotification(string $message) {
$formattedMessage = $this->formatMessage($message);
$this->channel->send($formattedMessage);
}
}
class WarningNotification extends Notification {
public function formatMessage(string $message): string {
return "[Warning] " . $message;
}
public function sendNotification(string $message) {
$formattedMessage = $this->formatMessage($message);
$this->channel->send($formattedMessage);
}
}
class CriticalNotification extends Notification {
public function formatMessage(string $message): string {
return "[Critical] " . $message;
}
public function sendNotification(string $message) {
$formattedMessage = $this->formatMessage($message);
$this->channel->send($formattedMessage);
}
}
// 步骤 5: 使用桥接模式
$regularEmail = new RegularNotification(new Email());
$warningSMS = new WarningNotification(new SMS());
$criticalSocialMedia = new CriticalNotification(new SocialMedia());
$regularEmail->sendNotification("This is a regular message.");
$warningSMS->sendNotification("This is a warning message.");
$criticalSocialMedia->sendNotification("This is a critical message.");
在这个示例中,Notification 类是抽象部分,它持有对实现部分(Message 接口)的引用。RegularNotification, WarningNotification, 和 CriticalNotification 分别实现了具体的消息类型。Email, SMS, 和 SocialMedia 则分别实现了具体的发送渠道。这样,我们就可以独立地扩展消息类型和发送渠道,而不需要修改现有代码。
应用场景
- 抽象与实现分离:当一个类的实现应该独立于它的抽象接口时,例如,图形库中的形状绘制,形状的类型(圆形、矩形)和绘制上下文(屏幕、打印机)应该可以独立变化。
- 多维度分类:当一个类有多个分类维度,而这些分类维度需要独立扩展时,例如,设备的型号和颜色。
- 减少子类数量:当使用继承来组合多个分类维度会导致子类数量呈指数增长时,桥接模式可以减少子类的数量,简化类层次结构。
备注:桥接模式和建造者模式的区别,建造者模式主要强调的是主流程一致(即只有一座桥,桥不可扩展),桥接模式主要强调的是桥(可以有很多座桥,桥作为业务扩展项)
组合模式(Composite Pattern)
目的:允许你将对象组合成树状结构来表示整体-部分层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
JS示例
假设我们要创建一个文件系统模型,其中包含文件夹和文件。文件夹可以包含文件和其他文件夹,而文件只能作为叶子节点存在。
// 1. 定义抽象组件
class FileSystemNode {
constructor(name) {
this.name = name;
}
add(node) {
throw new Error('Not implemented');
}
remove(node) {
throw new Error('Not implemented');
}
display(level) {
throw new Error('Not implemented');
}
}
// 2. 实现具体组件
class File extends FileSystemNode {
constructor(name) {
super(name);
}
display(level) {
console.log('-'.repeat(level * 2) + this.name);
}
}
class Directory extends FileSystemNode {
constructor(name) {
super(name);
this.children = [];
}
add(node) {
this.children.push(node);
}
remove(node) {
const index = this.children.indexOf(node);
if (index !== -1) {
this.children.splice(index, 1);
}
}
display(level) {
console.log('-'.repeat(level * 2) + this.name);
this.children.forEach(child => child.display(level + 1));
}
}
// 3. 使用组合模式
const root = new Directory('root');
const docs = new Directory('documents');
const images = new Directory('images');
const textFile = new File('text.txt');
const imageFile = new File('image.png');
docs.add(textFile);
images.add(imageFile);
root.add(docs);
root.add(images);
root.display(0);
在这个例子中,FileSystemNode 是抽象组件,File 和 Directory 是具体组件。Directory 类实现了 add 和 remove 方法来管理其子节点,而 display 方法则递归地显示整个树状结构。
PHP示例
假设我们要创建一个文件系统模型,其中包含文件夹和文件。文件夹可以包含文件和其他文件夹,而文件只能作为叶子节点存在。
// 1. 定义抽象组件
interface FileSystemComponent {
public function add(FileSystemComponent $component);
public function remove(FileSystemComponent $component);
public function getChildren(): array;
public function operation();
}
// 2. 文件和文件夹类声明
class File implements FileSystemComponent {
private $name;
public function __construct($name) {
$this->name = $name;
}
public function operation() {
echo "File Operation: " . $this->name . "\n";
}
public function add(FileSystemComponent $component) {
throw new Exception("Cannot add to a file.");
}
public function remove(FileSystemComponent $component) {
throw new Exception("Cannot remove from a file.");
}
public function getChildren() {
throw new Exception("A file has no children.");
}
}
class Directory implements FileSystemComponent {
private $name;
private $children = [];
public function __construct($name) {
$this->name = $name;
}
public function add(FileSystemComponent $component) {
$this->children[] = $component;
}
public function remove(FileSystemComponent $component) {
$key = array_search($component, $this->children);
if ($key !== false) {
unset($this->children[$key]);
}
}
public function getChildren() {
return $this->children;
}
public function operation() {
echo "Directory Operation: " . $this->name . "\n";
foreach ($this->children as $child) {
$child->operation();
}
}
}
// 3. 使用组合模式
$rootDir = new Directory("root");
$binDir = new Directory("bin");
$docDir = new Directory("doc");
$rootDir->add($binDir);
$rootDir->add($docDir);
$binDir->add(new File("vi"));
$binDir->add(new File("latex"));
$docDir->add(new File("readme"));
$docDir->add(new File("license"));
$rootDir->operation();
在这个例子中,FileSystemComponent 接口定义了所有组件共有的方法。File 和 Directory 类实现了这个接口,其中 Directory 类还实现了添加、移除和获取子组件的方法。通过递归地调用 operation 方法,我们可以遍历整个文件系统树并执行操作。
应用场景
- 表示对象的分层结构:例如文件系统中的目录和文件,UI 组件中的容器和子组件,或者公司组织结构中的部门和员工。
- 需要递归处理树状结构:例如遍历文件系统,或者在 UI 中遍历组件树来处理事件。
- 希望用户能忽略层次结构的存在:组合模式让用户能够以统一的方式处理单个对象和组合对象,而不必关心它们的实际类型。
装饰器模式(Decorator Pattern)
目的:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式相比生成子类更为灵活。
JS示例
假设我们有一个简单的文本处理系统,它可以处理纯文本,但我们想要扩展它的功能,比如添加加粗、斜体和下划线等功能。我们可以使用装饰器模式来实现这一目标。
// 1. 定义基本的文本处理类
class Text {
constructor(content) {
this.content = content;
}
render() {
return this.content;
}
}
// 2. 创建装饰器基类
class TextDecorator {
constructor(text) {
this.text = text;
}
render() {
return this.text.render();
}
}
// 3. 实现具体的装饰器
class BoldText extends TextDecorator {
render() {
return `<strong>${this.text.render()}</strong>`;
}
}
class ItalicText extends TextDecorator {
render() {
return `<em>${this.text.render()}</em>`;
}
}
class UnderlineText extends TextDecorator {
render() {
return `<u>${this.text.render()}</u>`;
}
}
// 4. 使用装饰器
const originalText = new Text('Hello, world!');
const boldText = new BoldText(originalText);
const italicText = new ItalicText(originalText);
const underlineText = new UnderlineText(originalText);
console.log(boldText.render()); // <strong>Hello, world!</strong>
console.log(italicText.render()); // <em>Hello, world!</em>
console.log(underlineText.render()); // <u>Hello, world!</u>
// 多个装饰器可以组合使用
const decoratedText = new BoldText(new ItalicText(new UnderlineText(originalText)));
console.log(decoratedText.render()); // <strong><em><u>Hello, world!</u></em></strong>
在这个例子中,Text 类是基础的文本处理类,TextDecorator 是装饰器基类,而 BoldText、ItalicText 和 UnderlineText 是具体的装饰器类。每个装饰器类都包裹了一个 Text 或者另一个 TextDecorator 实例,并通过重写 render 方法来添加特定的格式化效果。通过组合多个装饰器,我们可以灵活地为原始文本添加多种格式化效果。
PHP示例
我们将创建一个咖啡饮品的基础类,然后使用装饰器来动态添加不同的配料
// 抽象组件
abstract class Beverage {
protected $description = 'Unknown Beverage';
public function getDescription() {
return $this->description;
}
abstract public function cost();
}
// 具体组件
class Espresso extends Beverage {
public function __construct() {
$this->description = 'Espresso';
}
public function cost() {
return 1.99;
}
}
// 抽象装饰器
abstract class CondimentDecorator extends Beverage {
abstract public function getDescription();
}
// 具体装饰器
class Milk extends CondimentDecorator {
private $beverage;
public function __construct(Beverage $beverage) {
$this->beverage = $beverage;
}
public function getDescription() {
return $this->beverage->getDescription() . ', Milk';
}
public function cost() {
return $this->beverage->cost() + 0.25;
}
}
class Mocha extends CondimentDecorator {
private $beverage;
public function __construct(Beverage $beverage) {
$this->beverage = $beverage;
}
public function getDescription() {
return $this->beverage->getDescription() . ', Mocha';
}
public function cost() {
return $this->beverage->cost() + 0.50;
}
}
// 动态添加装饰器
$espresso = new Espresso();
echo $espresso->getDescription() . ': $' . number_format($espresso->cost(), 2) . "\n";
$milk = new Milk($espresso);
echo $milk->getDescription() . ': $' . number_format($milk->cost(), 2) . "\n";
$mochaMilk = new Mocha($milk);
echo $mochaMilk->getDescription() . ': $' . number_format($mochaMilk->cost(), 2) . "\n";
在这个示例中:
- Beverage 是抽象组件,它定义了所有饮料都应该有的行为。
- Espresso 是具体组件,它实现了 Beverage 接口,并且提供了具体的实现。
- CondimentDecorator 是抽象装饰器,它继承自 Beverage 并且定义了所有装饰器应该有的行为。
- Milk 和 Mocha 是具体装饰器,它们继承自 CondimentDecorator 并且实现了具体的行为。
在运行时,你可以动态地决定是否要添加 Milk 或 Mocha 到 Espresso 上,这使得你可以在不修改原始组件的情况下,根据需要添加新的功能。
请注意,在上面的代码中有一处小错误,$this->be饮料; 应该是 $this->beverage;。这是一个笔误,实际代码中应该使用正确的变量名。
应用场景
- 动态地给一个对象添加一些额外的职责。相比生成子类来扩展功能,装饰器提供了更灵活的替代方案。
- 避免使用大量继承层次结构。装饰器可以用较少的类来达到相同的效果。
- 需要增加功能的对象数量是动态的。如果功能的增加是基于运行时的条件,那么装饰器模式可以提供一种动态的解决方案。
外观模式/门面模式(Facade Pattern)
目的:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,使得子系统更容易使用。
JS示例
假设我们有一个家庭自动化系统,其中包括灯光控制、温度控制和安全系统。我们可以通过创建一个外观类来简化客户端与这些子系统的交互。
// 子系统类
class LightControl {
turnOn() {
console.log('Lights are on.');
}
turnOff() {
console.log('Lights are off.');
}
}
class TemperatureControl {
increaseTemp() {
console.log('Temperature increased.');
}
decreaseTemp() {
console.log('Temperature decreased.');
}
}
class SecuritySystem {
arm() {
console.log('Security system armed.');
}
disarm() {
console.log('Security system disarmed.');
}
}
// 外观类
class HomeAutomationFacade {
constructor() {
this.lightControl = new LightControl();
this.temperatureControl = new TemperatureControl();
this.securitySystem = new SecuritySystem();
}
morningRoutine() {
this.lightControl.turnOn();
this.temperatureControl.increaseTemp();
this.securitySystem.disarm();
}
eveningRoutine() {
this.lightControl.turnOff();
this.temperatureControl.decreaseTemp();
this.securitySystem.arm();
}
}
// 使用外观类
const facade = new HomeAutomationFacade();
// 执行早晨的例行程序
facade.morningRoutine();
// 输出:
// Lights are on.
// Temperature increased.
// Security system disarmed.
// 执行晚上的例行程序
facade.eveningRoutine();
// 输出:
// Lights are off.
// Temperature decreased.
// Security system armed.
在这个例子中,HomeAutomationFacade 类作为外观,它包含了对 LightControl、TemperatureControl 和 SecuritySystem 的引用,并提供了 morningRoutine 和 eveningRoutine 方法来执行一系列相关操作。这样,客户端只需要与 HomeAutomationFacade 交互,而不需要直接调用子系统中的各个方法,从而简化了客户端的代码。
PHP示例
假设我们有一个在线购物系统,包括库存管理、订单处理和支付处理三个子系统。我们可以通过外观模式来简化客户端与这些子系统的交互。
// 子系统类
class Inventory {
public function checkStock($productId) {
// 检查库存逻辑
echo "Checking stock for product ID: $productId\n";
}
}
class Order {
public function createOrder($productId, $quantity) {
// 创建订单逻辑
echo "Creating order for product ID: $productId, Quantity: $quantity\n";
}
}
class Payment {
public function processPayment($amount) {
// 处理支付逻辑
echo "Processing payment of amount: $amount\n";
}
}
// 外观类
class ShoppingCartFacade {
private $inventory;
private $order;
private $payment;
public function __construct() {
$this->inventory = new Inventory();
$this->order = new Order();
$this->payment = new Payment();
}
public function purchaseProduct($productId, $quantity, $amount) {
$this->inventory->checkStock($productId);
$this->order->createOrder($productId, $quantity);
$this->payment->processPayment($amount);
}
}
// 使用外观类
$shoppingCart = new ShoppingCartFacade();
$shoppingCart->purchaseProduct(123, 2, 50.00);
在这个例子中,ShoppingCartFacade 类作为外观,它包含了对 Inventory、Order 和 Payment 的引用,并提供了一个 purchaseProduct 方法来执行一系列相关操作。这样,客户端只需要与 ShoppingCartFacade 交互,而不需要直接调用子系统中的各个方法,从而简化了客户端的代码。
应用场景
- 子系统相对复杂,客户端需要与之交互,但不希望了解子系统的复杂性。
- 需要提供一个简单的一致性接口,以便客户端能够更方便地访问子系统。
- 子系统可能被频繁地重构,需要一个稳定的接口来屏蔽这些变化。
享元模式(Flyweight Pattern)
目的:运用共享技术有效支持大量细粒度的对象。通过共享来大幅度减少内存中对象的数量,从而提升系统性能。
JS示例
假设我们正在开发一个文本编辑器,需要显示大量相同颜色的文字。为了避免为每个字符都创建一个对象,我们可以使用享元模式来共享字符的颜色信息。
// 字符享元类
class Character {
constructor(color) {
this.color = color;
}
display(charCode) {
console.log(`Displaying character with code ${charCode} in color ${this.color}`);
}
}
class CharacterFactory {
static pool = {};
static getCharacter(color) {
if (!this.pool[color]) {
this.pool[color] = new Character(color);
}
return this.pool[color];
}
}
// 使用享元模式
function displayText(text, color) {
const char = CharacterFactory.getCharacter(color);
for (let i = 0; i < text.length; i++) {
char.display(text.charCodeAt(i));
}
}
displayText("Hello World", "red");
在这个例子中,Character 类代表一个字符,它包含颜色信息。CharacterFactory 类负责创建和管理 Character 对象,它使用一个静态的 pool 对象来缓存已经创建的享元对象。当请求一个特定颜色的字符时,getCharacter 方法会检查 pool 中是否存在该颜色的字符,如果不存在,则创建一个新的 Character 对象并将其添加到 pool 中;如果存在,则直接返回已有的对象。
PHP示例
假设我们正在开发一个游戏,需要创建大量的树木对象,但是树木的种类并不多,大部分的树木只是位置不同。我们可以使用享元模式来共享树木的种类信息,只存储每棵树的位置。
// 享元接口
interface TreeFlyweight {
public function render(int $x, int $y);
}
// 具体享元类
class PineTree implements TreeFlyweight {
public function render(int $x, int $y) {
echo "Rendering a pine tree at ($x, $y)\n";
}
}
class OakTree implements TreeFlyweight {
public function render(int $x, int $y) {
echo "Rendering an oak tree at ($x, $y)\n";
}
}
// 享元工厂
class TreeFactory {
private $trees = [];
public function getTree(string $type) {
if (!isset($this->trees[$type])) {
switch ($type) {
case 'pine':
$this->trees[$type] = new PineTree();
break;
case 'oak':
$this->trees[$type] = new OakTree();
break;
default:
throw new Exception("Unknown tree type: $type");
}
}
return $this->trees[$type];
}
}
// 使用享元模式
$factory = new TreeFactory();
$tree1 = $factory->getTree('pine');
$tree1->render(10, 20);
$tree2 = $factory->getTree('pine');
$tree2->render(15, 25);
$tree3 = $factory->getTree('oak');
$tree3->render(20, 30);
在这个例子中,TreeFactory 类负责创建和管理 TreeFlyweight 对象。当请求一个特定类型的树时,getTree 方法会检查 $trees 数组中是否存在该类型的树,如果不存在,则创建一个新的树对象并将其添加到数组中;如果存在,则直接返回已有的对象。这样,无论我们需要创建多少个相同类型的树,实际上只创建了一个树对象,从而节省了大量的内存空间。
应用场景
- 图形绘制:在图形编辑器中,如果需要绘制大量相似形状,如多个相同颜色和样式的圆或矩形,可以使用享元模式来共享形状的属性,只存储位置等变化的属性。
- 游戏开发:游戏中可能有大量的相似对象,如子弹、树木、敌人等,享元模式可以用来共享这些对象的不变属性,如纹理、模型等,只存储位置和状态等变化的属性。
- 数据库连接池:在数据库连接中,创建和销毁连接的成本很高,享元模式可以用来共享连接,减少创建新连接的次数。
总结:享元模式在需要大量相似对象的场景下非常有用,尤其当对象的创建和销毁成本较高时。通过共享不变的状态,可以显著减少内存的使用,提高应用程序的性能。在上述示例中,我们展示了如何在 PHP 中实现享元模式,以优化游戏中的树木对象创建过程。
备注:原型模式和享元模式的区别,原型模式强调快速创建,享元模式强调对象的复用
代理模式(Proxy Pattern)
目的:为其他对象提供一种代理以控制对这个对象的访问。代理模式可以用于延迟初始化、访问控制、方法调用计数等场景。
JS示例
假设我们有一个图片加载的场景,图片可能很大,我们不希望在页面加载时就立即加载所有的图片,而是当图片进入可视区域时才加载。我们可以使用代理模式来实现懒加载。
// 目标对象
class Image {
constructor(url) {
this.url = url;
this.img = null;
}
load() {
this.img = new Image();
this.img.src = this.url;
console.log(`Image loaded from ${this.url}`);
}
display() {
if (!this.img) {
this.load();
}
document.body.appendChild(this.img);
}
}
// 代理对象
class ImageProxy {
constructor(url) {
this.url = url;
this.image = null;
this.visible = false;
}
get isLoaded() {
return !!this.image;
}
checkVisibility() {
const rect = this.image.getBoundingClientRect();
this.visible = (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
handleScroll() {
if (this.visible && !this.isLoaded) {
this.image = new Image();
this.image.src = this.url;
this.image.onload = () => {
document.body.appendChild(this.image);
window.removeEventListener('scroll', this.handleScroll);
};
}
}
display() {
if (!this.isLoaded) {
window.addEventListener('scroll', this.handleScroll);
this.checkVisibility();
}
}
}
// 使用代理模式
const imageProxy = new ImageProxy('https://example.com/image.jpg');
imageProxy.display();
// 当滚动到图片所在位置时,图片将被加载并显示
在这个例子中,ImageProxy 类作为代理,它包含了对实际 Image 对象的引用。当调用 display 方法时,代理首先检查图片是否已经被加载,如果没有,则监听滚动事件,在图片进入可视区域时加载图片。这样,图片只有在真正需要显示时才会被加载,节省了网络带宽和初始页面加载时间。
PHP示例
假设我们有一个场景,需要从数据库中获取大量数据,但在应用程序的初始阶段并不需要所有数据,我们只想在用户请求具体数据时才加载。
// 目标对象
class HeavyDataLoader {
private $data;
public function loadData() {
// 模拟从数据库加载大量数据
$this->data = range(1, 1000000);
echo "Data loaded.\n";
}
public function getData() {
return $this->data;
}
}
// 代理对象
class DataProxy {
private $realSubject;
private $data;
public function __construct() {
$this->realSubject = new HeavyDataLoader();
}
public function loadDataIfNecessary() {
if ($this->data === null) {
$this->realSubject->loadData();
$this->data = $this->realSubject->getData();
}
}
public function getData() {
$this->loadDataIfNecessary();
return $this->data;
}
}
// 使用代理模式
$dataProxy = new DataProxy();
// 初始时,数据并未加载
echo "Initial data access:\n";
var_dump($dataProxy->getData());
// 再次访问数据,这次数据应该已经被加载
echo "\nSubsequent data access:\n";
var_dump($dataProxy->getData());
在这个例子中,HeavyDataLoader 类是目标对象,它负责加载大量数据。DataProxy 类是代理对象,它在第一次调用 getData() 方法时才真正调用目标对象的 loadData() 方法加载数据。之后的每次调用都会直接返回之前加载的数据,避免了重复加载。
应用场景
- 远程代理:当目标对象位于远程服务器上时,代理可以作为本地代表,处理网络通信。
- 虚拟代理:用于创建开销大的对象时,代理可以先提供一个轻量级的对象,直到真正需要时再创建实际对象。
- 保护代理:用于控制对敏感对象的访问,比如检查用户权限。
- 智能引用:代理可以追踪对象的使用,比如自动释放不再使用的对象。
结构型模式对比(小结)
模式名称 | 应用场景 | 优势 |
适配器模式 | 新业务要沿用或扩展旧业务接口 | 节约代码、方便管理 |
桥接模式 | 业务逻辑深度分层 | 降低耦合、层次感分明 |
组合模式 | 数据结构深度分层 | 降低耦合、层次感分明 |
装饰模式 | 可以灵活调用的业务扩展 | 降低耦合、方便扩展 |
外观模式 | 系统对外部调用的封装 | 入口统一、方便管理、降低系统/组件间的耦合度 |
享元模式 | 通过共享对象达到节省内存的效果 | 对象复用、降低内存 |
代理模式 | 系统对外部系统的业务封装 | 出口统一、方便管理、降低系统/组件间的耦合度 |