软件设计原则
软件设计原则又简称为 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)是指尽量使用组合或聚合关系来实现对象的复用,而不是使用继承关系来实现。这意味着一个软件实体应该优先通过组合或聚合来实现代码复用,而不是通过继承来实现。继承我们叫做白箱复用,相当于把所有的实现细节暴露给子类,组合/聚合也称为黑箱复用,对类以外的对象是无法获取到实现细节的。如果子类与父类的耦合度高,父类的任何改变都会导致子类的实现发生变化,这不利于类的扩展。这个原则的目的是降低类之间的耦合度,提高系统的灵活性和可维护性,一个类的变化对其他类造成的影响相对较少。例如,我们可以将一个复杂的功能拆分成多个简单的类或模块,然后通过组合的方式来实现这个功能。
总结
其实上面这些原则都有一个共同的目标,就是尽量面向抽象编程,降低类之间的耦合度,提高系统的灵活性、可扩展性和可维护性。强调面向对象编程的核心思想,即抽象、封装、继承和多态。
软件设计原则是面向对象编程的基础,它们是设计模式的基石,是编写高质量代码的关键。它可以帮我们编写出更加灵活、可维护、可扩展的代码,提高代码的质量和效率。在实际开发中,我们应该遵循这些原则,尽量将它们应用到我们的代码中。但是,这些原则并不是一成不变的,它们是相互关联、相互影响的,我们应该根据具体的情况来灵活运用这些原则,以满足实际需求,而不是教条地遵循这些原则。