设计模式原则分析

1. 单一职责原则(Single Responsibility Principle)

1.1 定义

单一职责原则是指一个类应该只有一个责任(即一个功能),一个类应该只负责一项任务。换句话说,如果一个类承担了多个职责,这些职责之间会有不同的变化原因,改变其中某个职责时,可能会影响到其他职责,导致系统更加复杂和难以维护。

1.2 不符合SRP原则举例

假设有一个类同时处理数据存储和报告生成的功能。当存储方式变化时(如从数据库变为文件存储),我们需要修改这个类的存储相关代码;而当报告生成的需求变化时(如格式从 PDF 变为 Excel),我们也需要修改这个类的报告生成代码。此时,这个类有两个职责,它们各自有不同的变化原因,导致这个类的维护变得复杂。

// 违反 SRP 原则:一个类同时负责数据存储和报告生成
public class UserManager {
    public void saveUser(User user) {
        // 保存用户到数据库
        System.out.println("Saving user to database");
    }

    public void generateUserReport(User user) {
        // 生成用户报告
        System.out.println("Generating report for user");
    }
}

1.3 遵守SRP原则举例

// 遵守 SRP 原则:将职责分离到不同的类中
public class UserRepository {
    public void saveUser(User user) {
        // 保存用户到数据库
        System.out.println("Saving user to database");
    }
}

public class UserReportGenerator {
    public void generateUserReport(User user) {
        // 生成用户报告
        System.out.println("Generating report for user");
    }
}

在这个例子中,UserRepository 只负责数据存储,而 UserReportGenerator 只负责生成报告。这样,每个类只负责一个功能,系统的职责划分更加清晰,修改某一功能时只需要修改对应的类,而不影响其他部分。

2. 开闭原则(Open/Closed Principle)

2.1 定义

开放-封闭原则指的是软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着当我们需要为系统添加新功能时,应该通过扩展已有的代码,而不是直接修改已有的代码。

2.2 不符合OCP原则举例

假设我们有一个计算薪水的程序,原本只考虑基本工资和奖金两个因素。如果以后需要加入更多的薪酬计算因素(如补贴、扣款等),如果我们每次都修改原有的计算逻辑,就会违反开放-封闭原则。

// 违反 OCP 原则:修改原有类以支持新的薪资计算方式
public class SalaryCalculator {
    public double calculateSalary(Employee employee) {
        // 计算基本工资和奖金
        return employee.getBaseSalary() + employee.getBonus();
    }
}

假设现在我们需要新增一个“补贴”因素,每次添加新因素时都需要修改 SalaryCalculator 类的 calculateSalary 方法。随着需求变化,SalaryCalculator 类会变得越来越复杂,而且容易出错。这就是违反了开放-封闭原则。

2.3 符合OCP原则举例

为了遵守开放-封闭原则,我们可以引入抽象类或接口,将不同的薪资计算策略封装成不同的类,而不是修改 SalaryCalculator 类。

// 遵守 OCP 原则:将薪资计算逻辑通过策略模式封装,便于扩展
public interface SalaryCalculatorStrategy {
    double calculateSalary(Employee employee);
}

public class BaseSalaryAndBonusCalculator implements SalaryCalculatorStrategy {
    @Override
    public double calculateSalary(Employee employee) {
        // 计算基本工资和奖金
        return employee.getBaseSalary() + employee.getBonus();
    }
}

public class BaseSalaryBonusAndSubsidyCalculator implements SalaryCalculatorStrategy {
    @Override
    public double calculateSalary(Employee employee) {
        // 计算基本工资、奖金和补贴
        return employee.getBaseSalary() + employee.getBonus() + employee.getSubsidy();
    }
}

public class SalaryCalculator {
    private SalaryCalculatorStrategy strategy;

    public SalaryCalculator(SalaryCalculatorStrategy strategy) {
        this.strategy = strategy;
    }

    public double calculateSalary(Employee employee) {
        return strategy.calculateSalary(employee);
    }
}

我们将计算薪水的逻辑提取到不同的策略类中,并定义了一个共同的 SalaryCalculatorStrategy 接口。每个薪资计算策略都实现了这个接口,分别处理不同的计算逻辑(如仅包含基本工资和奖金,或包含补贴等)。通过策略模式,我们能够灵活地扩展薪资计算方式,而无需修改现有的代码。如果以后还需要新增其他薪资计算方式,只需要新建一个实现 SalaryCalculatorStrategy 接口的类,而不需要修改 SalaryCalculator 类。

3. 里氏替换原则 (LSP)

3.1 定义

里氏替换原则是指子类对象应该能够替换父类对象,且程序的行为不受影响。换句话说,子类必须完全继承父类的行为,并且在替换父类实例时不会破坏父类的行为和功能。

3.2 符合LSP原则举例

假设我们有一个形状类 Shape,其中有一个方法 area() 来计算图形的面积。然后我们有两个子类:RectangleSquare。根据几何学,矩形的面积是 宽 * 高,而正方形的面积是 边长 * 边长。但是,假如我们在正方形类中重写了 setWidthsetHeight 方法,使其不再符合父类 Rectangle 的行为,那么就违反了里氏替换原则。

// 违反 LSP 原则:正方形类破坏了矩形类的预期行为
class Rectangle {
    private double width;
    private double height;

    public void setWidth(double width) {
        this.width = width;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    public double area() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setHeight(width);  // 强制宽度和高度相等,违反矩形的行为
    }

    @Override
    public void setHeight(double height) {
        super.setHeight(height);
        super.setWidth(height);  // 强制宽度和高度相等,违反矩形的行为
    }
}

正方形是矩形的一种特殊情况,但正方形并不等同于矩形。矩形的宽度和高度是独立的,而正方形的宽度和高度始终相等。如果我们让正方形继承矩形类,并且让 setWidthsetHeight 强制设置相等的值,这将破坏矩形类的预期行为。当我们将一个 Square 对象作为 Rectangle 类型的对象传递时,程序的行为会不符合预期,setWidthsetHeight 会强制设置相等的值,而这在矩形的情况下并不应该这样。

3.3 符合LSP原则举例

为了遵守里氏替换原则,我们应该避免让 Square 直接继承 Rectangle,因为它违反了矩形的行为。相反,我们可以通过创建一个新的抽象类 Shape 来统一接口,RectangleSquare 各自继承并实现具体的行为。

// 遵守 LSP 原则:通过抽象类和接口来正确设计
abstract class Shape {
    public abstract double area();
}

class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double area() {
        return width * height;
    }
}

class Square extends Shape {
    private double side;

    public Square(double side) {
        this.side = side;
    }

    public double area() {
        return side * side;
    }
}

Shape 类作为一个抽象类,定义了所有图形的公共接口 area() 方法。

RectangleSquare 都继承 Shape 类,但它们分别根据自己的特点实现 area() 方法,而不再强行使用不合适的继承关系。

正确的继承RectangleSquare 的行为各自独立,满足了不同的需求。正方形和矩形之间的差异得到了合理的隔离。

4. 接口隔离原则(ISP)

4.1 定义

接口隔离原则指的是不应该强迫一个类去依赖它不需要的接口。换句话说,接口应该尽可能地小,避免设计过于庞大和臃肿的接口。客户端不应该被迫实现它不使用的方法。

简单理解

  • 如果一个类依赖了一个接口,并且这个接口中有一些方法是该类不需要的,那么就违反了接口隔离原则。
  • 应该将一个大接口拆分成多个小接口,每个接口承担一个独立的职责,这样类只需要实现它需要的接口,而不是实现整个庞大的接口。

4.2 不符合ISP原则举例

假设我们有一个 Worker 接口,包含了多种不同的功能方法,像是 workeatsleep 等。然而,某些具体的实现类(比如 Robot)并不需要所有这些方法,比如它不需要 eatsleep 方法。这个接口设计就违反了接口隔离原则。

// 违反 ISP 原则:接口包含了不需要的方法
public interface Worker {
    void work();
    void eat();
    void sleep();
}

public class Human implements Worker {
    @Override
    public void work() {
        System.out.println("Human working...");
    }

    @Override
    public void eat() {
        System.out.println("Human eating...");
    }

    @Override
    public void sleep() {
        System.out.println("Human sleeping...");
    }
}

public class Robot implements Worker {
    @Override
    public void work() {
        System.out.println("Robot working...");
    }

    @Override
    public void eat() {
        // Robot doesn't need to implement this method
    }

    @Override
    public void sleep() {
        // Robot doesn't need to implement this method
    }
}

在这个例子中,Worker 接口定义了 eatsleep 方法,但对于 Robot 类而言,这些方法并不适用,因此需要实现这些无用的方法,违反了接口隔离原则。Robot 类并不需要 eatsleep 方法,但是由于接口设计不合理,它不得不提供这些空实现,从而导致了不必要的代码。

4.3 符合ISP原则举例

为了解决这个问题,我们可以将接口拆分成多个小接口,每个接口仅包含客户端需要的方法。Human 类实现包含 workeatsleep 的接口,而 Robot 类只实现 work 方法相关的接口。

// 遵守 ISP 原则:将接口拆分成多个小接口
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

public class Human implements Workable, Eatable, Sleepable {
    @Override
    public void work() {
        System.out.println("Human working...");
    }

    @Override
    public void eat() {
        System.out.println("Human eating...");
    }

    @Override
    public void sleep() {
        System.out.println("Human sleeping...");
    }
}

public class Robot implements Workable {
    @Override
    public void work() {
        System.out.println("Robot working...");
    }
}

5. 依赖倒置原则(Dependency Inversion Principle)

5.1 定义

依赖倒置原则是指高层模块不应该依赖低层模块,二者都应该依赖于抽象抽象不应该依赖细节,细节应该依赖于抽象

简单来说,依赖倒置原则强调的是通过依赖于抽象(如接口或抽象类),而不是依赖于具体实现,从而实现高层模块和低层模块的解耦。

5.2 不符合DIP原则举例

假设我们有一个 UserService 类,它依赖于 UserRepository 类来进行数据存储。如果 UserService 直接依赖于 UserRepository 类,这样就违反了依赖倒置原则,因为高层模块(UserService)直接依赖于低层模块(UserRepository)。

// 违反 DIP 原则:高层模块直接依赖低层模块
class UserRepository {
    public void save(User user) {
        // 保存用户到数据库
        System.out.println("User saved to database.");
    }
}

class UserService {
    private UserRepository userRepository;

    public UserService() {
        userRepository = new UserRepository();  // 直接依赖具体实现
    }

    public void registerUser(User user) {
        // 注册用户
        userRepository.save(user);
    }
}
  • UserService 直接依赖于 UserRepository 的实现。这样,如果我们需要更改 UserRepository 的实现,比如从数据库改为从文件中保存数据,UserService 就需要修改其代码,违反了高层模块不应该依赖低层模块的原则。
  • 如果以后需要实现不同类型的存储方式,比如 FileRepositoryMongoDBRepository,每次都需要修改 UserService,导致代码不容易维护和扩展。

5.3 符合DIP原则举例

为了遵守依赖倒置原则,我们应该引入一个抽象接口,UserRepository 应该是一个接口,而 UserService 只依赖这个接口,而不依赖具体实现类。具体的实现类可以通过依赖注入的方式传入。

// 遵守 DIP 原则:通过接口隔离高层模块和低层模块
interface UserRepository {
    void save(User user);
}

class UserRepositoryImpl implements UserRepository {
    @Override
    public void save(User user) {
        // 保存用户到数据库
        System.out.println("User saved to database.");
    }
}

class FileRepository implements UserRepository {
    @Override
    public void save(User user) {
        // 保存用户到文件
        System.out.println("User saved to file.");
    }
}

class UserService {
    private UserRepository userRepository;

    // 通过构造器注入依赖
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void registerUser(User user) {
        // 注册用户
        userRepository.save(user);
    }
}

6. 合成复用原则(Composite Reuse Principle)

6.1 定义

