设计模式中的几大原则

本文介绍一下设计模式中的几大原则,这些原则共同的目标是创建高内聚、低耦合易于维护、扩展和复用的代码。

1. 单一职责原则

1.1 解读

单一职责原则,Single Responsibility Principle,SRP

核心思想:一个类只负责一项明确的职责或功能,一个类应该只有一个引起它变化的原因。

一个类职责过多,职责都耦合在一起,甚至还会产生一些"上帝类"。当其中任何一个职责需求发生变更时,都需要修改这个类,因为高耦合,很可能导致变更之外的其他职责也发生变化。如果修改只影响一个职责明确的类,风险最低,影响范围最小。

单一职责原则是构建可维护系统的基石,我们需要发现职责,并把职责相互分离,让每个类专注做好一件事或一类事。确保每个类职责单一、内聚。如果一个类承担过多职责,就很难做到对扩展开放或清晰地定义抽象接口。

1.2 正反举例

以处理订单为例:

违反 SRP 原则设计:

java 复制代码
class OrderProcessor {
    public void processOrder(Order order) {
        // 1. 数据验证
        if (!validateOrder(order)) {
            throw new InvalidOrderException("Order validation failed");
        }
        // 2. 价格计算
        calculateTotalPrice(order);
        // 3. 数据持存储
        saveOrderToDatabase(order);
        // 4. 发送订单通知
        sendConfirmationEmail(order);
    }
    // ... 具体的验证、计算、保存、发送邮件方法实现 ...
}

以上代码设计存在的问题:OrderProcessor 类承担了太多职责:验证、计算、存储、通知等。任何一方面的需求变更(如邮件模板变化)都需要修改这个类,风险高且难以测试单个功能。

遵循 SRP 原则设计:

java 复制代码
class OrderValidator { // 职责:数据验证
    public boolean validate(Order order) { ... }
}

class PriceCalculator { // 职责:价格计算
    public void calculateTotal(Order order) { ... }
}

class OrderRepository { // 职责:数据存储
    public void save(Order order) { ... }
}

class NotificationService { // 职责:发送通知
    public void sendOrderConfirmation(Order order) { ... }
}

class OrderProcessor { // 职责:协调流程 (仅此一个)
    private OrderValidator validator;
    private PriceCalculator calculator;
    private OrderRepository repository;
    private NotificationService notifier;

    // 通过构造器或Setter注入依赖
    public OrderProcessor(OrderValidator v, PriceCalculator c, OrderRepository r, NotificationService n) {
        this.validator = v;
        this.calculator = c;
        this.repository = r;
        this.notifier = n;
    }

    public void processOrder(Order order) {
        if (!validator.validate(order)) {
            throw new InvalidOrderException("Order validation failed");
        }
        calculator.calculateTotal(order);
        repository.save(order);
        notifier.sendOrderConfirmation(order);
    }
}

以上代码设计的好处:每个类职责清晰单一,需求变更影响小,一个类只有一个引起它变化的原因。如邮件格式变了,只需改 NotificationService。而且 NotificationService 还可以进行复用,可以用于发送其他类型的通知。

并且易于测试,可以独立测试验证逻辑、计算逻辑、存储逻辑、通知逻辑。

2. 开放-封闭原则

2.1 解读

开放-封闭原则,Open-Closed Principle,OCP

核心思想:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭

需求会变是常态,面对新需求,对程序的改动是通过增加新代码进行的,而不是更改现有的、已经测试通过并运行稳定的代码。

不可能所有代码都绝对不修改。目标是隔离变化,让核心、稳定的部分尽量封闭,将易变的部分封装在可通过扩展来改变的抽象后面。

通常通过定义稳定的抽象层(接口或抽象类)来实现 OCP。具体实现依赖于这些抽象。当需要新行为时,创建新的实现类来扩展抽象,而不是修改已有的实现或依赖于抽象的客户端代码。

2.2 正反举例

以图形绘制系统为例:

违反OCP的设计:

java 复制代码
class GraphicsRenderer {
    public void drawShape(Object shape) {
        if (shape instanceof Circle) {
            drawCircle((Circle) shape); // 修改点:新增图形需要添加if分支
        } else if (shape instanceof Rectangle) {
            drawRectangle((Rectangle) shape); 
        }
    }
    private void drawCircle(Circle c) { ... }
    private void drawRectangle(Rectangle r) { ... }
}

以上代码设计存在的问题:如果添加新图形 Triangle,必须修改这个 drawShape 方法,违反了对修改关闭的原则,容易引入错误,且修改点会随着图形种类增多而爆炸。

遵循OCP的设计:

java 复制代码
interface Shape { // 稳定的抽象:所有图形都必须能绘制自己
    void draw();
}

class Circle implements Shape { // 具体实现:圆形
    @Override
    public void draw() {
        // 具体绘制圆形的代码
        System.out.println("Drawing a Circle");
    }
}

class Rectangle implements Shape { // 具体实现:矩形
    @Override
    public void draw() {
        // 具体绘制矩形的代码
        System.out.println("Drawing a Rectangle");
    }
}

class GraphicsRenderer {
    public void drawShape(Shape shape) { // 参数是抽象Shape
        shape.draw(); // 核心逻辑稳定:只调用draw()方法,不关心具体类型
    }
}

// 未来扩展:添加三角形
class Triangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Triangle");
    }
}

以上代码设计的好处:做到了对扩展开放,对修改关闭,且系统核心(GraphicsRenderer)稳定,不易因添加新功能而引入错误。

3. 里氏替换原则

3.1 解读

里氏替换原则,Liskov Substitution Principle,LSP

核心思想:子类型必须能够替换掉他们的父类型,而程序的行为不变

所有引用基类(父类)的地方必须能透明地使用其子类的对象,子类必须完全实现父类的行为契约,不能破坏父类的原有功能。子类可以扩展父类的功能,但不能改变父类原有的行为(包括输入、输出、异常等约定)。

3.2 正反举例

以几何图形系统为例:

违反LSP的设计

java 复制代码
class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int getArea() { return width * height; }
}

class Square extends Rectangle {
    // 正方形:宽高必须相等,因此重写setter方法强制相等
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width); 
    }

    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height); 
    }
}

// 客户端代码:期望操作矩形
public class Client {
    public void resize(Rectangle rect) {
        // 假设这个操作期望只增加宽度
        rect.setWidth(rect.getWidth() + 10);
        // 预期:高度不变,面积增加
    }

    public static void main(String[] args) {
        Client client = new Client();
        Rectangle rect = new Rectangle();
        rect.setWidth(5);
        rect.setHeight(10);
        client.resize(rect); // 正常:宽度15,高度10,面积150

        // 使用正方形(子类替换父类)
        Rectangle square = new Square();
        square.setWidth(5); // 此时宽高都是5
        client.resize(square); // 宽度变为15,高度也被改为15,面积225(预期是宽度15高度5,面积75)
    }
}

以上代码设计存在的问题:Square 继承 Rectangle 并重写了 setWidth 和 setHeight 方法,导致在 resize 方法中(期望操作矩形)传入正方形时,行为发生了不可预期的改变(高度被意外修改)。子类不能透明替换父类。

遵循LSP的设计

java 复制代码
// 使用组合代替继承
abstract class Shape {
    public abstract int getArea();
}

class Rectangle extends Shape {
    private int width;
    private int height;
    // 构造器、getter/setter
    @Override
    public int getArea() { return width * height; }
}

class Square extends Shape {
    private int side;
    // 构造器、getter/setter for side
    @Override
    public int getArea() { return side * side; }
}

// 客户端代码:只依赖Shape抽象
public class Client {
    public void printArea(Shape shape) {
        System.out.println("Area: " + shape.getArea());
    }
}

以上代码设计的好处:通过多态和抽象基类(Shape)实现了解耦与灵活性,子类(Rectangle 和 Square)各自独立实现 getArea 方法,互不干扰,确保行为独立,客户端仅依赖抽象的 Shape 接口,无需关心具体类型即可统一调用面积计算,行为一致且可扩展。

