Skip to content

面向对象

基本概念

对象

继承

多态

封装

抽象

接口

SOLID 原则

SOLID 是由 Robert C. Martin (Uncle Bob) 提出的五个基本原则的首字母缩写。

单一职责原则 (Single Responsibility Principle - SRP)

  • 定义: 一个类应该只有一个引起它变化的原因,即一个类应该只承担一个职责。如果一个类承担了多个职责,那么当其中一个职责发生变化时,可能会影响到其他职责,导致类变得脆弱。

  • 目的:提高类的内聚性,降低类的复杂度,提高可读性和可维护性。当需求变更时,只需修改相关的类。

  • 例子:假设有一个 Employee 类,它既负责存储员工信息,又负责计算薪水,还负责将员工数据保存到数据库。

c++
    // 违反 SRP 的例子
    class Employee {
        properties:
            name
            department
            salary_base
            hours_worked

        methods:
            get_employee_details() { ... }
            calculate_salary() {
                // 薪水计算逻辑
                return salary_base * hours_worked * rate;
            }
            save_to_database() {
                // 将员工数据保存到数据库的逻辑
                connect_db();
                insert_data(this.details);
                disconnect_db();
            }
    }

根据 SRP,应该将这些职责分离:

c++
    // 应用 SRP 的例子
    class EmployeeData {
        properties:
            name
            department
            salary_base
            hours_worked

        methods:
            get_name() { return name; }
            get_department() { return department; }
            // ... 其他 getter/setter
    }

    class SalaryCalculator {
        methods:
            calculate(employee_data: EmployeeData) {
                return employee_data.salary_base * employee_data.hours_worked * rate;
            }
    }

    class EmployeeRepository {
        methods:
            save(employee_data: EmployeeData) {
                connect_db();
                insert_data(employee_data);
                disconnect_db();
            }
    }

现在,如果薪水计算规则改变,只需要修改 SalaryCalculator 类。如果数据库存储方式改变,只需要修改 EmployeeRepository 类。

开放—封闭原则 (Open/Closed Principle - OCP)

  • 定义:软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着当需要添加新功能时,应该通过添加新代码(例如创建新的子类或实现新的接口)来实现,而不是修改已有的、经过测试的代码。
  • 目的:提高系统的可维护性和稳定性。避免因修改现有代码而引入新的bug。
  • 例子: 假设有一个 AreaCalculator 类,用于计算不同形状的面积。
c++
    // 违反 OCP 的例子
    class Shape {
        properties:
            type // "rectangle", "circle"
            width
            height
            radius
    }

    class AreaCalculator {
        methods:
            calculate_area(shape: Shape) {
                area = 0;
                if (shape.type == "rectangle") {
                    area = shape.width * shape.height;
                } else if (shape.type == "circle") {
                    area = PI * shape.radius * shape.radius;
                }
                // 如果要增加三角形,需要修改这里的代码
                return area;
            }
    }

根据 OCP,应该使用抽象和多态:

c++
    // 应用 OCP 的例子
    abstract class Shape {
        abstract methods:
            calculate_area(): number
    }

    class Rectangle extends Shape {
        properties:
            width
            height
        methods:
            calculate_area() {
                return width * height;
            }
    }

    class Circle extends Shape {
        properties:
            radius
        methods:
            calculate_area() {
                return PI * radius * radius;
            }
    }

    // 如果要增加三角形
    class Triangle extends Shape {
        properties:
            base
            height
        methods:
            calculate_area() {
                return 0.5 * base * height;
            }
    }

    class AreaCalculator {
        methods:
            calculate_total_area(shapes: List<Shape>) {
                total_area = 0;
                for each shape in shapes {
                    total_area += shape.calculate_area(); // 不需要修改 AreaCalculator
                }
                return total_area;
            }
    }

当需要支持新形状时,只需创建一个新的 Shape 子类,而 AreaCalculator 无需修改。

