深透析面向对象的编码设计规则

一、面向对象的五大设计原则:SOLID 原则

SOLID 是面向对象设计中的五个基础原则的缩写,分别是:

  1. 单一职责原则(Single Responsibility Principle, SRP)
  2. 开放封闭原则(Open/Closed Principle, OCP)
  3. 里氏替换原则(Liskov Substitution Principle, LSP)
  4. 接口隔离原则(Interface Segregation Principle, ISP)
  5. 依赖倒置原则(Dependency Inversion Principle, DIP)

除了 SOLID 原则之外,还有一些其他常见的面向对象设计原则:

  1. 合成复用原则(Composite Reuse Principle, CRP)
  2. 最少知识原则(Law of Demeter, LoD)
  3. 高内聚,低耦合

1. 单一职责原则(SRP)

定义:一个类应该仅有一个引起它变化的原因,即一个类只应该有一个职责。

示例

假设我们有一个类负责用户数据的管理以及日志记录,这样的类可能会变得非常复杂和难以维护。如果用户数据管理和日志记录发生变化,可能需要修改同一个类,这违背了单一职责原则。

// 违反单一职责原则
public class UserManager {
    public void createUser(String username) {
        // 创建用户逻辑
        System.out.println("User " + username + " created.");
        log("Created user: " + username);
    }

    private void log(String message) {
        System.out.println("Log: " + message);
    }
}

为了遵守单一职责原则,我们应该将日志记录和用户管理分离为两个独立的类:

// 遵守单一职责原则
public class UserManager {
    private Logger logger;

    public UserManager(Logger logger) {
        this.logger = logger;
    }

    public void createUser(String username) {
        // 创建用户逻辑
        System.out.println("User " + username + " created.");
        logger.log("Created user: " + username);
    }
}

public class Logger {
    public void log(String message) {
        System.out.println("Log: " + message);
    }
}

现在 UserManager 只负责管理用户,而 Logger 负责日志记录,符合单一职责原则。


2. 开放封闭原则(OCP)

定义:软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。

这意味着在不修改现有代码的情况下,可以通过扩展来实现新功能。

示例

假设我们有一个用于计算不同几何形状面积的类,如果我们需要添加新形状,那么就需要修改这个类,这违背了开放封闭原则。

// 违反开放封闭原则
public class AreaCalculator {
    public double calculateArea(Object shape) {
        if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return Math.PI * circle.radius * circle.radius;
        } else if (shape instanceof Rectangle) {
            Rectangle rectangle = (Rectangle) shape;
            return rectangle.width * rectangle.height;
        }
        return 0;
    }
}

为了遵循开放封闭原则,我们可以使用抽象类或接口,这样我们在添加新形状时不需要修改现有的代码:

// 遵守开放封闭原则
public interface Shape {
    double calculateArea();
}

public class Circle implements Shape {
    double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class Rectangle implements Shape {
    double width, height;

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

    @Override
    public double calculateArea() {
        return width * height;
    }
}

public class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.calculateArea();
    }
}

通过这种方式,如果我们需要添加新形状,只需要添加一个实现 Shape 接口的新类,而无需修改 AreaCalculator 类。


3. 里氏替换原则(LSP)

定义:子类对象必须能够替换掉父类对象,并且程序逻辑不应因此而改变。

里氏替换原则强调继承时保持行为一致。如果子类不能替换父类,或者在使用子类时需要额外判断或处理,说明违反了该原则。

示例

假设我们有一个矩形类和一个继承自矩形的正方形类,违反里氏替换原则的设计如下:

// 违反里氏替换原则
public class Rectangle {
    protected double width, height;

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

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

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

public class Square extends Rectangle {
    @Override
    public void setWidth(double width) {
        this.width = width;
        this.height = width; // 保证正方形的边长一致
    }

    @Override
    public void setHeight(double height) {
        this.width = height;
        this.height = height; // 保证正方形的边长一致
    }
}

Square 违反了里氏替换原则,因为正方形的行为不同于矩形。为了解决这个问题,可以将矩形和正方形分为独立的类,而不是使用继承:

// 遵守里氏替换原则
public interface Shape {
    double getArea();
}

public class Rectangle implements Shape {
    protected double width, height;

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

    @Override
    public double getArea() {
        return width * height;
    }
}

public class Square implements Shape {
    private double side;

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

    @Override
    public double getArea() {
        return side * side;
    }
}

现在,SquareRectangle 不再共享不兼容的行为,符合里氏替换原则。


4. 接口隔离原则(ISP)

定义:客户端不应该被迫依赖它不使用的方法。换句话说,接口应该为特定的客户端设计,而不是通用的“大而全”接口。

示例

如果一个接口包含过多的方法,而实现这个接口的类只需要部分方法,那么就违反了接口隔离原则:

// 违反接口隔离原则
public interface Worker {
    void work();
    void eat();
}

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

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

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

    // Robot 不需要 eat 方法
    @Override
    public void eat() {
        throw new UnsupportedOperationException();
    }
}

RobotWorker 不需要 eat() 方法,但仍然必须实现它。这违反了接口隔离原则。为了解决这个问题,可以将接口分解为更小的、专注于特定职责的接口:

// 遵守接口隔离原则
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

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

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

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

现在 RobotWorker 只需要实现 Workable 接口,而不需要 Eatable 接口,符合接口隔离原则。


5. 依赖倒置原则(DIP)

定义:高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。

依赖倒置原则的核心思想是通过依赖抽象(接口或抽象类)来降低模块之间的耦合度。

示例

假设我们有一个类直接依赖于具体的数据库实现,这违反了依赖倒置原则:

// 违反依赖倒置原则
public class UserRepository {
    private MySQLDatabase database;

    public UserRepository() {
        this.database = new MySQLDatabase();
    }

    public void save(User user) {
        database.save(user);
    }
}

UserRepository 直接依赖于 MySQLDatabase,如果我们要切换到其他数据库,就需要修改 UserRepository。为了遵守依赖倒置原则,可以引入抽象:

// 遵守依赖倒置原则
public interface Database {
    void save(User user);
}

public class MySQLDatabase implements Database {
    @Override
    public void save(User user) {
        System.out.println("Saving user to MySQL database");
    }
}

public class MongoDBDatabase implements Database {
    @Override
    public void save(User user) {
        System.out.println("Saving user to MongoDB database");
    }
}

public class UserRepository {
    private Database database;

    public UserRepository(Database database) {
        this.database = database;
    }

    public void save(User user) {
        database.save(user);
    }
}

通过依赖 Database 接口,UserRepository 不再依赖于具体的数据库实现,这使得我们能够轻松切换不同的数据库实现。

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

定义:合成复用原则主张应当尽量通过组合(或聚合)来复用代码,而不是通过继承。简单来说,它建议我们优先使用“有一个”(has-a)的关系,而不是“是一个”(is-a)的关系。

原因:
  • 继承是强耦合,子类会依赖于父类的实现,并且父类的修改可能会影响到子类。
  • 组合则是松耦合,组件之间的依赖较小,修改某个类的实现不会影响到其他类。
示例

违背合成复用原则:

假设我们有两个类:BookEBook,它们之间使用继承关系。

// 违反合成复用原则
public class Book {
    private String title;
    private String author;

    public void printBookInfo() {
        System.out.println("Title: " + title + ", Author: " + author);
    }
}

public class EBook extends Book {
    private String downloadUrl;

    public void download() {
        System.out.println("Downloading from " + downloadUrl);
    }
}

在这种设计中,EBook 继承了 Book,但实际上,EBook 并不是 Book 的一种,而是一个扩展版本,违背了合成复用原则。

遵循合成复用原则:

可以将下载功能分离为单独的类,并通过组合实现代码复用:

// 遵守合成复用原则
public class Book {
    private String title;
    private String author;

    public void printBookInfo() {
        System.out.println("Title: " + title + ", Author: " + author);
    }
}

public class Downloadable {
    private String downloadUrl;

    public void download() {
        System.out.println("Downloading from " + downloadUrl);
    }
}

public class EBook {
    private Book book;
    private Downloadable downloadable;

    public EBook(Book book, Downloadable downloadable) {
        this.book = book;
        this.downloadable = downloadable;
    }

    public void printBookInfo() {
        book.printBookInfo();
    }

    public void download() {
        downloadable.download();
    }
}

在这个设计中,EBook 通过组合 BookDownloadable 类实现功能扩展,而不是通过继承来复用代码。这样既避免了强耦合,又提高了代码的复用性和灵活性。


7. 最少知识原则(Law of Demeter, LoD)

定义:最少知识原则也称为“迪米特法则”,其核心思想是一个对象应当对其他对象有尽可能少的了解。通俗地讲,类与类之间的通信应当尽量简单,避免过多的依赖或嵌套调用。

具体来说,最少知识原则规定对象只能调用:

  • 本对象的方法。
  • 该对象的字段(成员)的方法。
  • 方法参数对象的方法。
  • 创建对象的方法。
原因:
  • 减少类之间的依赖,提高代码的松耦合性。
  • 避免“链式调用”,提高系统的健壮性和可维护性。