合成复用原则是指优先使用对象的合成(组合)而非继承来实现代码复用。这意味着,通过将已有的类作为成员对象嵌入到新类中,来增强新类的功能,而不是通过继承扩展原有类的功能。

组合优于继承:当需要复用某些功能时,尽量通过组合来实现,而不是通过继承。这是因为继承在很多情况下会导致类之间的耦合性增加,而组合则更灵活且易于扩展。

6.2 不符合CRP原则举例

假设我们有一个 Bird 类和一个 Penguin 类,PenguinBird 的子类。我们在 Bird 类中定义了一些行为(如飞行),但是企鹅是不能飞的。如果我们用继承来实现复用,这样就会导致不合理的设计。

// 违反合成复用原则:企鹅类继承了鸟类,导致不适用的行为
class Bird {
    public void fly() {
        System.out.println("Bird is flying.");
    }
}

class Penguin extends Bird {
    // Penguin 是鸟类的一种,但企鹅不能飞
    @Override
    public void fly() {
        // 企鹅不能飞,但继承了飞行行为,导致设计不合理
        System.out.println("Penguins can't fly.");
    }
}

6.3 符合CRP原则举例

为了遵守合成复用原则,我们可以将飞行行为抽象出来,并将其作为一个接口或者类的成员。这样,企鹅类就不再需要继承鸟类,而是根据实际需要来组合不同的行为。

// 遵守合成复用原则:通过组合来复用功能
interface Flyable {
    void fly();
}

class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("Bird is flying.");
    }
}

class Penguin {
    // 企鹅并不具备飞行能力,不需要继承 Bird 类
    // 如果需要飞行,可以通过组合一个 Flyable 对象
    private Flyable flyable;

    public Penguin(Flyable flyable) {
        this.flyable = flyable;
    }

    public void tryToFly() {
        // 企鹅不能飞,但如果需要,可以选择其他飞行方式
        if (flyable != null) {
            flyable.fly();
        } else {
            System.out.println("Penguins can't fly.");
        }
    }
}

class Test {
    public static void main(String[] args) {
        // 创建一个可以飞的鸟类
        Bird bird = new Bird();
        Penguin penguin = new Penguin(null);  // 企鹅没有飞行能力
        penguin.tryToFly();  // 输出:Penguins can't fly.

        // 如果需要,可以通过组合飞行的能力
        Penguin flyingPenguin = new Penguin(bird);
        flyingPenguin.tryToFly();  // 输出:Bird is flying.
    }
}

7. 最少知识原则(迪米特法则)

7.1 定义

最少知识原则,又称迪米特法则,其核心思想是一个对象应该对其他对象有尽可能少的了解。也就是说,一个对象应该只与自己直接的朋友(即直接相关的对象)进行交互,而不应该与“陌生人”或“陌生人的朋友”进行交互。

7.2 不符合迪米特法则举例

假设我们有一个 Customer 类,它有一个 Order 类属性,而 Order 类又包含 Product 类。如果 Customer 直接访问 Product 类的属性,这样就违反了最少知识原则。

class Product {
    public String name;
    public double price;
}

class Order {
    private Product product;

    public Order(Product product) {
        this.product = product;
    }

    public Product getProduct() {
        return product;
    }
}

class Customer {
    private Order order;

    public Customer(Order order) {
        this.order = order;
    }

    // 违反最少知识原则:直接访问了 Product 对象的属性
    public void printProductName() {
        System.out.println(order.getProduct().name);  // 访问了 order -> product -> name
    }
}

7.3 符合迪米特法则举例

class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }
}

class Order {
    private Product product;

    public Order(Product product) {
        this.product = product;
    }

    public String getProductName() {
        return product.getName();
    }
}

class Customer {
    private Order order;

    public Customer(Order order) {
        this.order = order;
    }

    // 遵守最少知识原则:只通过 Order 类与 Product 交互
    public void printProductName() {
        System.out.println(order.getProductName());
    }
}
最后修改:2024 年 12 月 16 日
如果觉得我的文章对你有用,请随意赞赏