SOLID 指代了 OOP(Object Oriented Programming) 和 OOD(Object Oriented Design) 的五个基本原则:
| 首字母 | 指代 | 概念 |
|---|---|---|
| S | 单一功能原则(SRP) | 认为“对象应该仅具有一种单一功能”的概念。 |
| O | 开闭原则(OCP) | 认为“软件应该是对于扩展开放的,但是对于修改封闭的”的概念。 |
| L | 里氏替换原则(LSP) | 认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”的概念。 |
| I | 接口隔离原则(ISP) | 认为“多个特定客户端接口要好于一个宽泛用途的接口”的概念。 |
| D | 依赖反转原则(DIP) | 认为一个方法应该遵从“依赖于抽象而不是一个实例”的概念。 依赖注入是该原则的一种实现方式。 |
SOLID 五大原则相互紧密关联并不可分离,因此比起单独某一条原则,将其作为整体理解,并且结合使用的时候才能发挥最大效果。
全称为 Single Responsibiliby Principle,缩写 SRP。
虽然通常规定说是 每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来,不过我看到的文档上说的是:
Every software component should have one and only one responsibility.
每一个软件组件都应该有且只有一个职责(改变的原因)。
我觉得这个说法更符合我作为前端开发的理解和认知,毕竟……现在主要搞的还是组件开发,而这里的 software component 可以指代一个雷、一个函数、一个模块。
参考以下案例:
public class Square {
public int calculateArea() {
// ...
}
public int calculatePerimeter() {
// ...
}
// not realted to square calculation
public void draw() {
// ...
}
// not realted to square calculation
public void rotate() {
// ...
}
}
这个类就违反了 SRP,因为这个类具有两个职责:
对于这个类而言,它的 内聚性(Cohesion) 就偏低,而它的 耦合性(Coupling) 相应的就偏高。
Cohesion is the degree to which the various parts of software components are related
大概意思为:
内聚性(英語:Cohesion)也稱為内聚力,是一軟體度量,是指機能相關的程式組合成一模組的程度,或是各機能凝聚的狀態或程度。
对于程序来说,内聚性越高越高,对于上面这个案例来说,它的内聚性就不算特别高。下面是一个提升其内聚性的修改:
public class Square {
public int calculateArea() {
// ...
}
public int calculatePerimeter() {
// ...
}
}
public class SquareUI {
public void draw() {
// ...
}
public void rotate() {
// ...
}
}
这样就有了两个内聚性非常高的类,同样它们的 SRP 也得以提升,Square 包含了所有正方形的计算,而 SquareUI 负责所有正方形 UI 的绘制。
耦合性与内聚性是一个相对的概念,一般耦合度越高,内聚性越低。
The level of inter dependency between various software components
大概意思是:
耦合性(英語:Coupling)或稱耦合力或耦合度,是一種軟體度量,是指一程式中,模組及模組之間資訊或參數依赖的程度。
参考下面一个例子:
public class Student {
// paivate attributes
// ...
// ... other constructors, setters, getters, amd other method
// jdbc connection
public void save() {
// serialize object into string expression
String objStr = MyUtils.serializeIntoString(this);
Connection conn = null;
Statement stmt = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/MyDB", "root", "pwd");
stmt = conn.createStatement();
// execution...
} catch(Exception e) {
// error handling
}
}
}
对于学生这个类来说,它的耦合性就非常的高,保存学生功能信息这一功能高度依赖于 JDBC 的链接,同样这个 类/方法 的内聚性就非常的低,也违反了单一指责原则。
而下面的修正就降低了耦合性、提升了内聚性的同时,让每一个类都负责其对应的职责:
public class Student {
// paivate attributes
// ...
// ... other constructors, setters, getters, amd other method
}
public class StudentRepo {
// jdbc connection
public void save() {
// serialize object into string expression
String objStr = MyUtils.serializeIntoString(this);
Connection conn = null;
Statement stmt = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/MyDB", "root", "pwd");
stmt = conn.createStatement();
// execution...
} catch(Exception e) {
// error handling
}
}
}
其实这也是大多数项目现在的实现规范,比如说会有一个 DAO/POJO 保存对象的基本信息、序列化和反序列化功能,然后存在于一个对应的 Repo 进行数据库的操作。
有些翻译又称之为 开放封闭原则,全称 Open Closed Principle,缩写为 OCP,其主要定义为:
Software components should be closed for modification, but open for extension.
软件中的组件(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的。
其大概意思就是,新增加的功能应该在不改变现有代码的前提下发生,以及一个软件组件应该具备增添新功能或特性的可扩展性。
以下面的代码为例:
public class InsurancePremiumDiscountCalculator {
// only take HealthInsuranceCustomerProfile
public int calculatePremiumDiscountPercent(HealthInsuranceCustomerProfile customer) {
switch (customer.isLoyalCustomer()) {
// ...
}
return 0;
}
}
这是一个保险折扣的计算器,目前计算折扣这个方法只能接受一个 HealthInsuranceCustomerProfile 参数,但是如果保险公司除了健康险之外还增加了人寿险、车险、资产险等其他保险,那么就需要 overload 方法去进行实现,并且这也增加了很多重复代码。
参考修正的代码:
public interface CustomerProfile {
public boolean isLoyalCustomer();
}
public class VehicleInsuranceCustomerProfile extends CustomerProfile {
// ...
}
public class HealthInsuranceCustomerProfile extends CustomerProfile {
// ...
}
public class InsurancePremiumDiscountCalculator {
// now taking all the customer profile that extends CustomerProfile
public int calculatePremiumDiscountPercent(CustomerProfile customer) {
switch (customer.isLoyalCustomer()) {
// ...
}
return 0;
}
}
这部分的代码就实现了开闭原则,核心部分 CustomerProfile 具有可扩展性,如果新增加了其他延展 CustomerProfile 的类,也不需要对 CustomerProfile 和 InsurancePremiumDiscountCalculator 进行修改。
由此可以看到 OCP 的优势如下:
容易增加新的功能
大幅降低了开发和测试的成本
对于测试人员来说不需要重新跑整个 Regression Test Suite,只需要测试新增添部分的代码
OCP 需要低耦合的代码,因此也实现了 SRP
同样,使用 OCP 也需要注意:
全称为 Liskov Substitute Principle,缩写为 LSP。
Objects should be replaceable with their subtypes without affecting the correctness of the program.
大抵意思是说:
派生类(子类)对象可以在程序中代替其基类(超类)对象,且并不影响程序的正确执行。
原文为:
Let q ( x ) q(x) q(x) be a property provable about objects x x x of type T T T. Then q ( y ) q(y) q(y) should be true for objects y y y of type S S S where S S S is a subtype of T T T.
这也是一种对 is-A 想法的挑战,比如说常见的有:SUV 是一种车,鸵鸟是鸟,汽油是能源,但是对于 里氏替换原则 来说,鸵鸟是鸟 这一说法存在一些问题,因为常规意义上来说鸟会飞,而鸵鸟不会。
参考一下代码:
public class Bird {
public void fly() {
// ...
}
}
public class Ostrich {
@Override
public void fly() {
// unimplemented
throw new RuntimeException();
}
}
这时候鸵鸟就有一个继承了,但是没有实现的类。
里氏替换原则提出过一个说法:
If it looks like a duck and quacks like a duck but it needs batteries, you probably have the wrong abstraction.
如果它看起来像鸭子,叫起来也像鸭子,但是它需要电池才能工作,那么你的抽象大概是做错了。
继续参考下面的例子:
public class Car {
public double getCabinWidth() {
// ...
}
}
// sth like f1 fomula car
public class RacingCar extends Car {
@Override
public double getCabinWidth() {
// unimplemented
}
// 驾驶舱
public double getCockpitWidth() {
// ...
}
}
public class CarUtils {
public static void getCabinWidths(List<Car> cars) {
for (Car car: cars) {
// if one of the car is RacingCar, this will be broken
System.out.println(car.getCabinWidth);
}
}
}
这里假设车能够获取车厢的宽度,赛车继承了车这个类。不过对于赛车(如 F1 赛车)而言,它们只有驾驶舱而并不存在车厢,那么这个时候在运行迭代时,车的列表中存在一辆赛车就会导致程序运行失败。
参考下列修正:
public class Vehicle {
public double getInteriorWidth() {
// ...
}
}
public class Car extends Vehicle {
@Override
public double getInteriorWidth() {
this.getCabinWidth();
}
public double getCabinWidth() {
// ...
}
}
public class RacingCar extends Vehicle {
@Override
public double getInteriorWidth() {
this.getCockpitWidth();
}
public double getCockpitWidth() {
// ...
}
}
public class VehicleUtils {
public static void getInteriorWidths(List<Vehicle> vehicles) {
for (Vehicle vehicle: vehicles) {
System.out.println(vehicle.getInteriorWidth);
}
}
}
在这个实现中,基础类 Vehicle,车 和 赛车同时实现了这个基础类,并且在调用基础类的时候返回了各自内部实现的函数。这时候在迭代中调用 ehicle.getInteriorWidth 就不会出现任何的问题,并且任一 vehicle 被子类替换也不会影响正确执行。
这样就充分满足了里氏替换原则的规则。
另一个里氏替换原则的特性是莫BB,直接干,以下面这个案例来说,自家的产品会在其他的产品上再打个小折扣:
public class Product{
protected double discount;
public double getDiscount {
return this.discount;
}
}
public class InHouseProduct {
public void applyExtraDiscount() {
this.discount = this.discount * 1.5;
}
public double getDiscount {
return this.discount;
}
}
// some other mtehods
public static someMethod() {
for (Product product: productList) {
if (product instanceof InHouseProduct) {
((InHouseProduct) product).applyExtraDiscount();
}
System.out.println(product.getDiscount());
}
}
使用 instanceof 就相当于在询问,并实现操作。但是如果换成下面这种写法,不在调用类中查询基类,而是直接调用:
public class Product{
protected double discount;
public double getDiscount {
return this.discount;
}
}
public class InHouseProduct {
public void applyExtraDiscount() {
this.discount = this.discount; * 1.5;
}
public double getDiscount {
applyExtraDiscount();
return this.discount;
}
}
// some other mtehods
public static someMethod() {
for (Product product: productList) {
System.out.println(product.getDiscount());
}
}
这样对于后期的维护也会更加简单。
全称 Interface Segregation Principle,缩写 ISP。
No client should be forced to depend on methods it does not use
客户(client)不应被迫使用对其而言无用的方法或功能。
参考以下接口,实现了一个万能打印机(包括打印、扫描、传真功能)的接口与其实现类:
public interface IMultiFunction {
public void print();
public void scan();
public void fax();
}
public class XeroWorkCentre implements IMultiFunction {
@Override
public void print(
// actual implementation
);
@Override
public void scan(
// actual implementation
);
@Override
public void fax(
// actual implementation
);
}
public class HPPrinterNScanner implements IMultiFunction {
@Override
public void print(
// actual implementation
);
@Override
public void scan(
// actual implementation
);
@Override
public void fax(
// not implemented
);
}
public class CannonPrinter implements IMultiFunction {
@Override
public void print(
// actual implementation
);
@Override
public void scan(
// not implemented
);
@Override
public void fax(
// not implemented
);
}
对于 IMultiFunction 来说,它的内聚很低,耦合很高,并且会留下一些没有实现的代码,这就违反了接口隔离原则。正确的做法可以将三个功能进行拆分,并且分别实现:
public interface IPrint {
public void print();
}
public interface IScan {
public void scan();
}
public interface IFax {
public void fax();
}
public class XeroWorkCentre implements IPrint, IScan, IFax {
@Override
public void print(
// actual implementation
);
@Override
public void scan(
// actual implementation
);
@Override
public void fax(
// actual implementation
);
}
public class HPPrinterNScanner implements IPrint, IScan {
@Override
public void print(
// actual implementation
);
@Override
public void scan(
// actual implementation
);
}
// ...
一些判断 ISP 的小技巧:
这个时候应该可以注意到,满足 ISP 需求的代码同样也满足 SRP,并且间接的遵从了里氏替换原则,这也就是为什么开篇的时候就提到了 SOLID 五大原则必须作为一个整体去看,他们内部联系错综复杂,相互补充,是无法剥离作为单一整体去对待。
全称 Dependence Inversion Principle,自然缩写 DIP
High-level modules should not on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details, details should depend on abstractions.
高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
以下图为例:
高层次的模块为:
ProductCatalogPaymentProcessorCustomerProfile低层次的模块为:
SQLProductRepoGooglePayGatewayWireTransferEmailSenderVoiceDialer混合的为:
Communication
与 CustomerProfile 相比是低层次,与 EmailSender 和 VoiceDialer 相比是高层次,
这个例子就违反了依赖反转原则,首先其中没有抽象借口的存在,其次按照图中的设计而言,高层次模块直接依赖于低层次层次的模块进行实现:
接下来就用一个比较常见的功能举例,进行代码实现,其中功能如下:
ProductCatalog 直接依赖于 SQLProductRepo 的实现才能工作:
public class ProductCatalog {
public void listAllProducts() {
SQLProductRepo sQLProductRepo = new SQLProductRepo();
List<String> allProductNames = sQLProductRepo.getAllProductNames();
// display all product names
}
}
public class SQLProductRepo {
public List<String> getAllProductNames() {
return Array.asList("prod1", "prod2");
}
}
为了满足第一条规定:
高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口
这时候可以介绍一个抽象接口,并且使用工厂返回这个抽象接口:
public interface ProductRepo {
public List<String> getAllProductNames();
}
// low level now depends on an abstraction
public class SQLProductRepo implements ProductRepo {
public List<String> getAllProductNames() {
return Array.asList("prod1", "prod2");
}
}
// factory class
public class ProductFactory {
public static ProductRepo create() {
return new SQLProductRepo();
}
}
// not directly estanstate
public class ProductCatalog {
public void listAllProducts() {
// productRepo is an interface, an abstraction
ProductRepo productRepo = ProductRepo.create();
List<String> allProductNames = productRepo.getAllProductNames();
}
}
同时,这样的修改也满足了第二条规则:
抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口
现在的关系为:
注意 ProductRepo 没有直接依赖于 SQLProductRepo 的实现,它只是返回了一个 SQLProductRepo 的实例,所有的对于数据库的操作依旧在 SQLProductRepo 中实现,因此,具体实现(SQLProductRepo) 对 抽象接口(SQLProductRepo) 有着依赖关系。
DI 是 DIP 的一种实现方法,两个在概念上无法互用,下面的案例会补充一下怎么样使用 DI 的方式继续降低耦合度:
public class ECommerceMainApp {
public static void main(String[] args) {
ProductRepo = ProductRepo = ProductFactory.create();
ProductCatalog productCatalog = new ProductCatalog(productRepo);
}
}
public interface ProductRepo {
public List<String> getAllProductNames();
}
// low level now depends on an abstraction
public class SQLProductRepo implements ProductRepo {
public List<String> getAllProductNames() {
return Array.asList("prod1", "prod2");
}
}
// factory class
public class ProductFactory {
public static ProductRepo create() {
return new SQLProductRepo();
}
}
// not directly estanstate
public class ProductCatalog {
private ProductRepo productRepo;
// the dependency is injected to ProductCatalog
public ProductCatalog(ProductRepo productRepo) {
this.productRepo = productRepo;
}
public void listAllProducts() {
List<String> allProductNames = productRepo.getAllProductNames();
}
}
这个情况下,productRepo 作为一个依赖注入进了 ProductCatalog,对于 ProductCatalog 来说,它就不需要管理什么时候这个对象会被实例化,从而继续降低了二者的耦合度。
上面的 DI 案例中依赖注入依旧在主程序中发生,使用 IoC 可以在不同的 Context 中进行依赖注入的管理,继续降低耦合度。这部分 Java 自己没办法实现,必须依赖其他的框架在其他的线程/Context 中进行依赖注入。
改造前:
const relationship = Object.freeze({
parent: 0,
child: 1,
sibling: 2,
});
class Person {
constructor(name) {
this.name = name;
}
}
// low level module
class Relationships {
constructor() {
this.data = [];
}
addParentAndChild(parent, child) {
this.data.push({
from: parent,
type: relationship.parent,
to: child,
});
}
}
// high level module
class Research {
constructor(relationships) {
// find all child of the parent
const relations = relationships.data;
for (const rel of relations.filter(
(r) => r.from.name === 'John' && r.type === relationship.parent
)) {
console.log(`John has a child name ${rel.to.name}`);
}
}
}
const parent = new Person('John');
const child1 = new Person('Chris');
const child2 = new Person('Matt');
const rel = new Relationships();
rel.addParentAndChild(parent, child1);
rel.addParentAndChild(parent, child2);
const research = new Research(rel);
改造后:
const relationship = Object.freeze({
parent: 0,
child: 1,
sibling: 2,
});
class Person {
constructor(name) {
this.name = name;
}
}
class RelationshipBrowser {
constructor() {
if (this.constructor === 'RelationshipBrowser') {
throw new Error('RelationshipBrowser should be abstract.');
}
}
findAllChildrenOf(name) {}
}
// low level module
class Relationships extends RelationshipBrowser {
constructor() {
super();
this.data = [];
}
addParentAndChild(parent, child) {
this.data.push({
from: parent,
type: relationship.parent,
to: child,
});
}
findAllChildrenOf(name) {
return this.data
.filter((r) => r.from.name === name && r.type === relationship.parent)
.map((r) => r.to);
}
}
// high level module
class Research {
constructor(browser) {
for (const p of browser.findAllChildrenOf('John')) {
console.log(`John has a child called ${p.name}`);
}
}
}
const parent = new Person('John');
const child1 = new Person('Chris');
const child2 = new Person('Matt');
const rel = new Relationships();
rel.addParentAndChild(parent, child1);
rel.addParentAndChild(parent, child2);
const research = new Research(rel);
上文中出现的俚语原本的说法为:
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
如果它看起来像鸭子、游泳像鸭子、叫声像鸭子,那么它可能就是隻鸭子。
专有名词是鸭子测试。
我觉得鸵鸟的实现可以将基础类替换为动物,毕竟动物都会动:
public class Animal {
public void move() {}
}
public class Bird {
@Override
public void move() {
this.fly();
}
public void fly() {
// ...
}
}
public class Ostrich {
@Override
public void move() {
this.run();
}
public void run() {
// ...
}
}
关于 DI 和 IoC 在 Spring 的应用,之前在学的部分有: