SOLID,面向对象设计五大基本原则

Posted GoldenaArcher

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SOLID,面向对象设计五大基本原则相关的知识,希望对你有一定的参考价值。

SOLID,面向对象设计五大基本原则

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,因为这个类具有两个职责:

  • 进行正方形相关的计算
  • 正方形的 UI 操作,这里假设绘图与旋转都发生在 UI Canvas 上

对于这个类而言,它的 内聚性(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 的类,也不需要对 CustomerProfileInsurancePremiumDiscountCalculator 进行修改。

由此可以看到 OCP 的优势如下:

  • 容易增加新的功能

  • 大幅降低了开发和测试的成本

    对于测试人员来说不需要重新跑整个 Regression Test Suite,只需要测试新增添部分的代码

  • OCP 需要低耦合的代码,因此也实现了 SRP

同样,使用 OCP 也需要注意:

  • 不要盲目地遵从 OCP 原则
  • 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 的小技巧:

  1. interface 太过冗长
  2. interface 内聚太低
  3. 有未实现的空白函数

这个时候应该可以注意到,满足 ISP 需求的代码同样也满足 SRP,并且间接的遵从了里氏替换原则,这也就是为什么开篇的时候就提到了 SOLID 五大原则必须作为一个整体去看,他们内部联系错综复杂,相互补充,是无法剥离作为单一整体去对待。

依赖反转原则

全称 Dependence Inversion Principle,自然缩写 DIP

  1. High-level modules should not on low-level modules. Both should depend on abstractions.

  2. Abstractions should not depend on details, details should depend on abstractions.

  3. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
    抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

  4. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

以下图为例:

面向对象的五大基本原则(SOLID)

面向对象的五大基本原则(SOLID)

面向对象的五大基本原则(SOLID)

面向对象五大设计原则

跟着盒子的代码设计示例,一起对面向对象的设计模式之SOLID原则加深理解

跟着盒子的代码设计示例,一起对面向对象的设计模式之SOLID原则加深理解