同时,正方形通过直接继承 Shape 而非矩形,避免了因继承关系导致的行为不一致(如强制继承矩形的长宽属性),从而更符合单一职责原则和里氏替换原则。

4. 依赖倒置原则

4.1 解读

依赖倒置原则,Dependency Inversion Principle,DIP

核心思想:高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象

传统依赖方向:高层调用并依赖具体低层实现,高层依赖低层

倒置:将传统依赖方向进行倒置,DIP 要求高层和低层都依赖于同一个稳定的抽象接口。

针对接口编程,不对实现编程,这是实现 DIP 的关键。模块间通过接口(或抽象类)进行交互,而不是直接依赖具体的实现类。

DIP 极大地降低了模块间的耦合度。高层模块只关心抽象接口提供的功能契约,不关心具体是谁、如何实现的。低层模块只要符合接口契约,就可以被高层使用,甚至可以在运行时动态替换。

4.2 正反举例

以数据存储服务为例:

违反DIP的设计:

java 复制代码
class ReportService { // 高层业务模块
    private FileDataStorage storage = new FileDataStorage(); // 直接依赖具体低层实现

    public void generateReport(ReportData data) {
        // ... 生成报告的业务逻辑 ...
        storage.save(data); // 调用具体实现的方法
    }
}

class FileDataStorage { // 低层实现:文件存储
    public void save(ReportData data) {
        // 具体保存到文件的代码
        System.out.println("Saving report data to a file...");
    }
}

以上代码存在的问题:ReportService(高层模块)直接依赖并实例化了具体的 FileDataStorage(低层实现),这种设计导致系统紧耦合:更换存储方式(如改用 DatabaseDataStorage)需修改 ReportService 源代码,违背开闭原则。

遵循DIP的设计:

java 复制代码
interface DataStorage { // 稳定的抽象:定义存储契约
    void save(ReportData data);
}

class FileDataStorage implements DataStorage { // 具体实现:文件存储
    @Override
    public void save(ReportData data) {
        // 具体保存到文件的代码
        System.out.println("Saving report data to a file...");
    }
}

class DatabaseDataStorage implements DataStorage { // 具体实现:数据库存储
    @Override
    public void save(ReportData data) {
        // 具体保存到数据库的代码
        System.out.println("Saving report data to database...");
    }
}

class CloudStorage implements DataStorage { ... } // 另一个具体实现:云存储

class ReportService { // 高层业务模块
    private DataStorage storage; // 依赖抽象接口,而非具体实现

    // 依赖注入
    public ReportService(DataStorage storage) {
        this.storage = storage; 
    }

    public void generateReport(ReportData data) {
        // ... 生成报告的业务逻辑 (稳定) ...
        storage.save(data); 
    }
}

// 使用示例 (通常在应用启动或配置处组装)
public static void main(String[] args) {
    // 选择使用哪种存储实现 (配置决定)
    DataStorage storage = new DatabaseDataStorage(); 
    ReportService reportService = new ReportService(storage); // 注入依赖
    reportService.generateReport(someData);
}

以上代码设计的好处:以通过依赖接口而非具体实现,实现了高层与低层的解耦:ReportService 仅依赖稳定的接口,无需关心底层存储细节,更换存储方式仅需在配置层注入不同实现,无需修改其代码。同时,低层实现必须遵循接口规范,这种设计使得系统易于扩展且便于测试。

5. 迪米特法则

5.1 解读

迪米特法则,Law of Demeter,LoD

核心思想:一个对象应该对其他对象保持最少的了解

如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。

迪米特法则根本思想,是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。

5.2 正反举例

以超市购物系统为例:

违反LoD的设计

java 复制代码
class Customer {
    private ShoppingCart cart;
    public ShoppingCart getCart() { return cart; }
}

class ShoppingCart {
    private List<Item> items;
    public List<Item> getItems() { return items; }
}

class Item {
    private String name;
    private double price;
    public double getPrice() { return price; }
}

class Cashier {
    // 收银员直接深入到顾客的购物车内部获取商品并计算价格
    public double calculateTotalPrice(Customer customer) {
        double total = 0.0;
        for (Item item : customer.getCart().getItems()) {
            total += item.getPrice();
        }
        return total;
    }
}

以上代码设计存在的问题:Cashier 类直接调用了 customer.getCart().getItems(),这使它不仅依赖于Customer,还依赖于 ShoppingCart 和 Item 的内部结构。如果 ShoppingCart 不再用 List<Item> 存储商品,Cashier 的代码也必须修改。

遵循LoD的设计

java 复制代码
class Customer {
    private ShoppingCart cart;
    public double getCartTotalPrice() {
        return cart.calculateTotalPrice();
    }
}

class ShoppingCart {
    private List<Item> items;
    public double calculateTotalPrice() {
        double total = 0.0;
        for (Item item : items) {
            total += item.getPrice();
        }
        return total;
    }
}

class Item { ... } // 同上

class Cashier {
    public double checkout(Customer customer) {
        return customer.getCartTotalPrice(); // 只调用Customer的方法
    }
}

以上代码设计的好处:收银员仅与直接关联的客户交互,不再深入访问客户内部的购物车或商品等细节,仅通过客户暴露的接口完成业务。若购物车内部数据结构调整,只需修改购物车自身的方法,无需改动收银员或客户的公共接口,确保了系统各模块的独立性与可维护性,同时明确了谁的数据谁负责计算的职责边界。

6. 接口隔离原则

6.1 解读

接口隔离原则,Interface Segregation Principle,ISP

核心思想:客户端不应该被迫依赖于它不使用的接口,一个类对另一个类的依赖应该建立在最小的接口上。

当一个接口包含过多方法时,实现类可能被迫实现无用逻辑(如空方法或异常抛出),导致代码冗余、维护成本上升并增加潜在错误风险。通过接口隔离原则拆分接口为小而专一的子接口,可确保客户端仅依赖所需方法,避免不必要耦合。

6.2 正反举例

以多功能打印机为例:

违反ISP的设计

java 复制代码
interface MultiFunctionPrinter {
    void print(Document doc);
    void scan(Document doc);
    void fax(Document doc);
}

// 经济型打印机:只有打印功能
class EconomyPrinter implements MultiFunctionPrinter {
    public void print(Document doc) { ... }
    public void scan(Document doc) {
        throw new UnsupportedOperationException("Scan not supported");
    }
    public void fax(Document doc) {
        throw new UnsupportedOperationException("Fax not supported");
    }
}

// 高级打印机:支持所有功能
class AdvancedPrinter implements MultiFunctionPrinter { ... }

以上代码设计存在的问题:EconomyPrinter 被迫实现了它根本不需要的方法,违反了 ISP。客户端如果调用这些方法可能会遇到异常。

遵循ISP的设计

java 复制代码
// 拆分成多个专用接口
interface Printer {
    void print(Document doc);
}

interface Scanner {
    void scan(Document doc);
}

interface FaxMachine {
    void fax(Document doc);
}

// 经济型打印机:只实现Printer
class EconomyPrinter implements Printer {
    public void print(Document doc) { ... }
}

// 中档打印机:实现打印和扫描
class MidRangePrinter implements Printer, Scanner {
    public void print(Document doc) { ... }
    public void scan(Document doc) { ... }
}

// 高级打印机:支持所有功能
class AdvancedPrinter implements Printer, Scanner, FaxMachine { ... }

// 更灵活的接口组合
interface MultiFunctionDevice extends Printer, Scanner, FaxMachine {
    // 组合功能
}

以上代码设计的好处:通过接口隔离原则实现了模块解耦,避免冗余依赖。系统扩展时,新增功能(如复印)可通过创建独立的 Copier 接口实现,无需修改现有接口或实现类,确保了高内聚、低耦合的设计目标。

7. 合成复用原则

7.1 解读

合成复用原则,Composite Reuse Principle,CRP

核心思想:尽量使用对象组合(has-a) / 聚合(contanis-a),而不是继承关系(is-a)达到软件复用的目的