里氏替换原则 (Liskov Substitution Principle - LSP)

  • 定义:所有引用基类(父类)的地方必须能够透明地使用其子类的对象,而不改变程序的正确性。简单来说,子类应该能够完全替换掉它们的父类,并且程序行为不会出错。
  • 目的:确保继承的正确使用,保持类层次结构的稳定性和一致性。
  • 例子: 经典的例子是“矩形-正方形”问题。如果 Square 继承自 Rectangle
c++
    class Rectangle {
        properties:
            width
            height
        methods:
            set_width(w) { this.width = w; }
            set_height(h) { this.height = h; }
            get_area() { return width * height; }
    }

    // 正方形是矩形的一种特殊情况
    class Square extends Rectangle {
        methods:
            // 为了保持正方形的特性,设置宽度时高度也随之改变
            set_width(side) {
                super.set_width(side);
                super.set_height(side);
            }
            // 设置高度时宽度也随之改变
            set_height(side) {
                super.set_height(side);
                super.set_width(side);
            }
    }

    function client_code(rectangle: Rectangle) {
        rectangle.set_width(5);
        rectangle.set_height(4);
        // 期望面积是 5 * 4 = 20
        // 但如果传入的是 Square 对象,由于 set_height(4) 会将 width 也设为 4,
        // 面积会变成 4 * 4 = 16,这与期望不符。
        assert(rectangle.get_area() == 20); // 对于 Square 会失败
    }

这里,Square 子类改变了 Rectangle 父类的行为(set_widthset_height 不再独立),导致 client_code 在使用 Square 对象替换 Rectangle 对象时行为不一致。这违反了 LSP。

修正思路:可能不应该让 Square 继承 Rectangle,或者改变基类的设计,例如让 Rectangle 的宽高在构造后不可变,或者使用更通用的 Shape 抽象。

接口隔离原则 (Interface Segregation Principle - ISP)

  • 定义:客户端不应该被迫依赖于它不使用的方法。一个类对另一个类的依赖应该建立在最小的接口上。这意味着应该使用多个专门的接口,而不是一个庞大臃肿的通用接口。
  • 目的:降低类之间的耦合度,提高系统的灵活性和可维护性。
  • 例子: 假设有一个多功能打印机接口 IMultiFunctionPrinter
c++
    // 违反 ISP 的例子
    interface IMultiFunctionDevice {
        methods:
            print_document(document)
            scan_document(document)
            fax_document(document)
    }

    class SimplePrinter implements IMultiFunctionDevice {
        methods:
            print_document(document) { /* 打印逻辑 */ }
            scan_document(document) {
                // SimplePrinter 不支持扫描,被迫实现一个空方法或抛出异常
                throw new Error("Scan not supported");
            }
            fax_document(document) {
                // SimplePrinter 不支持传真
                throw new Error("Fax not supported");
            }
    }

SimplePrinter 被迫依赖并实现它不需要的 scan_documentfax_document 方法。

根据 ISP,应该将接口拆分:

c++
    // 应用 ISP 的例子
    interface IPrinter {
        methods:
            print_document(document)
    }

    interface IScanner {
        methods:
            scan_document(document)
    }

    interface IFaxDevice {
        methods:
            fax_document(document)
    }

    class SimplePrinter implements IPrinter {
        methods:
            print_document(document) { /* 打印逻辑 */ }
    }

    class Photocopier implements IPrinter, IScanner {
        methods:
            print_document(document) { /* ... */ }
            scan_document(document) { /* ... */ }
    }

    class AllInOnePrinter implements IPrinter, IScanner, IFaxDevice {
        methods:
            print_document(document) { /* ... */ }
            scan_document(document) { /* ... */ }
            fax_document(document) { /* ... */ }
    }

现在,类只需要实现它们真正需要的接口。

