软件设计原则

软件设计原则又简称为 SOLID 原则,设计模式主要是基于 OOP 编程提炼的,它基于以下几个原则。

单一职责原则

单一职责原则(Single Responsibility Principle,SRP)是指一个类只负责一个功能领域中的职责,或者可以定义为一个类只有一个引起它变化的原因,否则类应该被拆分。如果一个类承担了过多的职责,就可能导致代码的耦合度过高,难以维护和扩展。这个原则的目的是将一个类的职责分解为多个独立的类,这样可以提高类的内聚性,降低它们的耦合性,提高类的复用性。

示例

假如有一个类用于处理日志消息,但是这个类不仅仅负责创建日志消息,还负责将其写入文件。根据单一职责原则,我们应该将这两个职责分开,让一个类专注于创建日志消息,而另一个类专注于日志消息的存储。

class LogMessageCreator {
    public String createLogMessage(String message) {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + ": " + message;
    }
}

class LogMessageWriter {
    public void writeLogMessage(String message) throws IOException {
        try (FileWriter writer = new FileWriter("log.txt", true)) {
            writer.write(message + "\n");
            writer.flush();
        }
    }
}

public class Logger {
    private LogMessageCreator logMessageCreator;
    private LogMessageWriter logMessageWriter;

    public Logger() {
        this.logMessageCreator = new LogMessageCreator();
        this.logMessageWriter = new LogMessageWriter();
    }

    public void log(String message) {
        String logMessage = logMessageCreator.createLogMessage(message);
        logMessageWriter.writeLogMessage(logMessage);
    }
}

开闭原则

开闭原则(Open-Closed Principle,OCP)是指一个软件实体(如类、模块和函数)应该对扩展开放,对修改关闭。这意味着一个软件实体应该通过扩展来实现新的功能,而不是通过修改已有的代码来实现。这个原则的目的是使软件实体在不修改的情况下可以被扩展,以满足新的需求。实现开闭原则的核心思想是面向抽象编程,用抽象构建框架,用实现扩展细节。例如,我们可以使用抽象类或接口来定义一个通用的框架,然后通过继承或实现来添加新的功能,而不是直接修改已有代码。

示例

假设有一个图形绘制应用程序,其中有一个 Shape 类。在遵守开闭原则的情况下,如果要添加新的形状类型,应该能够扩展 Shape 类而无需修改现有代码。这可以通过创建继承自 Shape 的新类来实现,如 Circle 和 Rectangle。

interface Shape {
    void draw();
}

class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("绘制圆形");
    }
}

class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("绘制矩形");
    }
}

/**
 * 绘制图形
 */
public class DrawingApp {
    public void draw(Shape shape) {
        shape.draw();
    }
}

里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP)是指一个对象应该可以被它的子类替换,而不影响程序的正确性。这意味着一个软件实体应该在任何使用基类(父类)的地方都可以替换成子类,而不必改变程序的逻辑。这个原则的目的是保证继承关系正确使用,避免继承导致的问题。实现里氏替换原则的关键是抽象化,抽象化是面向对象编程的核心思想,它可以通过接口或抽象类来实现。例如,我们可以使用接口或抽象类来定义一个通用的规范,然后通过实现来提供具体的功能,而不是直接使用具体的实现类。

示例

假设有一个函数接受 Car 对象作为参数,根据里氏替换原则,这个函数应该也能接受 Car 的子类对象,而不影响程序运行。

class Car {
    public void drive() {
        System.out.println("开车");
    }
}

class ElectricCar extends Car {
    @Override
    public void drive() {
        System.out.println("开电车");
    }
}

class OilCar extends Car {
    @Override
    public void drive() {
        System.out.println("开油车");
    }
}

public class CarDemo {
    public static void main(String[] args) {
        ElectricCar electricCar = new ElectricCar();
        driveCar(electricCar);

        OilCar oilCar = new OilCar();
        driveCar(oilCar);
    }
}

public static void driveCar(Car car) {
    car.drive();
}

依赖倒置原则

依赖倒置原则(Dependency Inversion Principle,DIP)是指高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于具体,具体应该依赖于抽象。这意味着一个软件实体应该通过抽象接口来与依赖对象通信,而不是直接依赖于具体实现。这个原则的目的是降低模块之间的耦合度,提高系统的稳定性和可维护性。实现依赖倒置原则的关键是面向接口编程,通过接口来定义规范,然后通过实现来提供具体的功能。例如,我们可以使用接口来定义一个通用的规范,然后通过实现来提供具体的功能,而不是直接使用具体的实现类。

接口隔离原则

接口隔离原则(Interface Segregation Principle,ISP)是指一个类对另一个类的依赖应该建立在最小的接口上。这意味着一个类不应该依赖于它不需要的接口,或者说一个类对另一个类的依赖应该建立在最小的接口上。实现接口隔离原则的关键是定义多个小而专用的接口,而不是定义一个大而全的接口。这个原则的目的是降低类之间的耦合度,提高系统的灵活性和可维护性。

迪米特法则

迪米特法则(Law of Demeter,LoD)又称为最少知道原则(Least Knowledge Principle,LKP),是指一个对象应该对其他对象有尽可能少的了解,也就是说一个对象应该对其他对象有尽可能少的依赖。如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。迪米特原则主要强调只和朋友交流,不和陌生人说话。出现在成员变量、方法的输入、输出参数中的类都可以称之为成员朋友类,而出现在方法体内部的不属于朋友类。实现迪米特法则的关键是封装,封装是面向对象编程的核心思想,它可以通过接口或抽象类来实现。其目的是降低类之间的耦合度,提高模块的相对独立性。

示例

比如我到外面去吃饭,买单时,我拿手机去扫码付款,正常流程是这样的。一种不正常的流程是,我把手机给服务员,服务员拿我的手机进行扫码支付。

  • 服务员拿我的手机进行扫码付款
/**
 * 手机
 */
class Phone {
    // 余额  
    int money = 100;
}

/**
 * 我(消费者)
 */
class Customer {
    Phone phone = new Phone();
    public Phone getPhone() {
        return this.phone;
    }  
}

/**
 * 买单(服务员)
 */
public void pay(Customer customer, int cost) {
    /**
     * 先拿到我的手机,然后再扣钱
     */
    customer.getPhone().money -= cost;
}

public static void main(String[] args) {
    Customer customer = new Customer();
    pay(customer, 10);
}
  • 我自己拿手机扫码
/**
 * 手机
 */
class Phone {
    // 余额  
    int money = 100;
}

/**
 * 我(消费者)
 */
class Customer {
    Phone phone = new Phone();
    public void pay(int cost) {
        this.phone.money -= cost;
    }  
}

/**
 * 买单(服务员)
 */
public void pay(Customer customer, int cost) {
    /**
     * 我自己拿手机扫码
     */
    customer.pay(cost);
}

public static void main(String[] args) {
    Customer customer = new Customer();
    pay(customer, 10);
}

也就是说,第三方不要通过 objA.getObjB().getObjC().method() 获取外部对象,而应该在 objA 的内部封装一个具体方法提供给外部调用。

组合/聚合(合成)复用原则

组合/聚合(合成)复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用组合或聚合关系来实现对象的复用,而不是使用继承关系来实现。这意味着一个软件实体应该优先通过组合或聚合来实现代码复用,而不是通过继承来实现。继承我们叫做白箱复用,相当于把所有的实现细节暴露给子类,组合/聚合也称为黑箱复用,对类以外的对象是无法获取到实现细节的。如果子类与父类的耦合度高,父类的任何改变都会导致子类的实现发生变化,这不利于类的扩展。这个原则的目的是降低类之间的耦合度,提高系统的灵活性和可维护性,一个类的变化对其他类造成的影响相对较少。例如,我们可以将一个复杂的功能拆分成多个简单的类或模块,然后通过组合的方式来实现这个功能。

总结

其实上面这些原则都有一个共同的目标,就是尽量面向抽象编程,降低类之间的耦合度,提高系统的灵活性、可扩展性和可维护性。强调面向对象编程的核心思想,即抽象、封装、继承和多态。

软件设计原则是面向对象编程的基础,它们是设计模式的基石,是编写高质量代码的关键。它可以帮我们编写出更加灵活、可维护、可扩展的代码,提高代码的质量和效率。在实际开发中,我们应该遵循这些原则,尽量将它们应用到我们的代码中。但是,这些原则并不是一成不变的,它们是相互关联、相互影响的,我们应该根据具体的情况来灵活运用这些原则,以满足实际需求,而不是教条地遵循这些原则。