继承的缺点

  • 破坏封装:子类依赖于父类的实现细节(白箱复用)。
  • 紧耦合:父类变化可能迫使子类变化,即使子类不关心这些变化。
  • 灵活性差:继承在编译时静态定义,无法在运行时动态改变。

组合/聚合的优点

  • 黑箱复用:新对象通过调用组合对象的方法来复用其功能,无需知道内部细节。
  • 松耦合:只需保证被组合对象的接口稳定。
  • 运行时动态替换:组合关系通常通过接口实现,可以在运行时动态替换具体实现。

如何选择:除非是严格的"is-a"关系且符合LSP,否则优先考虑组合。

7.2 正反举例

以汽车引擎系统为例:

违反CRP的设计

java 复制代码
// 通过继承复用
class GasEngine {
    public void start() { ... }
}

class ElectricEngine {
    public void start() { ... }
}

class GasCar extends GasEngine { // Car "is-a" Engine? 
    // 汽车除了引擎还有轮子、底盘等,不能仅通过继承引擎实现
}

class ElectricCar extends ElectricEngine { ... }

以上代码设计存在的问题:继承表达的是"is-a"关系,但汽车不是引擎,这导致逻辑混乱,且无法灵活更换引擎。

遵循CRP的设计

java 复制代码
// 定义引擎接口
interface Engine {
    void start();
}

class GasEngine implements Engine {
    public void start() { System.out.println("Gas engine started"); }
}

class ElectricEngine implements Engine {
    public void start() { System.out.println("Electric engine started"); }
}

// 汽车拥有(has-a)一个引擎
class Car {
    private Engine engine; // 组合

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start(); 
        System.out.println("Car is ready to drive!");
    }

    // 可以动态更换引擎
    public void setEngine(Engine newEngine) {
        this.engine = newEngine;
    }
}


public class Main {
    public static void main(String[] args) {
        Engine gasEngine = new GasEngine();
        Car gasCar = new Car(gasEngine);
        gasCar.start(); // Gas engine started, Car is ready...

        // 更换为电动引擎
        Engine electricEngine = new ElectricEngine();
        gasCar.setEngine(electricEngine);
        gasCar.start(); // Electric engine started, Car is ready...
    }
}

以上代码设计的好处:Car 通过组合(拥有引擎)复用引擎的功能,而不是继承引擎。更符合现实。

8. 总结

七大面向对象设计原则(SOLID + LoD + CRP)共同构成了设计模式的基础:

原则 核心思想
SRP 一个类只做一件事
OCP 对扩展开放,对修改关闭
LSP 子类必须能透明替换父类
ISP 客户端不应依赖它不需要的接口
DIP 依赖抽象,而非具体实现
LoD 减少对象间的交互,降低耦合
CRP 优先使用组合,而非继承

掌握这些原则可以创建出高内聚、低耦合易于维护、扩展和复用的代码。。

相关推荐
京东零售技术2 小时前
在京东 探索技术的无限可能
面试
寒山李白3 小时前
Java 依赖注入、控制反转与面向切面:面试深度解析
java·开发语言·面试·依赖注入·控制反转·面向切面
Gixy3 小时前
聊聊纯函数与不可变数据结构
前端·设计模式
ZzMemory3 小时前
藏起来的JS(四) - GC(垃圾回收机制)
前端·javascript·面试
Java菜鸟、3 小时前
设计模式(代理设计模式)
java·开发语言·设计模式
想用offer打牌4 小时前
面试回答喜欢用构造器注入,面试官很满意😎...
后端·spring·面试
何中应4 小时前
【设计模式-3.7】结构型——组合模式
java·设计模式·组合模式
magic 2454 小时前
Java设计模式之观察者模式详解
观察者模式·设计模式
独立开阀者_FwtCoder4 小时前
MySQL FULLTEXT索引解析:为什么它能大幅提升文本搜索性能?
前端·javascript·面试
异常君5 小时前
Java PriorityQueue 源码剖析:二叉堆的实现原理与应用
java·面试