依赖倒置原则 (Dependency Inversion Principle - DIP)

  • 定义
    1. 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
    2. 抽象不应该依赖于细节,细节应该依赖于抽象。
  • 目的:减少高层模块对低层模块实现的依赖,增强系统的灵活性和可维护性。方便替换低层模块的实现。
  • 例子: 假设有一个 NotificationService (高层模块)需要发送邮件,它直接依赖于 EmailSender (低层模块)。
c++
    // 违反 DIP 的例子
    class EmailSender { // 低层模块
        methods:
            send_email(to, subject, body) { /* 发送邮件的具体实现 */ }
    }

    class NotificationService { // 高层模块
        properties:
            email_sender: EmailSender

        constructor() {
            this.email_sender = new EmailSender(); //直接依赖具体实现
        }

        methods:
            send_notification(user_email, message) {
                this.email_sender.send_email(user_email, "Notification", message);
            }
    }

如果将来需要支持短信通知,就需要修改 NotificationService

根据 DIP,应该引入抽象(接口):

c++
    // 应用 DIP 的例子
    interface IMessageSender { // 抽象
        methods:
            send(recipient, message_content): boolean
    }

    class EmailSender implements IMessageSender { // 低层模块,实现抽象
        methods:
            send(recipient_email, message_text) {
                // 发送邮件的具体实现
                print("Sending email to " + recipient_email + ": " + message_text);
                return true;
            }
    }

    class SmsSender implements IMessageSender { // 另一个低层模块
        methods:
            send(phone_number, message_text) {
                // 发送短信的具体实现
                print("Sending SMS to " + phone_number + ": " + message_text);
                return true;
            }
    }

    class NotificationService { // 高层模块,依赖抽象
        properties:
            message_sender: IMessageSender // 依赖接口

        constructor(sender: IMessageSender) { // 通过依赖注入传入具体实现
            this.message_sender = sender;
        }

        methods:
            notify(recipient_info, message) {
                this.message_sender.send(recipient_info, message);
            }
    }

    // 使用
    email_sender_instance = new EmailSender();
    notification_service_via_email = new NotificationService(email_sender_instance);
    notification_service_via_email.notify("user@example.com", "Hello via Email!");

    sms_sender_instance = new SmsSender();
    notification_service_via_sms = new NotificationService(sms_sender_instance);
    notification_service_via_sms.notify("+1234567890", "Hello via SMS!");

现在 NotificationService 依赖于 IMessageSender 接口,而不是具体的发送器实现。可以轻松地传入不同的发送器实现。

包(Package)设计原则

这些原则主要由 Robert C. Martin 提出,用于指导如何将类组织到包中,以管理依赖关系和可维护性。主要分为包内聚原则和包耦合原则。

包内聚原则 (Package Cohesion Principles)

目标是使包内的元素具有强关联性。

1. 共同封闭原则 (Common Closure Principle - CCP)

  • 定义:包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中的所有类(或大部分类)产生影响,而对于其他的包不造成任何影响。如果一组类因为相同类型的原因而改变,那么它们应该被放在同一个包中。
  • 目的:将变化的影响限制在单个包内,减少因需求变更而需要修改、重新测试和重新发布的包的数量。
  • 例子: 假设有一个电子商务系统,其中订单处理相关的类包括 Order, OrderItem, OrderValidator, OrderProcessor。 如果订单处理的业务规则(如税收计算、折扣策略)发生变化,很可能这几个类都需要相应地修改。
c++
    package OrderProcessing { // 遵循 CCP
        class Order { ... }
        class OrderItem { ... }
        class OrderValidator {
            // 验证规则可能随业务变化
        }
        class OrderProcessor {
            // 处理流程可能随业务变化
        }
        // 如果添加了 OrderDiscountCalculator,它也应该在这个包里
    }

    // 如果 OrderDisplay 只是显示订单,不随核心处理逻辑变化,则不一定在此包
    package UserInterface {
        class OrderDisplay { ... }
    }

