面向对象设计原则(solid)

单一职责原则SRP

每个类应该仅有一个职责,换句话说,类应该只做一件事。如果一个类承担了多个责任,未来修改时可能会影响到多个地方,从而引入复杂性和难以维护的问题。

高内聚,低耦合:类与类之间应具有明显的边界,类内的方法间应具有高度相关

几种有问题的情形:

  1. 如果类名包含“and”和”or“等连接词
class JournalAndPersistenceManager{
    void add_entry(std::string);
    void save();
    
};
class UserManagerAndLogger{
    void createUser();
    void logMessage(String);
};
  1. 类中有多个不相关的方法
class Employee {
private:
    std::String name;
    double salary;
public:
    Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
    // 职责1:计算工资
    double calculatePay() {
        return salary * 1.0;
    }
    
    // 职责2:数据库操作
    void saveToDatabase() {}
    
    // 职责3:报告生成
    std::string generateReport() {
        return "员工报告:" + name + ", 工资:" + salary;
    }
  1. 修改某个功能时需要修改多个不相关的部分

如果修改数据库存储方式时,需要同时修改工资计算逻辑,那么这些功能可能被错误地放在了同一个类中。

下面是日志和保存的实现

struct Journal
{
 std::string title;
 std::vector<std::string> entries;

 explicit Journal(std::string title)
  : title{std::move(title)}
 {}

 void add(std::string entry)
 {
  entries.emplace_back(std::move(entry));
 }
};

struct PersistenceManager
{
 static void save(const Journal& j, const std::string& filename)
 {
  std::ofstream out(filename);
  out << j.title;
  for(const auto& e : j.entries)
  {
   out << e << std::endl;
  }
 }
};

开放封闭原则OCP

软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。也就是说,当需求变化时,可以通过增加新功能(例如,派生类)来扩展现有功能,而不是修改现有代码。

通过扩展现有代码,我们可以增加新的功能或行为,而不会影响到已经稳定运行的代码。

以下BadProductFilter类是不好的实现

#include <iostream>
#include <iterator>
#include <ostream>
#include <string>
#include <vector>
#include <algorithm>


enum class Color
{
 Red,
 Green,
 Blue,
};

std::string to_string(const Color& color)
{
 switch(color)
 {
  case Color::Red:
   return "Red";
  case Color::Green:
   return "Green";
  case Color::Blue:
   return "Blue";
  default:
   return "Undefined";
 }
}

enum class Size
{
 Small,
 Medium,
 Large,
};

std::string to_string(const Size& size)
{
 switch(size)
 {
  case Size::Small:
   return "Small";
  case Size::Medium:
   return "Medium";
  case Size::Large:
   return "Large";
  default:
   return "Undefined";
 }
}

struct Product
{
 std::string name;
 Color color;
 Size size;

 Product(std::string name, Color color, Size size)
  : name{std::move(name)},
   color{std::move(color)},
   size{std::move(size)}
 {}

 friend std::ostream& operator<<(std::ostream& out, const Product& product);
};

std::ostream& operator<<(std::ostream& out, const Product& product)
{
 return out << to_string(product.size) << " " << to_string(product.color) << " " << product.name;
}

/* Open for modification not extension */
struct BadProductFilter
{
 using Items = std::vector<Product*>;

 static Items by_color(const Items& items, const Color& color)
 {
  Items ret;
  std::copy_if(items.cbegin(), items.cend(), std::back_inserter(ret), [&color] (const auto* e) { return e->color == color; });
  return ret;
 };

 static Items by_size(const Items& items, const Size& size)
 {
  Items ret;
  std::copy_if(items.cbegin(), items.cend(), std::back_inserter(ret), [&size] (const auto* e) { return e->size == size; });
  return ret;
 };

