软件设计模式核心教程:初学者快速入门
引言
软件设计模式是软件开发中针对常见问题的典型解决方案 1。它们如同经验丰富的工程师们绘制的蓝图,可以被定制以解决代码中特定的设计难题 1。对于初学者而言,理解设计模式的挑战往往在于其抽象性。本教程旨在提供一种比《Head First 设计模式》更简洁、快速的入门方法,专注于核心设计模式。通过现实世界的类比来阐释每个模式的核心思想,将有助于初学者更好地理解这些概念。正如城市规划师使用蓝图来设计城市布局,而建筑师在城市内部使用特定的建筑设计图纸一样,软件架构模式关注系统的高层结构,而设计模式则为该结构内重复出现的编码问题提供可复用的解决方案 3。
设计模式并非具体的代码或算法,而是一种概念性的指导方针,可以在任何编程语言中实现 4。它们提供了一种共享的设计语言,有助于开发者之间更有效地沟通,减少误解并提高生产力 2。本教程将包含 Python 和 C++ 的简单代码示例,并使用 Mermaid 语法尝试绘制图示,以帮助理解。
设计模式概述
设计模式通常根据其要解决的问题类型分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns)和行为型模式(Behavioral Patterns)4。这种分类方式有助于理解特定模式旨在解决何种类型的设计挑战。
- 创建型模式 (Creational Patterns):专注于对象的创建过程或与对象创建相关的问题 5。它们的目标是以一种灵活和可重用的方式来创建对象,使得对象的创建过程更加高效和适应不同的需求 7。
- 结构型模式 (Structural Patterns):关注如何组合类和对象以形成更大的结构,同时保持这些结构的灵活性和效率 6。它们帮助设计功能强大且灵活的类 4。
- 行为型模式 (Behavioral Patterns):描述对象类之间相互通信的方式以及它们如何分配职责 4。这些模式有助于管理算法、对象间的通信或职责分配,促进对象间的松散耦合 6。
理解这些分类有助于在遇到特定设计问题时,缩小可能适用的设计模式范围。
核心设计模式详解
本节将详细介绍六个核心设计模式:单例模式、工厂方法模式、观察者模式、策略模式、适配器模式和外观模式。这些模式因其广泛的适用性和对理解更复杂模式的基础作用而被选中。
1. 单例模式 (Singleton Pattern)
类别:创建型模式 5
一句话比喻:单例模式就像一个国家只有一个总统或一个班级只有一个班主任。无论何时何地提及“总统”或“班主任”,指的都是同一个人 8。
定义:单例模式确保一个类只有一个实例,并提供一个全局访问点来获取该实例 5。该类本身负责强制执行此规则 4。
解决的问题:
- 控制类的实例化,确保系统中某个类只有一个对象被创建 5。
- 为该唯一对象提供一个全局访问点,方便应用程序的不同部分共享该实例 5。
- 适用于管理共享资源(如数据库连接、日志记录器、配置文件)或创建成本较高的对象 8。
结构:
- 一个私有的构造函数,以防止外部代码直接使用
new
操作符创建类的实例 8。 - 一个私有的静态成员变量,用于保存该类的唯一实例 8。
- 一个公有的静态方法(通常命名为
getInstance()
或类似名称),作为获取该唯一实例的全局访问点。此方法会检查实例是否已创建:如果未创建,则创建一个新实例并存储;如果已创建,则返回现有实例 8。
代码段
classDiagram class Singleton { -static instance: Singleton -Singleton() +static getInstance(): Singleton +someBusinessLogic() }
- 一个私有的构造函数,以防止外部代码直接使用
工作原理:客户端代码通过调用
getInstance()
方法来获取单例类的实例。该方法确保了即使有多个请求,也只会返回同一个实例对象。使用场景:
- 日志记录器 (Logging):整个应用程序通常只需要一个日志记录器实例来统一管理日志输出 5。
- 配置文件管理器 (Configuration Settings):应用程序的配置信息通常是全局共享的,适合用单例模式管理 5。
- 数据库连接池 (Database Connections):限制数据库连接的数量并共享连接 5。
- 线程池 (Thread Pools) 5。
Python 代码示例:
Python 中实现单例有多种方式,包括使用元类、装饰器或覆盖 new 方法。以下是使用元类的示例 11:
class SingletonMeta(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] class Logger(metaclass=SingletonMeta): def __init__(self, file_name): # 确保初始化逻辑只在第一次创建时执行 if not hasattr(self, 'file_name'): self.file_name = file_name print(f"Logger instance created for {self.file_name}") def log(self, message): print(f"Logging to {self.file_name}: {message}") # 客户端代码 logger1 = Logger("app.log") logger2 = Logger("another.log") # 即使参数不同,仍返回第一个实例 print(f"logger1 is logger2: {logger1 is logger2}") # 输出: True logger1.log("This is a test message.") logger2.log("This is another test message.") # 仍然写入 app.log # 证明它们是同一个实例 print(f"logger1 file: {logger1.file_name}") # 输出: app.log print(f"logger2 file: {logger2.file_name}") # 输出: app.log
注意:上述 Python 示例中,
__init__
仍然会在每次调用Logger()
时被调用,尽管返回的是同一个实例。为了避免重复初始化,通常需要在__init__
中添加检查,或者将初始化逻辑移到__call__
中仅在创建新实例时执行的部分。在refactoring.guru
的示例中,它指出__init__
参数的更改不影响返回的实例 11。C++ 代码示例:
以下是一个基础的线程不安全的单例实现 12。在多线程环境中,需要额外的同步机制(如互斥锁)来保证线程安全 12。
#include <iostream> #include <string> #include <mutex> // 用于线程安全版本 class Logger { private: static Logger* instance; static std::mutex mtx; // 用于线程安全 std::string fileName; // 私有构造函数 Logger(const std::string& file) : fileName(file) { std::cout << "Logger instance created for " << fileName << std::endl; } public: // 删除拷贝构造函数和赋值操作符 Logger(const Logger&) = delete; Logger& operator=(const Logger&) = delete; // 获取实例的静态方法 (线程安全版本) static Logger* getInstance(const std::string& file) { std::lock_guard<std::mutex> lock(mtx); // 加锁 if (instance == nullptr) { instance = new Logger(file); } return instance; } void log(const std::string& message) { std::cout << "Logging to " << fileName << ": " << message << std::endl; } std::string getFileName() const { return fileName; } }; // 初始化静态成员 Logger* Logger::instance = nullptr; std::mutex Logger::mtx; // 客户端代码 // int main() { // Logger* logger1 = Logger::getInstance("app.log"); // Logger* logger2 = Logger::getInstance("another.log"); // 仍返回第一个实例 // std::cout << "logger1 is logger2: " << (logger1 == logger2) << std::endl; // 输出: 1 (True) // logger1->log("This is a test message."); // logger2->log("This is another test message."); // 仍然写入 app.log // std::cout << "logger1 file: " << logger1->getFileName() << std::endl; // 输出: app.log // std::cout << "logger2 file: " << logger2->getFileName() << std::endl; // 输出: app.log // // 在实际应用中,需要考虑单例的销毁时机 // // delete Logger::instance; // 不推荐直接这样删除,通常由单例类自身管理或在程序结束时处理 // // Logger::instance = nullptr; // return 0; // }
优点:
- 确保类只有一个实例 8。
- 提供对该实例的全局访问点 8。
- 单例对象仅在第一次请求时才被创建(懒汉式加载),可以节省资源 4。
缺点:
- 违反了单一职责原则:单例类既控制其自身的创建,又提供业务逻辑 8。
- 可能掩盖不良设计,例如组件之间过度耦合 8。
- 对单元测试不友好,因为构造函数是私有的,难以模拟或替换单例的依赖 8。
- 在多线程环境下需要特别注意线程安全问题,可能需要加锁,从而引入复杂性和性能开销 4。许多开发者认为单例模式是一种反模式,其使用率正在下降 11。
2. 工厂方法模式 (Factory Method Pattern)
类别:创建型模式 5
一句话比喻:工厂方法模式就像一家多功能饮料工厂,有一个通用的“生产饮料”方法。如果想生产可乐,就让可乐子工厂去实现这个方法;如果想生产橙汁,就让橙汁子工厂去实现。客户只需要告诉工厂想要什么类型的饮料,而不需要知道具体怎么生产 5。
定义:工厂方法模式定义了一个创建对象的接口(工厂方法),但由子类决定要实例化的类是哪一个。工厂方法让类的实例化推迟到子类进行 5。
解决的问题:
- 当一个类不知道它所必须创建的对象的类时。
- 当一个类希望由它的子类来指定它所创建的对象时 15。
- 将对象的创建与使用分离,客户端代码无需关心具体的产品类名,只需关心产品接口和工厂接口 5。
结构:
- Product (产品):定义工厂方法所创建的对象的接口 16。
- ConcreteProduct (具体产品):实现 Product 接口的类 16。
- Creator (创建者):声明工厂方法
factoryMethod()
,该方法返回一个 Product类型的对象。Creator 也可以定义一个工厂方法的默认实现 16。创建者的主要职责通常不是创建产品,而是包含一些依赖于产品对象的业务逻辑 18。 - ConcreteCreator (具体创建者):重写工厂方法以返回一个 ConcreteProduct 的实例 16。
代码段
classDiagram class Product { <<interface>> +operation() } class ConcreteProductA { +operation() } class ConcreteProductB { +operation() } class Creator { <<abstract>> +factoryMethod(): Product +someOperation() } class ConcreteCreatorA { +factoryMethod(): Product } class ConcreteCreatorB { +factoryMethod(): Product } Product <|.. ConcreteProductA Product <|.. ConcreteProductB Creator <|-- ConcreteCreatorA Creator <|-- ConcreteCreatorB Creator..> Product : creates ConcreteCreatorA..> ConcreteProductA : creates ConcreteCreatorB..> ConcreteProductB : creates
工作原理:客户端调用创建者的工厂方法来创建一个产品对象。具体创建哪个产品由具体创建者子类决定。创建者通常包含一些依赖于产品对象的操作,这些操作会调用工厂方法来获取产品,然后使用该产品。
使用场景:
- 当一个类无法预先知道它需要创建的对象的类时。例如,一个框架提供给用户,用户根据自己的需求创建不同类型的对象 15。
- 当你想为你的库或框架的用户提供一种扩展其内部组件的方式时。
- JDBC 中用于创建连接,Spring 框架中用于管理 Bean 17。
Python 代码示例 19:
from abc import ABC, abstractmethod # Product Interface class Document(ABC): @abstractmethod def open(self): pass # Concrete Products class TextDocument(Document): def open(self): return "Opening Text Document..." class PDFDocument(Document): def open(self): return "Opening PDF Document..." # Creator class Application(ABC): @abstractmethod def create_document(self) -> Document: pass def new_document(self): doc = self.create_document() print(doc.open()) print("Document created and opened via factory method.") # Concrete Creators class TextApplication(Application): def create_document(self) -> Document: return TextDocument() class PDFApplication(Application): def create_document(self) -> Document: return PDFDocument() # Client code def client_code(app: Application): app.new_document() print("Launching Text Application:") client_code(TextApplication()) print("\nLaunching PDF Application:") client_code(PDFApplication())
C++ 代码示例 17:
#include <iostream> #include <string> // Product Interface class Document { public: virtual ~Document() {} virtual std::string open() const = 0; }; // Concrete Products class TextDocument : public Document { public: std::string open() const override { return "Opening Text Document..."; } }; class PDFDocument : public Document { public: std::string open() const override { return "Opening PDF Document..."; } }; // Creator class Application { public: virtual ~Application() {} // The factory method virtual Document* createDocument() const = 0; void newDocument() const { Document* doc = this->createDocument(); std::cout << doc->open() << std::endl; std::cout << "Document created and opened via factory method." << std::endl; delete doc; } }; // Concrete Creators class TextApplication : public Application { public: Document* createDocument() const override { return new TextDocument(); } }; class PDFApplication : public Application { public: Document* createDocument() const override { return new PDFDocument(); } }; // Client code // void clientCode(const Application& app) { // app.newDocument(); // } // int main() { // std::cout << "Launching Text Application:" << std::endl; // TextApplication textApp; // clientCode(textApp); // std::cout << "\nLaunching PDF Application:" << std::endl; // PDFApplication pdfApp; // clientCode(pdfApp); // return 0; // }
优点:
- 将产品对象的创建过程封装起来,客户端代码与具体产品解耦 16。
- 易于扩展:增加新的产品时,只需要增加对应的具体产品类和具体工厂类,符合开闭原则 15。
- 创建者类可以独立于具体产品类进行演化。
缺点:
- 每增加一个产品,就需要增加一个具体产品类和一个对应的具体创建者类,导致类的个数成倍增加,增加了系统的复杂度 16。
3. 观察者模式 (Observer Pattern)
类别:行为型模式 5
一句话比喻:观察者模式就像订阅报纸或YouTube频道。当你订阅(成为观察者)后,一旦有新报纸出版或新视频上传(主题状态改变),你就会自动收到通知 4。
定义:观察者模式定义了对象之间一种一对多的依赖关系,当一个对象(称为主题或发布者)的状态发生改变时,所有依赖于它的对象(称为观察者或订阅者)都将得到通知并自动更新 4。
解决的问题:
- 当一个对象的改变需要同时改变其他对象,而不知道具体有多少对象有待改变时 22。
- 当一个对象应该能通知其他对象,而又不应与这些对象形成紧密耦合时 21。
结构:
- Subject (主题/发布者):维护一个观察者列表,并提供用于添加、删除和通知观察者的方法 20。
- Observer (观察者/订阅者):定义一个更新接口,供主题在状态改变时调用 20。
- ConcreteSubject (具体主题):存储状态,并在状态改变时向其观察者发送通知 22。
- ConcreteObserver (具体观察者):实现 Observer 接口,维护与主题状态一致的状态。当收到更新通知时,它会查询主题以获取最新状态并更新自身 22。
代码段
classDiagram class Subject { <<abstract>> #_observers: List~Observer~ +attach(observer: Observer) +detach(observer: Observer) +notify() } class ConcreteSubject { -_state +getState() +setState(state) +attach(observer: Observer) +detach(observer: Observer) +notify() } class Observer { <<interface>> +update(subject: Subject) } class ConcreteObserverA { +update(subject: Subject) } class ConcreteObserverB { +update(subject: Subject) } Subject <|-- ConcreteSubject Observer <|.. ConcreteObserverA Observer <|.. ConcreteObserverB ConcreteSubject o-- Observer : observers
工作原理:具体观察者通过调用具体主题的
attach()
方法将自己注册到主题。当具体主题的状态发生变化时,它会调用notify()
方法,该方法遍历观察者列表并调用每个观察者的update()
方法。观察者在其update()
方法中从主题获取更新后的状态。使用场景:
- 图形用户界面(GUI)组件:当模型数据改变时,多个视图需要更新 4。
- 事件处理系统:例如,按钮点击事件,多个监听器响应 21。
- 邮件列表或新闻订阅服务 4。
- 股票市场应用中,股价变动时通知关注该股票的用户 2。
Python 代码示例 24:
from abc import ABC, abstractmethod from typing import List class Subject(ABC): @abstractmethod def attach(self, observer: 'Observer') -> None: pass @abstractmethod def detach(self, observer: 'Observer') -> None: pass @abstractmethod def notify(self) -> None: pass class WeatherStation(Subject): _observers: List['Observer'] = _temperature: float = 0.0 def attach(self, observer: 'Observer') -> None: print("WeatherStation: Attached an observer.") self._observers.append(observer) def detach(self, observer: 'Observer') -> None: print("WeatherStation: Detached an observer.") self._observers.remove(observer) def notify(self) -> None: print("WeatherStation: Notifying observers...") for observer in self._observers: observer.update(self) def set_temperature(self, temp: float) -> None: print(f"\nWeatherStation: Temperature changed to {temp}°C") self._temperature = temp self.notify() @property def temperature(self) -> float: return self._temperature class Observer(ABC): @abstractmethod def update(self, subject: Subject) -> None: pass class PhoneDisplay(Observer): def update(self, subject: WeatherStation) -> None: if isinstance(subject, WeatherStation): print(f"PhoneDisplay: Temperature is now {subject.temperature}°C") class WindowDisplay(Observer): def update(self, subject: WeatherStation) -> None: if isinstance(subject, WeatherStation): print(f"WindowDisplay: Current temperature: {subject.temperature}°C") # Client code station = WeatherStation() phone_display = PhoneDisplay() window_display = WindowDisplay() station.attach(phone_display) station.attach(window_display) station.set_temperature(25.5) station.set_temperature(27.0) station.detach(phone_display) station.set_temperature(22.0)
C++ 代码示例 23:
#include <iostream> #include <vector> #include <string> #include <algorithm> // For std::remove class Observer; // Forward declaration class Subject { public: virtual ~Subject() {} virtual void attach(Observer* observer) = 0; virtual void detach(Observer* observer) = 0; virtual void notify() = 0; virtual float getTemperature() const = 0; // Added for observers to get state }; class Observer { public: virtual ~Observer() {} virtual void update(Subject* subject) = 0; }; class WeatherStation : public Subject { private: std::vector<Observer*> observers; float temperature; public: WeatherStation() : temperature(0.0f) {} void attach(Observer* observer) override { std::cout << "WeatherStation: Attached an observer." << std::endl; observers.push_back(observer); } void detach(Observer* observer) override { observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end()); std::cout << "WeatherStation: Detached an observer." << std::endl; } void notify() override { std::cout << "WeatherStation: Notifying observers..." << std::endl; for (Observer* observer : observers) { observer->update(this); } } void setTemperature(float temp) { std::cout << "\nWeatherStation: Temperature changed to " << temp << "°C" << std::endl; this->temperature = temp; notify(); } float getTemperature() const override { return temperature; } }; class PhoneDisplay : public Observer { public: void update(Subject* subject) override { if (WeatherStation* station = dynamic_cast<WeatherStation*>(subject)) { std::cout << "PhoneDisplay: Temperature is now " << station->getTemperature() << "°C" << std::endl; } } }; class WindowDisplay : public Observer { public: void update(Subject* subject) override { if (WeatherStation* station = dynamic_cast<WeatherStation*>(subject)) { std::cout << "WindowDisplay: Current temperature: " << station->getTemperature() << "°C" << std::endl; } } }; // int main() { // WeatherStation station; // PhoneDisplay phoneDisplay; // WindowDisplay windowDisplay; // station.attach(&phoneDisplay); // station.attach(&windowDisplay); // station.setTemperature(25.5f); // station.setTemperature(27.0f); // station.detach(&phoneDisplay); // station.setTemperature(22.0f); // return 0; // }
优点:
- 主题和观察者之间是松散耦合的:主题只知道观察者实现了某个接口,观察者可以独立改变,主题也可以独立改变 20。
- 支持广播通信:一个通知可以发送给任意数量的观察者 20。
- 可以在运行时动态添加和删除观察者 22。
缺点:
- 如果存在很多观察者,通知所有观察者可能会比较耗时,影响性能 20。
- 观察者不知道其他观察者的存在,可能导致意外的连锁更新 20。
- 如果主题和观察者之间存在循环依赖,可能会导致系统崩溃。
- 内存泄漏风险:如果观察者没有正确地注销,可能会导致内存泄漏 20。
4. 策略模式 (Strategy Pattern)
类别:行为型模式 5
一句话比喻:策略模式就像选择不同的出行方式去机场。你可以选择打车、坐公交或乘地铁,每种方式都是一种策略。你可以根据时间、费用或偏好在运行时选择或切换策略 5。
定义:策略模式定义了一系列算法,并将每一个算法封装起来,使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户 5。
解决的问题:
- 当你想在运行时根据不同情况选择不同的算法或行为时 26。
- 当你有许多相关的类,它们之间的区别仅在于它们的行为(算法)时,策略模式可以帮助你避免使用大量的条件语句 26。
- 将算法的实现细节与使用算法的类隔离开 26。
结构:
- Strategy (策略接口/抽象类):定义所有支持的算法的公共接口 26。Context 使用这个接口来调用由 ConcreteStrategy 定义的算法。
- ConcreteStrategy (具体策略类):实现 Strategy 接口,封装了具体的算法或行为 26。
- Context (上下文):持有一个对 Strategy 对象的引用。它不直接执行算法,而是调用配置好的 Strategy 对象的相应方法 26。Context 通常提供一个方法来让客户端设置或替换策略对象。
代码段
classDiagram class Context { -strategy: Strategy +Context(strategy: Strategy) +setStrategy(strategy: Strategy) +executeStrategy(data: any) } class Strategy { <<interface>> +execute(data: any): any } class ConcreteStrategyA { +execute(data: any): any } class ConcreteStrategyB { +execute(data: any): any } Context o-- Strategy Strategy <|.. ConcreteStrategyA Strategy <|.. ConcreteStrategyB
工作原理:Context 对象在其内部维护一个对 Strategy 对象的引用。当 Context 需要执行某个算法时,它会调用 Strategy 对象的接口方法。客户端可以根据需要向 Context 提供不同的 Strategy 对象,从而改变 Context 的行为。
使用场景:
- 多种排序算法的选择(如快速排序、冒泡排序)5。
- 支付系统中的多种支付方式(如信用卡支付、PayPal 支付)21。
- 文件压缩,可以选择不同的压缩算法 21。
- 表单验证,根据不同的验证规则进行验证 27。
Python 代码示例 30:
from abc import ABC, abstractmethod from typing import List, Any # Strategy Interface class PaymentStrategy(ABC): @abstractmethod def pay(self, amount: float) -> None: pass # Concrete Strategies class CreditCardPayment(PaymentStrategy): def __init__(self, card_number: str): self.card_number = card_number def pay(self, amount: float) -> None: print(f"Paying ${amount:.2f} using Credit Card {self.card_number[-4:]}") # Show last 4 digits class PayPalPayment(PaymentStrategy): def __init__(self, email: str): self.email = email def pay(self, amount: float) -> None: print(f"Paying ${amount:.2f} using PayPal account {self.email}") # Context class ShoppingCart: def __init__(self, payment_strategy: PaymentStrategy): self._payment_strategy = payment_strategy @property def payment_strategy(self) -> PaymentStrategy: return self._payment_strategy @payment_strategy.setter def payment_strategy(self, strategy: PaymentStrategy) -> None: self._payment_strategy = strategy def checkout(self, amount: float) -> None: print("Processing payment...") self._payment_strategy.pay(amount) print("Payment successful.") # Client code cart1 = ShoppingCart(CreditCardPayment("1234-5678-9012-3456")) cart1.checkout(100.50) print("\nSwitching payment method...") cart1.payment_strategy = PayPalPayment("user@example.com") cart1.checkout(75.20)
C++ 代码示例 28:
#include <iostream> #include <string> #include <vector> #include <algorithm> // For std::sort in a simplified example #include <memory> // For std::unique_ptr // Strategy Interface class SortStrategy { public: virtual ~SortStrategy() = default; virtual void sort(std::vector<int>& data) const = 0; }; // Concrete Strategies class AscendingSort : public SortStrategy { public: void sort(std::vector<int>& data) const override { std::cout << "Sorting in ascending order." << std::endl; std::sort(data.begin(), data.end()); } }; class DescendingSort : public SortStrategy { public: void sort(std::vector<int>& data) const override { std::cout << "Sorting in descending order." << std::endl; std::sort(data.begin(), data.end(), std::greater<int>()); } }; // Context class DataSorter { private: std::unique_ptr<SortStrategy> strategy_; public: explicit DataSorter(std::unique_ptr<SortStrategy> strategy) : strategy_(std::move(strategy)) {} void set_strategy(std::unique_ptr<SortStrategy> strategy) { strategy_ = std::move(strategy); } void sortData(std::vector<int>& data) const { if (strategy_) { strategy_->sort(data); std::cout << "Sorted data: "; for (int val : data) { std::cout << val << " "; } std::cout << std::endl; } else { std::cout << "No sorting strategy set." << std::endl; } } }; // Client code // void clientCode() { // std::vector<int> numbers = {5, 1, 4, 2, 8}; // std::vector<int> numbers_copy; // std::cout << "Client: Using Ascending Sort." << std::endl; // numbers_copy = numbers; // Create a copy for sorting // DataSorter sorter(std::make_unique<AscendingSort>()); // sorter.sortData(numbers_copy); // std::cout << "\nClient: Using Descending Sort." << std::endl; // numbers_copy = numbers; // Create another copy // sorter.set_strategy(std::make_unique<DescendingSort>()); // sorter.sortData(numbers_copy); // } // int main() { // clientCode(); // return 0; // }
优点:
- 开闭原则:可以在不修改现有 Context 代码的情况下引入新的策略 26。
- 将算法的定义、实现和使用分离开来,提高了代码的灵活性和可维护性 26。
- 避免了在 Context 中使用冗长的条件判断语句来选择算法 26。
- 策略类可以被不同的 Context 复用。
缺点:
- 客户端必须了解所有的策略,并自行决定使用哪一个策略 26。
- 会增加系统中类的数量,每个具体策略都是一个类 26。
- 如果策略仅在少数情况下有所不同,则可能会导致不必要的开销。
5. 适配器模式 (Adapter Pattern)
类别:结构型模式 5
一句话比喻:适配器模式就像一个电源转换插头。你的笔记本电脑充电器可能是一个两孔插头(旧接口),但墙上的插座是三孔的(新接口)。电源转换插头(适配器)允许你的两孔插头插入三孔插座并正常工作,它不改变充电器或插座的功能,只是让它们兼容 4。
定义:适配器模式允许将一个类的接口转换成客户端期望的另一个接口。适配器使原本由于接口不兼容而不能一起工作的那些类可以一起工作 4。它也被称为包装器 (Wrapper) 4。
解决的问题:
- 如何重用一个接口与客户端期望不符的现有类?32
- 如何让接口不兼容的类协同工作?32
- 如何为一个类提供一个可选的接口?32
结构:适配器模式主要有两种实现方式:对象适配器和类适配器。
- 对象适配器 (Object Adapter):通过组合实现。适配器持有一个被适配者 (Adaptee) 的实例,并实现目标接口 (Target)。
- Target (目标接口):客户端期望使用的接口 31。
- Adaptee (被适配者):一个已存在的类,但其接口与 Target 接口不兼容 31。
- Adapter (适配器):实现 Target 接口,并包含一个 Adaptee 的实例。Adapter 将对 Target 接口的调用转换为对 Adaptee 相应方法的调用 31。
- Client (客户端):通过 Target 接口与 Adapter 交互 4。
- 类适配器 (Class Adapter):通过多重继承(如果语言支持)实现。Adapter 同时继承 Target 接口(或类)和 Adaptee 类。
- 这种方式在 Python 和 C++ 中可以通过多重继承实现,但在 Java 中不可行(Java 不支持类的多重继承,但可以实现多个接口)。
对象适配器 Mermaid 图示:
代码段
classDiagram class Client { +operation(target: Target) } class Target { <<interface>> +request() } class Adaptee { +specificRequest() } class Adapter { -adaptee: Adaptee +Adapter(adaptee: Adaptee) +request() } Client..> Target : uses Target <|.. Adapter : implements Adapter o-- Adaptee : wraps
- 对象适配器 (Object Adapter):通过组合实现。适配器持有一个被适配者 (Adaptee) 的实例,并实现目标接口 (Target)。
工作原理:
- 对象适配器:客户端调用适配器的
request()
方法(Target 接口定义的方法)。适配器内部将这个调用委托给其包含的 Adaptee 实例的specificRequest()
方法,可能在调用前后进行数据转换。 - 类适配器:客户端调用适配器的
request()
方法。由于适配器继承了 Adaptee,它可以直接调用 Adaptee 的specificRequest()
方法。
- 对象适配器:客户端调用适配器的
使用场景:
- 当你希望使用某个已存在的类,但其接口与你的需求不匹配时 31。
- 当你希望创建一个可复用的类,该类可以与一些不相关的拥有不兼容接口的类协同工作时。
- 集成第三方库或遗留代码,这些库或代码的接口与当前系统不兼容 4。
Python 代码示例 (对象适配器) 33:
# Adaptee (Old system with a different interface) class OldLogger: def log_message(self, severity: int, text: str) -> None: print(f": {text}") # Target Interface (What the client expects) class NewLoggerInterface: def info(self, message: str) -> None: pass def warning(self, message: str) -> None: pass def error(self, message: str) -> None: pass # Adapter class LoggerAdapter(NewLoggerInterface): def __init__(self, old_logger: OldLogger): self._old_logger = old_logger def info(self, message: str) -> None: self._old_logger.log_message(1, f"INFO: {message}") # Mapping to old severity def warning(self, message: str) -> None: self._old_logger.log_message(2, f"WARNING: {message}") def error(self, message: str) -> None: self._old_logger.log_message(3, f"ERROR: {message}") # Client code def client_code(logger: NewLoggerInterface): logger.info("This is an informational message.") logger.warning("This is a warning message.") logger.error("This is an error message.") old_system_logger = OldLogger() print("Using the adapter for the old logger:") adapter = LoggerAdapter(old_system_logger) client_code(adapter) # Example of a new logger that already conforms to the interface class ModernLogger(NewLoggerInterface): def info(self, message: str) -> None: print(f"[ModernLogger INFO]: {message}") def warning(self, message: str) -> None: print(f": {message}") def error(self, message: str) -> None: print(f": {message}") print("\nUsing a modern logger directly:") modern_logger = ModernLogger() client_code(modern_logger)
C++ 代码示例 (对象适配器) 35:
#include <iostream> #include <string> #include <memory> // For std::unique_ptr // Adaptee (Old system with a different interface) class EuropeanSocket { public: void specificPlug() const { std::cout << "European socket providing power (specific plug)." << std::endl; } }; // Target Interface (What the client expects) class USASocketInterface { public: virtual ~USASocketInterface() = default; virtual void requestPower() const = 0; }; // Adapter class SocketAdapter : public USASocketInterface { private: std::unique_ptr<EuropeanSocket> adaptee_; // Using smart pointer for ownership public: SocketAdapter(std::unique_ptr<EuropeanSocket> adaptee) : adaptee_(std::move(adaptee)) {} void requestPower() const override { std::cout << "Adapter: Converting USA request to European specific plug." << std::endl; if (adaptee_) { adaptee_->specificPlug(); } else { std::cout << "Adapter: No European socket connected." << std::endl; } } }; // Client code // void clientUsesUSASocket(const USASocketInterface& socket) { // socket.requestPower(); // } // int main() { // auto europeanPlug = std::make_unique<EuropeanSocket>(); // SocketAdapter adapter(std::move(europeanPlug)); // std::cout << "Client: Trying to use European plug via adapter in USA socket." << std::endl; // clientUsesUSASocket(adapter); // return 0; // }
优点:
- 单一职责原则:可以将接口转换代码从主要的业务逻辑中分离出来 33。
- 开闭原则:可以在不修改现有客户端代码的情况下引入新的适配器类型 33。
- 提高了类的复用性:使得原本不兼容的类可以协同工作 32。
缺点:
- 增加了代码的整体复杂度,因为需要引入新的接口和类 33。
- 有时,直接修改服务类的接口使其与代码库的其他部分匹配可能更简单(如果可以控制服务类的话)34。
6. 外观模式 (Facade Pattern)
类别:结构型模式 5
一句话比喻:外观模式就像餐厅里的服务员。你(客户)只需要告诉服务员你要点什么菜,服务员会负责与厨房、收银台等多个复杂子系统打交道,最后把菜品和账单给你。你不需要知道后厨是怎么运作的,服务员为你提供了一个简化的接口 5。
定义:外观模式为复杂子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用 5。
解决的问题:
- 当需要为一个复杂子系统提供一个简单接口时,以减少客户端与子系统众多对象之间的依赖关系 39。
- 当你想将子系统组织成层时,外观可以作为每一层的入口点。
- 当子系统的抽象和实现紧密耦合时 39。
结构:
- Facade (外观类):知道哪些子系统类负责处理请求,并将客户端的请求代理给适当的子系统对象 37。它通常封装了与子系统交互的复杂逻辑。
- Subsystem classes (子系统类):实现子系统的功能,处理由 Facade 对象指派的任务。它们不了解 Facade 的存在,即它们可以在没有 Facade 的情况下工作 37。
- Client (客户端):使用 Facade 来与子系统交互,而不是直接与子系统中的众多类交互 37。
代码段
classDiagram class Client class OrderFacade { -inventorySystem: InventorySystem -paymentSystem: PaymentSystem -shippingSystem: ShippingSystem +placeOrder(productId: string, quantity: int, paymentDetails: string, shippingAddress: string) } class InventorySystem { +checkStock(productId: string, quantity: int): bool +updateStock(productId: string, quantity: int) } class PaymentSystem { +processPayment(amount: float, paymentDetails: string): bool } class ShippingSystem { +arrangeShipping(productId: string, quantity: int, shippingAddress: string) } Client..> OrderFacade : uses OrderFacade o-- InventorySystem OrderFacade o-- PaymentSystem OrderFacade o-- ShippingSystem
工作原理:客户端向 Facade 发送请求。Facade 将这些请求转发给子系统中的相应对象。Facade 可能还会执行一些额外的工作,例如初始化子系统或协调子系统对象之间的交互。客户端不知道子系统的复杂性。
使用场景:
- 为一个复杂的子系统提供一个简单的接口,例如一个包含多个类的库 37。
- 当你希望将客户端代码与子系统的内部组件解耦时,以便更灵活地开发子系统 41。
- 当你需要构建一个分层系统时,可以使用外观模式为每个层级定义入口。
- 在线购物应用中,
OrderFacade
可以简化下订单流程,该流程可能涉及库存检查、支付处理和安排配送等多个子系统操作 36。
Python 代码示例 38:
# Subsystem components class CPU: def freeze(self): print("CPU: Freezing...") def jump(self, position): print(f"CPU: Jumping to {position}...") def execute(self): print("CPU: Executing...") class Memory: def load(self, position, data): print(f"Memory: Loading data '{data}' to {position}...") class HardDrive: def read(self, lba, size): print(f"HardDrive: Reading {size} sectors from LBA {lba}...") return "boot_data" # Facade class ComputerFacade: def __init__(self): self._cpu = CPU() self._memory = Memory() self._hard_drive = HardDrive() def start(self): print("ComputerFacade: Starting computer...") self._cpu.freeze() boot_address = "0x00" boot_data = self._hard_drive.read(lba=0, size=1024) self._memory.load(boot_address, boot_data) self._cpu.jump(boot_address) self._cpu.execute() print("ComputerFacade: Computer started successfully.") # Client code computer = ComputerFacade() computer.start()
C++ 代码示例 37:
#include <iostream> #include <string> #include <memory> // For std::unique_ptr // Subsystem components class Amplifier { public: void on() { std::cout << "Amplifier: On" << std::endl; } void off() { std::cout << "Amplifier: Off" << std::endl; } void setVolume(int level) { std::cout << "Amplifier: Setting volume to " << level << std::endl; } }; class DvdPlayer { public: void on() { std::cout << "DVD Player: On" << std::endl; } void off() { std::cout << "DVD Player: Off" << std::endl; } void play(const std::string& movie) { std::cout << "DVD Player: Playing movie '" << movie << "'" << std::endl; } }; class Projector { public: void on() { std::cout << "Projector: On" << std::endl; } void off() { std::cout << "Projector: Off" << std::endl; } void wideScreenMode() { std::cout << "Projector: Setting to wide screen mode" << std::endl; } }; // Facade class HomeTheaterFacade { private: std::unique_ptr<Amplifier> amp_; std::unique_ptr<DvdPlayer> dvd_; std::unique_ptr<Projector> projector_; public: HomeTheaterFacade(std::unique_ptr<Amplifier> amp, std::unique_ptr<DvdPlayer> dvd, std::unique_ptr<Projector> projector) : amp_(std::move(amp)), dvd_(std::move(dvd)), projector_(std::move(projector)) {} void watchMovie(const std::string& movie) { std::cout << "Get ready to watch a movie..." << std::endl; projector_->on(); projector_->wideScreenMode(); amp_->on(); amp_->setVolume(5); dvd_->on(); dvd_->play(movie); } void endMovie() { std::cout << "\nShutting movie theater down..." << std::endl; dvd_->off(); amp_->off(); projector_->off(); } }; // Client code // int main() { // auto amp = std::make_unique<Amplifier>(); // auto dvd = std::make_unique<DvdPlayer>(); // auto projector = std::make_unique<Projector>(); // HomeTheaterFacade homeTheater(std::move(amp), std::move(dvd), std::move(projector)); // homeTheater.watchMovie("Raiders of the Lost Ark"); // homeTheater.endMovie(); // return 0; // }
优点:
- 将客户端代码与复杂的子系统类解耦,降低了客户端的复杂性 37。
- 提供了一个更简单、更易于使用的接口 5。
- 提高了子系统的可维护性和可扩展性,因为子系统的变化不会直接影响到客户端(只要外观接口不变)41。
缺点:
- 外观类本身可能成为一个“上帝对象”,即一个知道太多或负责太多的类,如果它与所有子系统类都耦合,就违反了单一职责原则 37。
- 外观提供的简化接口可能无法满足所有客户端的需求,某些高级功能可能仍然需要直接访问子系统 41。
如何选择和应用设计模式
选择和应用设计模式并非总是直截了当的。它通常涉及以下几个步骤 7:
- 识别问题 (Identify the problem):首先要清晰地理解你试图解决的设计问题。代码中是否存在难以管理的依赖关系?是否需要更灵活地替换行为或算法?组件之间是否难以保持同步?7。准确识别问题是选择正确模式的关键。
- 理解现有架构 (Understand your existing architecture):考虑当前系统的组件如何交互,存在哪些约束(例如,遗留系统、特定框架的限制),以及架构的灵活性如何 7。这有助于选择能够平滑集成到现有设置中的模式。
- 选择合适的设计模式 (Choose the right Design Pattern):根据问题的性质和现有架构的特点,选择最能解决问题的设计模式。参考模式的意图、适用场景、优点和缺点。
- 实现具体类 (Implement concrete classes):将设计模式的抽象结构转化为代码中的具体类和接口 7。在编写这些类时,应注重模块化和可重用性,以便将来轻松扩展系统 7。
- 集成模式到架构中 (Integrate the pattern into your architecture):将新实现的模式组件集成到现有系统中。
- 测试集成 (Test the integration):进行彻底的测试,包括功能测试、集成测试和性能测试,以确保模式按预期工作并且不会对系统产生负面影响 7。例如,如果集成了命令模式,应单独测试每个命令,并检查撤销/重做功能是否正常工作 7。
- 优化和重构 (Optimize and refactor further if needed):根据测试结果和实际运行情况,进行必要的优化和代码重构。
- 文档化变更 (Document the changes):记录所做的设计决策和模式实现,方便团队其他成员理解和未来的维护。
- 监控和维护 (Monitor and maintain):持续监控模式在实际应用中的表现,并根据需要进行维护。
设计模式并非万能药,不应过度使用。错误地应用模式或在不必要的情况下使用模式,反而会增加代码的复杂性。
结论与进一步学习
本教程介绍了六种核心的软件设计模式,旨在为初学者提供一个比传统教材更快速的入门途径。通过类比、定义、问题解析、结构图示、代码示例以及优缺点分析,力求清晰地阐述每个模式的精髓。这些模式——单例、工厂方法、观察者、策略、适配器和外观——代表了创建型、结构型和行为型模式中的常用解决方案,能够帮助开发者编写出更灵活、可维护和可重用的代码 2。
然而,掌握设计模式是一个持续学习和实践的过程。本教程所涵盖的仅是众多设计模式中的一小部分 5。随着经验的积累,开发者会遇到更复杂的设计挑战,需要学习和应用更多高级或特定领域的模式。因此,将本教程视为一个起点而非终点至关重要。
更进一步,有效运用设计模式的前提是能够准确识别当前面临的设计问题类型 7。每个设计模式都有其特定的“意图”或旨在解决的“问题”。例如,当你发现系统中多个部分需要对某个对象的状态变化做出响应时,就应该联想到观察者模式。当需要让一个已有的类与系统其他部分协同工作,但其接口不兼容时,适配器模式可能就是答案。这种识别问题的能力,以及将问题与特定模式的“意图”联系起来的技巧,是通过不断实践、反思和研究他人代码逐渐培养起来的。因此,在学习了这些模式的“是什么”和“怎么做”之后,更重要的是在实际项目中练习“何时用”和“为何用”。
建议初学者在后续的学习和工作中,积极尝试在小型项目或代码练习中应用这些模式,并阅读更多关于其他设计模式的资料,以及优秀开源项目中设计模式的实际应用案例,从而深化理解,提升设计能力。
推荐资源
- Refactoring.Guru (https://refactoring.guru/): 提供清晰的设计模式解释、示例代码(包括 Python 和 C++)以及相关的重构技巧。本教程中的许多代码示例和结构解释参考了此网站。
- GeeksforGeeks - Software Design Patterns (Software Design Patterns Tutorial | GeeksforGeeks): 提供了多种设计模式的概述和代码示例。
- 《Design Patterns: Elements of Reusable Object-Oriented Software》 (GoF Book): 设计模式领域的经典著作,虽然对初学者可能较深入,但对于希望深入理解设计模式的开发者来说是必读之作。
- 在线教程和视频: 诸如 Code With Mosh 44 或 kudvenkat 的 YouTube 播放列表 45 等资源也提供了不同风格的设计模式教学。