【整理】设计模式代码示例及应用场景2

结构型模式

结构型设计模式关注于如何将类和对象组合在一起形成更大的结构,同时保证这些结构的灵活、高效和可重用。以下是一些主要的结构型设计模式及其简介:

适配器模式(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);

应用场景

  1. 兼容性问题:当你需要在系统中集成一个外部组件或库,但它的接口与你的系统不兼容时。
  2. 复用代码:你想复用一些已经存在的类,但是它们的接口不符合当前需求。
  3. 框架或库的更新:当一个你依赖的框架或库更新了API,而你不想修改所有使用该API的地方时,可以使用适配器模式来平滑过渡。
  4. 多态性:你想在不改变客户端代码的情况下,通过多态性来扩展系统行为。
  5. 系统升级:在系统升级过程中,新的模块可能需要与旧的模块交互,适配器模式可以帮助解决接口不匹配的问题。

桥接模式(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 则分别实现了具体的发送渠道。这样,我们就可以独立地扩展消息类型和发送渠道,而不需要修改现有代码。

应用场景

  1. 抽象与实现分离:当一个类的实现应该独立于它的抽象接口时,例如,图形库中的形状绘制,形状的类型(圆形、矩形)和绘制上下文(屏幕、打印机)应该可以独立变化。
  2. 多维度分类:当一个类有多个分类维度,而这些分类维度需要独立扩展时,例如,设备的型号和颜色。
  3. 减少子类数量:当使用继承来组合多个分类维度会导致子类数量呈指数增长时,桥接模式可以减少子类的数量,简化类层次结构。

备注:桥接模式和建造者模式的区别,建造者模式主要强调的是主流程一致(即只有一座桥,桥不可扩展),桥接模式主要强调的是桥(可以有很多座桥,桥作为业务扩展项)

组合模式(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 方法,我们可以遍历整个文件系统树并执行操作。

应用场景

  1. 表示对象的分层结构:例如文件系统中的目录和文件,UI 组件中的容器和子组件,或者公司组织结构中的部门和员工。
  2. 需要递归处理树状结构:例如遍历文件系统,或者在 UI 中遍历组件树来处理事件。
  3. 希望用户能忽略层次结构的存在:组合模式让用户能够以统一的方式处理单个对象和组合对象,而不必关心它们的实际类型。

装饰器模式(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;。这是一个笔误,实际代码中应该使用正确的变量名。

应用场景

  1. 动态地给一个对象添加一些额外的职责。相比生成子类来扩展功能,装饰器提供了更灵活的替代方案。
  2. 避免使用大量继承层次结构。装饰器可以用较少的类来达到相同的效果。
  3. 需要增加功能的对象数量是动态的。如果功能的增加是基于运行时的条件,那么装饰器模式可以提供一种动态的解决方案。

外观模式/门面模式(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 交互,而不需要直接调用子系统中的各个方法,从而简化了客户端的代码。

应用场景

  1. 子系统相对复杂,客户端需要与之交互,但不希望了解子系统的复杂性。
  2. 需要提供一个简单的一致性接口,以便客户端能够更方便地访问子系统。
  3. 子系统可能被频繁地重构,需要一个稳定的接口来屏蔽这些变化。

享元模式(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 数组中是否存在该类型的树,如果不存在,则创建一个新的树对象并将其添加到数组中;如果存在,则直接返回已有的对象。这样,无论我们需要创建多少个相同类型的树,实际上只创建了一个树对象,从而节省了大量的内存空间。

应用场景

  1. 图形绘制:在图形编辑器中,如果需要绘制大量相似形状,如多个相同颜色和样式的圆或矩形,可以使用享元模式来共享形状的属性,只存储位置等变化的属性。
  2. 游戏开发:游戏中可能有大量的相似对象,如子弹、树木、敌人等,享元模式可以用来共享这些对象的不变属性,如纹理、模型等,只存储位置和状态等变化的属性。
  3. 数据库连接池:在数据库连接中,创建和销毁连接的成本很高,享元模式可以用来共享连接,减少创建新连接的次数。

总结:享元模式在需要大量相似对象的场景下非常有用,尤其当对象的创建和销毁成本较高时。通过共享不变的状态,可以显著减少内存的使用,提高应用程序的性能。在上述示例中,我们展示了如何在 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() 方法加载数据。之后的每次调用都会直接返回之前加载的数据,避免了重复加载。

应用场景

  1. 远程代理:当目标对象位于远程服务器上时,代理可以作为本地代表,处理网络通信。
  1. 虚拟代理:用于创建开销大的对象时,代理可以先提供一个轻量级的对象,直到真正需要时再创建实际对象。
  2. 保护代理:用于控制对敏感对象的访问,比如检查用户权限。
  3. 智能引用:代理可以追踪对象的使用,比如自动释放不再使用的对象。

结构型模式对比(小结)

模式名称

应用场景

优势

适配器模式

新业务要沿用或扩展旧业务接口

节约代码、方便管理

桥接模式

业务逻辑深度分层

降低耦合、层次感分明

组合模式

数据结构深度分层

降低耦合、层次感分明

装饰模式

可以灵活调用的业务扩展

降低耦合、方便扩展

外观模式

系统对外部调用的封装

入口统一、方便管理、降低系统/组件间的耦合度

享元模式

通过共享对象达到节省内存的效果

对象复用、降低内存

代理模式

系统对外部系统的业务封装

出口统一、方便管理、降低系统/组件间的耦合度

  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值