SOLID原则是一组设计原则,它们旨在帮助开发人员创建易于维护和可扩展的软件系统,这些原则的缩写代表以下5个原则:
1. 单一职责原则(SRP):一个类应该只有一个职责。
2. 开闭原则(OCP):软件实体应该对扩展开放,对修改关闭。
3. 里式替换原则(LSP):子类应该能够替换其父类并保持系统行为的一致性。
4. 接口隔离原则(ISP):客户端不应该依赖于它不使用的接口。
5. 依赖反转原则(DIP):高层模块不应该依赖于底层模块,它们应该通过抽象接口进行交互。
这些原则旨在提高软件系统的可维护性、可扩展性、可重用性和可测试性,使代码更加灵活、易于理解和修改。
SRP
SRP(Single Responsibility Principle)是SOLID原则中的第一个原则,也是面向对象设计最重要的原则之一。它的原则是:一个类应该只有一个责任。
简单地说,SRP要求将一个类的功能和职责限制在一个单独的领域内,避免一个类承担多个不相关的职责,而导致复杂、难以理解和维护的代码。
以下是SRP原则的几个关键点:
1. 每个类应该有一个明确的目的或职责,且该职责应该是单一的。
2. 一个类中应该只有一个引起它变化的原因。如果一个类有多个职责,那么这些职责会彼此耦合,导致一方面的改变会对另一方面产生影响,从而导致难以维护的代码。
3. SRP不是要求我们将每个类都写得非常小,而是要求我们遵循一种组织代码的原则,将代码组织得清晰和可读性高。
下面是一个违反SRP原则的示例:
public class Employee {
public void calculatePay() {...}
public void saveEmployee() {...}
public void promoteEmployee() {...}
}
上面的示例中,Employee类承担了三个职责:代表一个雇员、计算薪水、保存和推广员工。这违反了SRP原则,因为Employee类不应该承担那么多责任。
下面是一个遵循SRP原则的重构后的示例:
public class Employee {
// responsibilities related to employee data management
public void saveEmployee() {...}
// responsibilities related to employee promotions
public void promoteEmployee() {...}
// responsibilities related to employee salary calculations
public void calculatePay() {...}
}
public class EmployeeDao {
public void saveEmployee(Employee employee) {...}
}
public class EmployeeCalculator {
public void calculatePay(Employee employee) {...}
}
上面的示例中,我们将Employee类的职责分为三部分:维护员工数据、员工晋升和计算员工工资。三部分职责都被赋予了独立的类,每个类都可以专注于完成特定的职责,代码更易于维护和扩展。
OCP
OCP(Open-Closed Principle)是SOLID原则中的第二个原则。它的原则是:软件实体(类、模块、函数等)应该对扩展开放(Open)而对修改关闭(Closed)。
简单地说,OCP要求软件实体应该可以通过添加新的代码来扩展其行为,而不是通过修改现有的代码来改变其行为,从而避免导致其他模块或类出现问题。
以下是OCP原则的几个关键点:
1. 开放扩展,指的是允许在不修改可能会影响其他代码的情况下,添加新的功能和特性。
2. 关闭修改,指的是防止已有的代码被修改,而导致出现新的问题或引入新的错误。
3. 为了遵循OCP原则,可以使用抽象化方式,将变化预备在代码中。比如,定义一个接口,实现该接口的类都可以执行某种行为,新增功能时,则新建一个实现该接口的类即可。
下面是一个违反OCP原则的示例:
public class Car {
private String make;
private int model;
private int year;
public void startEngine() {...}
public void stopEngine() {...}
public void accelerate() {...}
public void brake() {...}
public void turnUpHeater() {...}
}
上面的示例中,Car类有一段代码用于调节汽车加热器的温度,如果增加了新的功能,就会导致这个类发生变化,违反了OCP原则。
下面是一个遵循OCP原则的重构后的示例:
public interface Car {
public void startEngine();
public void stopEngine();
public void accelerate();
public void brake();
}
public interface CarHeater {
public void turnUpHeater();
}
public class Ferrari implements Car {
public void startEngine() {...}
public void stopEngine() {...}
public void accelerate() {...}
public void brake() {...}
}
public class Audi implements Car {
public void startEngine() {...}
public void stopEngine() {...}
public void accelerate() {...}
public void brake() {...}
}
public class FerrariHeater implements CarHeater {
public void turnUpHeater() {...}
}
public class AudiHeater implements CarHeater {
public void turnUpHeater() {...}
}
上面的示例中,我们将CarHeater抽象成一个接口,定义了温度调节的行为,让不同的汽车都继承Car接口,对应实现自己的具体操作。这样,我们可以通过添加新的CarHeater实现类来增加新的功能,不需要修改Car类的代码,遵循了OCP原则。
LSP
LSP(Liskov Substitution Principle),即里氏替换原则,是SOLID原则中的第三个原则。它的原则是:任何使用父类引用的地方,都应该能够被其子类代替,且程序不会出错。
简单来说,就是子类对象可以在任何需要父类对象的场合替代父类对象而不会影响程序的正确性。
Java中的编译器强制规则(静态类型检查):
1.子类型可以增加方法,但不可删
2.子类型需要实现抽象类型 (接口、抽象类)中所有未实现的方法
3.子类型中重写的方法必须有相同或子类型的返回值或者符合co-variant的参数
4.子类型中重写的方法必须使用同样类型的参数或者符合contra-variant的参数(此种情况Java目前按照重载overload处理)
5.子类型中重写的方法不能抛出额外的异常
也适用于指定的行为(方法):更强的不变量、更弱的前置条件、更强的后置条件
LSP是一种子类型关系的特殊定义,称为强行为子类型化。
LSP原则举例:
class Car extends Vehicle {
int fuel;
boolean engineOn;
//@ invariant speed < limit;
//@ invariant fuel >= 0;
//@ requires fuel > 0 && !engineOn;
//@ ensures engineOn;
void start() { … }
void accelerate() { … }
//@ requires speed != 0;
//@ ensures speed < \old(speed)
void brake() { … }
}
class Hybrid extends Car {
int charge;
//@ invariant charge >= 0;
//@ requires (charge > 0
|| fuel > 0) && !engineOn;
//@ ensures engineOn;
void start() { … }
void accelerate() { … }
//@ requires speed != 0;
//@ ensures speed < \old(speed)
//@ ensures charge > \old(charge)
void brake() { … }
}
从代码一到代码二,子类满足相同的不变量(以及额外的不变量),start方法前置条件更弱,brake方法后置条件更强。
在编程语言中,LSP依赖于以下限制:
协变:
父类型→子类型:越来越具体specific;返回值类型:不变或变得更具体;异常的类型:也是如此。
数组是协变的,泛型不是协变的。(类型擦拭)
如何实现两个泛型类的协变?
可采用通配符实现两个泛型类的协变。
无限定通配符?使用的两种情况:
情况1:方法的实现不依赖于类型参数(不调用其中的方法);
情况2:或者只依赖于Object 类中的功能
无限定通配符,一般用于定义一个引用变量,其可以指向多个不同类型的变量:
SuperClass<?> sup0 = new SuperClass<String>();
sup0 = new SuperClass<People>();
sup0 = new SuperClass<Animal>();
<? super A> 下限通配符
<? extends A> 上限通配符
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
List<Integer> li = Arrays.asList(1, 2, 3);
List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
注:List<? extends Number> list,意味着list可以匹配多种类型中的一种,但并不意味着同一个list可以存放所有的这些类型,无限定通配符和下限通配符同理。
一个类型变量如果有多个限定(类或接口),则它是所有限定类型的子类型;如果多个限定中有类(至多只允许一个类),要写到声明的最前面。
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
限定的类型参数允许调用限定类型中的方法。
反协变(逆变):
父类型→子类型:越来越具体specific
参数类型:要相反的变化,要不变或越来越抽象
注:目前Java中遇到参数逆变、参数协变都当作overload处理。
ISP
ISP (Interface Segregation Principle) 接口隔离原则是 SOLID 原则中的第四个原则,它的基本思想是:将一个庞大的接口按照功能拆分成独立的小接口。
具体来说,ISP原则要求我们在设计接口的时候,不应该尝试一次性把所有的功能都包含进一个接口,而是应该将其拆分成多个更小的接口,每个接口只包含与其相关的功能,这样我们在使用接口的时候,就不需要依赖或引用那些我们并不需要的方法。通过接口的拆分,不需要的方法将不再被实现,从而提高系统的内聚性和灵活性。
以下是 ISP 原则的几个关键点:
- 接口的职责应该是单一的,即每个接口只负责一个特定的行为。
- 接口中的方法应该是用户可用的,不关系对于某些调用方是否具有意义的方法应该在接口设计上避免。
- 接口应该尽可能小,这并不意味着我们应该将所有的接口都设计得很小,而是尽量保证每个接口只包含必需的方法和属性。
DIP
DIP (Dependency Inversion Principle) 依赖倒置原则是 SOLID 原则中的第五个原则,它旨在提高软件系统的稳定性和可维护性。
DIP 原则的基本思想是:高层模块不应该依赖于低层模块,它们应该都依赖于抽象;而抽象不应该依赖于具体实现,而是具体实现应该依赖于抽象。
具体来说,DIP 原则要求我们在设计代码结构时,要尽量避免高层模块直接依赖于低层模块,而应该通过抽象接口或抽象类来建立依赖关系。这样,高层模块就不会直接依赖于单个或几个具体的低层模块,而是依赖于它们所实现的抽象层,从而降低了模块之间的耦合,提高了系统的灵活性和可维护性。
以下是 DIP 原则的几个关键点:
- 高层模块和低层模块不应该直接依赖于具体的实现,而是应该依赖于抽象接口或抽象类。
- 抽象不应该依赖于具体实现,而是具体实现应该依赖于抽象。
- 细节应该依赖于抽象,而不是抽象依赖于细节。