当订单处理逻辑变化时,我们期望只需要修改 OrderProcessing 包。

2. 共同重用原则 (Common Reuse Principle - CRP)

  • 定义:一个包中的所有类应该是被共同重用的。如果你重用了包中的一个类,那么你就应该重用包中的所有类。不应强迫用户依赖他们不需要的东西。
  • 目的:避免因重用一个类而被迫引入大量不相关的依赖。使得包的粒度更适合重用。
  • 例子: 假设有一个 DataStructures 包,包含了 LinkedList, Queue, Stack, BinaryTree, Graph。 如果一个应用只需要使用 LinkedList,但根据 CRP,它被迫也依赖(或引入)了 Queue, Stack, BinaryTree, Graph。这可能不是好的包划分。
c++
    // 可能违反 CRP 的例子 (如果这些类不是通常一起被重用)
    package GenericDataStructures {
        class LinkedList { ... }
        class Queue { ... }       // 通常基于 LinkedList 或 Array
        class Stack { ... }       // 通常基于 LinkedList 或 Array
        class BinaryTree { ... }
        class Graph { ... }
    }

    // 应用 CRP 的思路可能是更细粒度的包
    package BasicListStructures { // 被高度共同重用
        class LinkedList { ... }
        class ArrayBasedList { ... }
    }
    package AdvancedTreeStructures {
        class BinaryTree { ... }
        class AVLTree { ... }
    }

如果 QueueStack 总是与 LinkedList 一起被讨论和实现,并且一个使用 Queue 的应用几乎总会间接或直接地对 LinkedList 的概念有所依赖(即使 Queue 内部封装了 LinkedList),那么将它们放在一个包里可能是合理的。关键在于“共同重用”的程度。

3. 重用/发布等价原则 (Reuse/Release Equivalence Principle - REP)

  • 定义:重用的粒度就是发布的粒度。一个可重用的元素(如一个包或一组类)应该作为一个整体被发布和版本化。
  • 目的:使得重用方能够清晰地知道他们依赖的是哪个版本的组件,方便版本管理和依赖跟踪。
  • 例子: 如果有一个 LoggingFramework 包,它包含多个类。当这个框架发布时,它应该有一个版本号(如 v1.0.0)。使用这个框架的应用会依赖这个特定版本。如果框架有更新(如 v1.1.0),应用可以选择是否升级。这个包作为一个单元进行发布。

包耦合原则 (Package Coupling Principles)

目标是管理包之间的依赖关系,使其尽可能松散和可控。

1. 无环依赖原则 (Acyclic Dependencies Principle - ADP)

  • 定义:在包的依赖关系图中不允许出现环。即,如果包A依赖包B,包B依赖包C,那么包C不能反过来依赖包A。
  • 目的:避免“依赖地狱”。循环依赖使得包难以独立开发、测试、部署和版本化。一个包的改变可能引发不可预测的连锁反应。
  • 例子
c++
    // 违反 ADP
    package PaymentProcessing {
        import BillingSystem; // 支付处理依赖计费系统
        class ProcessPayment { ... }
    }

    package BillingSystem {
        import UserNotifications; // 计费系统依赖用户通知
        class GenerateInvoice { ... }
    }

    package UserNotifications {
        import PaymentProcessing; // 用户通知又反过来依赖支付处理 (形成了环)
        class SendPaymentReceipt { ... }
    }

修正思路: 1. 依赖倒置:让 UserNotificationsPaymentProcessing 都依赖于一个定义在更高层或独立包中的抽象接口(例如 IPaymentNotifier)。 2. 提取公共依赖:创建一个新的包,包含 UserNotifications 需要从 PaymentProcessing 获取的功能(或反之),让两者都依赖这个新包。