 static Items by_color_and_size(const Items& items, const Color& color, const Size& size)
 {
  Items ret;
  std::copy_if(items.cbegin(), items.cend(), std::back_inserter(ret),
     [&color, &size] (const auto* e)
     {
      return e->color == color && e->size == size;
     });
  return ret;
 };
};

/* Good way */
template <typename T>
struct ISpecification
{
 virtual ~ISpecification() = default;
 virtual bool is_satisfied(const T*) const = 0;
};

template <typename T>
struct IFilter
{
 virtual ~IFilter() = default;
 virtual std::vector<T*> filter(const std::vector<T*>&, const ISpecification<T>&) const = 0;
};

struct GoodFilter : public IFilter<Product>
{
 using Items = std::vector<Product*>;
 Items filter(const Items& items, const ISpecification<Product>& spec) const override
 {
  Items result;
  std::copy_if(items.cbegin(), items.cend(), std::back_inserter(result),
     [&spec] (const auto& e)
     {
      return spec.is_satisfied(e);
     });
  return result;
 }
};

struct ColorSpecification : public ISpecification<Product>
{
 Color color;

 explicit ColorSpecification(Color color)
  : color{std::move(color)}
 {}

 bool is_satisfied(const Product* prod) const override
 {
  return prod->color == color;
 }
};

struct SizeSpecification : public ISpecification<Product>
{
 Size size;

 explicit SizeSpecification(Size size)
  : size{std::move(size)}
 {}

 bool is_satisfied(const Product* prod) const override
 {
  return prod->size == size;
 }
};

template <typename T>
struct CombinedSpecification : public ISpecification<T>
{
 ISpecification<T>& first;
 ISpecification<T>& second;

 explicit CombinedSpecification(ISpecification<T>& first, ISpecification<T>& second)
  : first{first},
   second{second}
 {}

 bool is_satisfied(const T* item) const override
 {
  return first.is_satisfied(item) && second.is_satisfied(item);
 }
};


int main()
{
 Product apple("Apple", Color::Green, Size::Small);
 Product tree("Tree", Color::Green, Size::Large);
 Product pool("Pool", Color::Blue, Size::Large);

 std::vector<Product*> all_products{&apple, &tree, &pool};

 GoodFilter gf;
 ColorSpecification green_spec(Color::Green);
 auto green_things{gf.filter(all_products, green_spec)};
 for(const auto* e : green_things)
 {
  std::cout << *e << std::endl;
 }

 return 0;
}

里氏替换原则LSP

子类必须能够替代其基类的位置,并且不改变程序的正确性。换句话说,使用基类类型的地方,应该能够使用子类类型而不引入错误。子类在继承父类时,应该保持与父类一致的行为。

这意味着:

  • 子类应该完全实现父类的抽象方法
  • 子类可以有自己的个性,但不能改变父类的行为
  • 子类不应该抛出父类方法没有声明的异常
  • 子类的前置条件不应该比父类更严格
  • 子类的后置条件不应该比父类更宽松

以下案例是错的

1.子类抛出父类方法没有声明的异常

class fileProcessor
{
public:
 virtual void processFile(std::string fileName) {
        std::cout << "Processing file " << fileName << std::endl;
    }
};

class textFileProcessor : public fileProcessor
{
public:
 void processFile(std::string fileName) override {
        throw std::runtime_error("textFileProcessor cannot process file " + fileName);
    }
};

2.子类的前置条件比父类更严格


class textFileProcessor : public fileProcessor
{
public:
 void processFile(std::string fileName) override {
         if (fileName.size()>10) { 
            return;
        }
        std::cout << "Processing text file " << fileName << std::endl;
    }
};

3.子类的后置条件比父类更宽松

class BankAccount {
public:
    BankAccount(double balance) : balance(balance) {}

    virtual void deposit(double amount) {
        // 存款时更新余额
        balance += amount;
    }

    double getBalance() const { return balance; }

private:
    double balance;
};

class LimitedBankAccount : public BankAccount {
public:
    LimitedBankAccount(double balance, double limit)
        : BankAccount(balance), limit(limit) {}

    void deposit(double amount) override {
        // 如果存款金额超过了限制,就不进行存款
        if (amount > limit) {
            std::cerr << "Deposit limit exceeded!" << std::endl;
        } else {
            BankAccount::deposit(amount);
        }
    }

private:
    double limit;
};

接口隔离原则ISP

不应该强迫客户端依赖于它不需要的接口。接口应该小而精,客户端只需要依赖它实际用到的方法。这个原则帮助避免冗余的接口和方法,提高代码的灵活性和可维护性。

比如打印机没有扫描功能,就不应该加上扫描的实现,可以通过组合的形式,而非继承多态。

以下是错的


struct MFP : public IBadMachine
{
 void print(const Document&) override
 {}

 void scan(const Documents&) override
 {}

 void fax(const Documents&) override
 {}
};

正确的:

struct IPrinter
{
 virtual ~IPrinter() = default;
 virtual void print(const std::vector<Document*>&) = 0;
};

struct IScanner
{
 virtual ~IScanner() = default;
 virtual void scan(const std::vector<Document*>&) = 0;
};

struct Printer : public IPrinter
{
 void print(const std::vector<Document*>&) override
 {}
};

struct IMachine : public IPrinter, public IScanner
{};

struct Machine : public IMachine
{
 IPrinter& printer;
 IScanner& scanner;

 explicit Machine(IPrinter& printer, IScanner& scanner)
  : printer{printer},
   scanner{scanner}
 {}

 void print(const std::vector<Document*>& docs) override
 {
  printer.print(docs);
 }

 void scan(const std::vector<Document*>& docs) override
 {
  scanner.scan(docs);
 }
};

依赖倒置原则DIP

高层模块不应该依赖低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。通过依赖注入和使用接口或抽象类,来降低高层模块和低层模块之间的耦合性。

#include <iostream>
#include <memory>
using namespace std;

// 抽象通知接口
class INotificationService {
public:
    virtual void sendNotification(const string& message) = 0;
    virtual ~INotificationService() = default;
};

// 低层模块:邮件通知服务
class EmailService : public INotificationService {
public:
    void sendNotification(const string& message) override {
        cout << "Sending email: " << message << endl;
    }
};

// 低层模块:短信通知服务
class SMSService : public INotificationService {
public:
    void sendNotification(const string& message) override {
        cout << "Sending SMS: " << message << endl;
    }
};

// 高层模块:用户通知服务
class UserNotification {
private:
    unique_ptr<INotificationService> notificationService;
    
public:
    UserNotification(unique_ptr<INotificationService> service)
        : notificationService(move(service)) {}

    void notifyUser(const string& message) {
        notificationService->sendNotification(message);
    }
};

// 客户端代码
int main() {
    // 使用邮件通知服务
    unique_ptr<INotificationService> emailService = make_unique<EmailService>();
    UserNotification userNotification(move(emailService));
    userNotification.notifyUser("Your order has been shipped!");

    // 使用短信通知服务
    unique_ptr<INotificationService> smsService = make_unique<SMSService>();
    UserNotification userNotificationSMS(move(smsService));
    userNotificationSMS.notifyUser("Your verification code is 1234.");

    return 0;
}

其他

  1. 优先使用对象组合,而不是类继承
  • 类继承通常为“白箱复用”,对象组合通常为“黑箱复用” 。
  • 继承在某种程度上破坏了封装性,子类父类耦合度高。
  • 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
  1. 封装变化点
  • 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
  1. 针对接口编程,而不是针对实现编程
  • 不将变量类型声明为某个特定的具体类,而是声明为某个接口。
  • 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
  • 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。

对象创建

建造者模式

是一种创建型设计模式,允许一步一步地构建复杂对象。它通过分解构建过程,使得对象的构建与其表示分离,从而可以创建不同类型的对象。建造者模式通常用于创建具有复杂结构的对象,并且这个过程需要分步骤完成。

特点:

  1. 分离对象的创建和表示:建造者模式将对象的创建过程与表示(即对象的具体内容)分开。构建过程与对象的表示是独立的。
  2. 控制复杂对象的创建过程:通过建造者模式,客户端可以通过不同的建造者来创建复杂的对象,而不需要关心每个步骤的细节。
  3. 灵活的对象创建:通过多个不同的建造者,可以创建不同的对象表示,客户端只需要选择合适的建造者。

静态工厂方式

静态工厂方法本质上是一种静态方法的应用,用来代替构造函数来创建对象。它可以作为创建对象的一种简化方式,不一定像简单工厂模式那样需要一个额外的工厂类。静态工厂方法可以通过类本身来提供对象的创建,而不需要显式的实例化。

特点:

  • 静态方法:通常是静态方法,不需要实例化类即可调用。
  • 可以返回不同类型的对象:可以根据不同的条件,返回不同类型的对象(比如不同的子类实例)。
  • 没有工厂类的概念:它通常是类本身的一部分,因此没有额外的工厂类存在。
  • 灵活性:可以更灵活地管理和控制对象的创建,可以避免每次创建对象时都调用构造函数。

工厂方法/抽象工厂方法

工厂方法(Factory Method)模式是一种创建型设计模式,它提供一个用于创建对象的接口,但让子类决定要实例化的类。工厂方法将类的实例化推迟到子类。换句话说,工厂方法模式允许客户端代码通过接口来创建对象,而不直接依赖于具体的类。

抽象工厂方法(Abstract Factory Method)模式是工厂方法模式的进一步扩展。它提供了一个创建一系列相关或相互依赖对象的接口,而无需指定具体的类。抽象工厂模式的核心思想是多个产品族的创建。每个工厂可以创建多个产品,而每个产品都有多个具体的实现。

工厂模式通常涉及以下几个角色:

抽象工厂(Abstract Factory):定义一个创建对象的接口,但不负责具体的对象创建过程。它通常是一个接口或者抽象类,其中定义了一个或多个创建对象的方法。

具体工厂(Concrete Factory):实现抽象工厂接口,负责实际创建具体的对象。每个具体工厂都对应着一种具体的对象类型。

产品(Product):工厂所创建的对象类型。它可以是一个接口、抽象类或者具体类。

具体产品(Concrete Product):实现产品接口的具体对象。工厂模式的每个具体工厂都负责创建特定的具体产品。

原型模式

通过复制(克隆)现有对象来创建新对象,而不是通过构造函数创建。来避免了重新创建对象的复杂过程。

原型模式非常适合以下场景:

  • 对象的创建成本较高:例如,当创建对象需要大量的计算、复杂的初始化过程,或涉及外部资源(如数据库或文件)的交互时,使用原型模式可以显著减少对象创建的成本。
  • 对象具有相似性:当需要创建多个相似或相同的对象时,通过克隆一个原型对象来进行快速复制,避免了重复创建的工作。
  • 对象的变化不大:对于需要频繁创建且大部分字段相同的对象,可以通过原型模式来减少创建时的额外开销。

单例模式

核心思想是:控制类的实例化过程,确保在整个应用程序中只存在一个实例

常见例子:

  • 系统配置管理器:整个系统只需要一个配置管理器
  • 日志记录器:应用程序通常只需要一个日志系统
  • 缓存管理器:全局缓存管理器应该是唯一的
  • 任务管理器:系统任务调度器应该是单例的

享元模式

主要目的是通过共享相同的对象来减少内存使用。该模式特别适用于需要大量相似对象的场景,通过减少对象的数量和内存占用来提高性能。

享元模式的核心思想是:通过共享对象来减少内存的使用。它将对象分为 内部状态 和 外部状态,其中 内部状态 是可以共享的,而 外部状态 是需要根据具体场景变化的

接口隔离

适配器模式

主要目的是将一个类的接口转换成客户端所期望的另一个接口。通过适配器模式,可以使得原本由于接口不兼容而无法一起工作的类能够协同工作。适配器模式通过提供一个转换层来实现兼容性,从而让不同接口的类能够顺利合作。

关键点:

  • 目标接口和源接口不兼容,但是需要通过适配器将其进行转换。
  • 适配器实现了目标接口,并在内部调用源接口的方法,将不兼容的接口适配为客户端可以使用的形式。
  • 装饰器与适配器的区别:装饰器是为了增加新功能,而适配器是为了兼容不同接口。

外观模式

代理模式

主要目的是通过一个代理对象来控制对目标对象的访问。代理模式可以用来控制访问、延迟加载、权限检查、远程通信等功能,它提供了一种替代真实对象的方式,并对目标对象的访问进行封装。

代理模式的关键思想是:用代理对象控制对目标对象的访问,代理对象通常会在调用目标对象的方法前后进行某些额外的操作,如缓存、延迟加载、权限校验等。

代理模式有几种常见的变种,具体包括:

  • 虚拟代理:通过延迟初始化(懒加载)来减少不必要的开销,只有当真正需要访问目标对象时才创建它。
  • 远程代理:用于表示一个对象在不同的地址空间(如远程服务器)上的实例,通常用来封装远程调用。
  • 保护代理:控制对目标对象的访问,根据不同的权限和角色来控制访问,通常用于安全控制。
  • 智能代理:提供额外的功能,如内存管理、日志记录、缓存等。

与装饰器模式的区别在于,代理模式侧重于控制,装饰器模式侧重于增加功能

组件协作

模板模式

在父类中定义了算法的骨架,并允许子类在不改变算法结构的前提下重定义算法的某些特定步骤

观察者模式

定义了对象之间的一对多依赖关系,当一个对象状态发生改变时,所有依赖于它的对象都得到通知并自动更新。观察者模式的核心思想是解耦,即主题(Subject)和观察者(Observer)之间没有直接的依赖关系,主题不需要知道观察者的具体信息,只需要知道观察者实现了统一的接口,可以进行通知。

观察者模式的特点:

  1. 一对多关系:一个主题(Subject)对象可以有多个观察者(Observers)。当主题发生变化时,它会通知所有观察者。
  2. 松耦合:主题对象和观察者对象之间通过接口进行交互,而不是直接依赖。主题只负责维护观察者列表,并在状态变化时通知它们,观察者只需要关心何时更新。
  3. 动态订阅:观察者可以随时注册和注销(订阅和取消订阅)自己,不需要主题对象的重新编译或修改。

策略模式

有一个抽象策略类(接口或抽象类),多个具体策略类实现该接口或继承该类,客户端通过上下文类来选择策略。策略模式解耦了上下文类和具体的算法,客户端只关心算法的接口,而不需要关心算法的具体实现。

单一职责

装饰器模式

用于动态地给对象添加额外的职责。它通过将对象包装在装饰器对象中来扩展对象的功能,而无需修改对象的代码。这使得装饰器模式可以在运行时动态地给对象增加或修改行为,而不会影响到其他对象

关键点:

  • 装饰器是通过继承来扩展功能:装饰器类继承了组件接口,可以调用被装饰对象的方法,并在此基础上添加额外的行为。
  • 装饰器对象持有原始对象:装饰器类通常包含一个指向原始对象的引用,通过这个引用调用原始对象的方法,然后在此基础上增强或改变功能。

桥接模式

有两个抽象类,一个表示抽象部分,另一个表示实现部分。抽象类通过组合关系持有实现类的实例。将抽象部分和实现部分分离,使它们可以独立变化,从而避免多重继承带来的复杂性。通常通过组合来连接抽象和实现,达到灵活扩展的效果。

数据结构

组合模式

主要目的是将对象组合成树形结构,以表示”部分-整体”的层次结构。组合模式使得客户端可以统一处理单个对象和对象集合(树形结构)之间的差异。它常用于表示树形结构的场景,类似于文件系统中的文件和文件夹结构。

状态变化

状态模式

状态模式是一种 行为型设计模式,它允许对象在其内部状态改变时改变其行为。换句话说,状态模式通过将不同的状态封装成独立的类,使得对象的行为随着状态的变化而变化,从而避免了大量的 if-else 或 switch 语句的嵌套。