示例

违反最少知识原则的设计:

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

public class Car {
    private Engine engine;

    public Car() {
        engine = new Engine();
    }

    public Engine getEngine() {
        return engine;
    }
}

public class Driver {
    public void drive(Car car) {
        car.getEngine().start();  // 违反了最少知识原则,Driver不应直接依赖Engine
    }
}

在这个例子中,Driver 类不仅依赖于 Car 类,还依赖于 Engine 类,这违反了最少知识原则。

遵循最少知识原则的设计:

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

public class Car {
    private Engine engine;

    public Car() {
        engine = new Engine();
    }

    public void start() {
        engine.start();  // 通过封装,将Engine的操作隐藏起来
    }
}

public class Driver {
    public void drive(Car car) {
        car.start();  // 遵守最少知识原则,Driver只和Car交互
    }
}

在这个设计中,Driver 只与 Car 交互,而不关心 Engine 的内部实现。通过封装细节,遵循了最少知识原则,减少了类之间的耦合。


8. 高内聚,低耦合

定义

  • 高内聚:一个模块或类的职责范围应尽量单一,所有功能应该高度相关,且彼此紧密联系。
  • 低耦合:模块或类之间的依赖关系应尽量少,保持松散的耦合,使得一个模块的变化不会引起其他模块的变化。
原因:
  • 高内聚有助于模块独立性,增强可读性和可维护性。
  • 低耦合有助于降低模块之间的依赖,提高系统的灵活性和扩展性。
示例

违反高内聚,低耦合的设计:

假设我们有一个 OrderProcessor 类,负责订单的处理、支付、库存更新等多种功能。

// 违反高内聚,低耦合原则
public class OrderProcessor {
    public void processOrder(Order order) {
        validateOrder(order);
        processPayment(order);
        updateInventory(order);
        generateInvoice(order);
    }

    private void validateOrder(Order order) {
        // 验证订单
    }

    private void processPayment(Order order) {
        // 处理付款
    }

    private void updateInventory(Order order) {
        // 更新库存
    }

    private void generateInvoice(Order order) {
        // 生成发票
    }
}

在这个例子中,OrderProcessor 的职责过多,它负责订单的各个方面,包括验证、付款、库存更新和发票生成。这样做降低了内聚性,并且使得类的修改频率增加,增加了耦合性。

遵循高内聚,低耦合的设计:

我们可以将每个职责分离到独立的类中,以提高内聚性并降低耦合性。

// 遵循高内聚,低耦合原则
public class OrderValidator {
    public void validate(Order order) {
        // 验证订单
    }
}

public class PaymentProcessor {
    public void processPayment(Order order) {
        // 处理付款
    }
}

public class InventoryUpdater {
    public void updateInventory(Order order) {
        // 更新库存
    }
}

public class InvoiceGenerator {
    public void generateInvoice(Order order) {
        // 生成发票
    }
}

public class OrderProcessor {
    private OrderValidator validator;
    private PaymentProcessor paymentProcessor;
    private InventoryUpdater inventoryUpdater;
    private InvoiceGenerator invoiceGenerator;

    public OrderProcessor(OrderValidator validator, PaymentProcessor paymentProcessor,
                          InventoryUpdater inventoryUpdater, InvoiceGenerator invoiceGenerator) {
        this.validator = validator;
        this.paymentProcessor = paymentProcessor;
        this.inventoryUpdater = inventoryUpdater;
        this.invoiceGenerator = invoiceGenerator;
    }

    public void processOrder(Order order) {
        validator.validate(order);
        paymentProcessor.processPayment(order);
        inventoryUpdater.updateInventory(order);
        invoiceGenerator.generateInvoice(order);
    }
}

现在,每个类只负责一个特定的职责,从而提高了每个类的内聚性。而 OrderProcessor 类通过组合这些独立的类来实现订单处理,降低了耦合性。


结论

  1. 合成复用原则 建议我们优先使用组合而非继承来实现代码复用,这样可以避免不必要的耦合,并提高系统的灵活性。

  2. 最少知识原则 强调尽量减少类与类之间的依赖,避免“链式调用”,从而降低系统的复杂性和维护成本。

  3. 高内聚低耦合 则是指导我们如何设计类和模块,使它们各自职责明确,彼此独立,从而提高代码的可维护性和可扩展性。

通过遵循这些原则,开发者可以设计出更加健壮、灵活、易于维护的系统。在实际开发中,时刻牢记这些原则,并根据实际场景灵活应用,会使代码质量得到显著提升。

你可能感兴趣的:(java,前端,javascript)