2. 稳定依赖原则 (Stable Dependencies Principle - SDP)

  • 定义:依赖关系应该指向更稳定的方向。一个包应该只依赖于比它自身更稳定的包。“稳定”意味着不容易改变。
  • 目的:减少不稳定包的变化对系统的影响。如果一个经常变化的包(不稳定)依赖于另一个经常变化的包,那么系统会非常脆弱。
  • 例子UI 包(用户界面,经常变动)通常应该依赖于 BusinessLogic 包(业务逻辑,相对稳定),而不应该是 BusinessLogic 依赖于 UI
    pseudocode
    package BusinessLogic { // 应该比较稳定
        class CoreService { ... }
    }
    
    package DataAccess { // 也应该比较稳定
        class Repository { ... }
    }
    
    package UserInterface { // 经常变化,不稳定
        import BusinessLogic; // UI 依赖业务逻辑 (好的方向)
        // import DataAccess;  // UI 可以依赖数据访问,但通常通过业务逻辑
    
        // 不好的情况: BusinessLogic imports UserInterface (违反 SDP)
    }
    如果 BusinessLogic 依赖 UserInterface,那么UI的任何小改动都可能迫使业务逻辑层重新编译、测试和部署。

3. 稳定抽象原则 (Stable Abstractions Principle - SAP)

  • 定义:一个包的抽象程度应该与其稳定性成正比。一个稳定的包(即不经常改变的包)也应该是抽象的,这样它的稳定性就不会阻止系统的扩展。一个不稳定的包应该是具体的,因为它的不稳定性允许其内部的具体代码可以被轻易修改。
  • 目的:使得稳定的、被广泛依赖的包能够通过抽象来支持扩展,而不需要修改自身。
  • 例子: 一个核心框架包 CoreFramework 非常稳定,被许多其他模块依赖。根据 SAP,这个包应该包含大量的接口和抽象类。
    pseudocode
    package CoreFramework { // 稳定且抽象
        interface IPlugin { ... }
        abstract class BaseService { ... }
        // ... 其他核心抽象
    }
    
    package FeatureModuleA { // 不稳定,具体,依赖 CoreFramework
        import CoreFramework;
        class MyPluginImplementation implements IPlugin { ... }
        class MySpecificService extends BaseService { ... }
    }
    CoreFramework 的稳定性使得它不常变动,而它的抽象性允许 FeatureModuleA 这样的包通过实现接口或继承抽象类来扩展功能,而无需修改 CoreFramework

遵循这些设计原则需要权衡和经验,它们的目标是创建出更易于理解、修改、测试和部署的软件系统。

UML

  1. 类图
  2. 对象图
  3. 用例图
  4. 时序图
  5. 协作图
  6. 状态图
  7. 活动图

设计模式

  1. 创建型模式

创建对象的方式

  • 单例模式:一个类只有一个实例,并提供一个访问它的全局访问点
  • 工厂模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到子类
  • 抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类
  • 建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
  • 原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
  1. 结构型模式

定义:

  • 适配器模式:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
  • 桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化
  • 装饰模式:动态地给一个对象添加一些额外的职责
  • 组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性
  • 外观模式:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
  • 享元模式:运用共享技术有效地支持大量细粒度的对象
  • 代理模式:为其他对象提供一种代理以控制对这个对象的访问
  1. 行为型模式

定义:

  • 观察者模式:定义对象间的一对多依赖关系,当一个对象改变状态时,所有依赖它的对象都会收到通知并自动更新
  • 中介者模式:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互
  • 策略模式:定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换
  • 模板方法模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
  • 命令模式:将请求封装成对象,从而使您可以用不同的请求、队列或日志来参数化其他对象。命令模式也支持可撤销的操作
  • 责任链模式:为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止
  • 状态模式:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类
  • 迭代器模式:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示
  • 访问者模式:表示一个作用于某对象结构中的各元素的操作。它使您可以在不改变各元素的类的前提下定义作用于这些元素的新操作

设计模式分类

如有转载或 CV 的请标注本站原文地址