发布于 2026-01-06 0 阅读
0

SOLID:提升编程技能的 5 条黄金法则

SOLID:提升编程技能的 5 条黄金法则

在软件开发领域,这个领域以其多元化和强烈的个人观点而闻名,但很少有实践能像 SOLID 原则那样,被公认为成为更优秀软件工程师的可靠途径。

罗伯特·C·马丁在 2000 年代初期正式提出的 5 条黄金法则,极大地影响了软件开发行业,并为提高代码质量和决策过程设定了新的标准,至今仍然具有现实意义。

SOLID 原则

SOLID 原则专为支持面向对象编程 (OOP) 范式而设计。因此,本文旨在帮助希望提升开发技能并编写更优雅、更易维护和可扩展代码的 OOP 开发人员。

本文将使用 TypeScript 语言,遵循通用的跨语言面向对象编程 (OOP) 概念。需要具备基本的 OOP 知识。


1. S = 单一职责原则 (SRP)

单一职责原则 (SRP) 是 SOLID 五大原则之一,它规定每个类应该只有一个职责,以保持有意义的关注点分离。

这种模式是针对一种常见的反模式——“上帝对象”——提出的解决方案。“上帝对象”指的是承担过多职责的类或对象,这使得理解、测试和维护变得困难。

遵循单一职责原则 (SRP) 有助于使代码组件可重用、松耦合且易于理解。让我们来探讨这一原则,并展示一个违反 SRP 的例子及其解决方法。

全球宣言

enum Color {
    BLUE = 'blue',
    GREEN = 'green',
    RED = 'red'
}

enum Size {
    SMALL = 'small',
    MEDIUM = 'medium',
    LARGE = 'large'
}

class Product {
    private _name: string;
    private _color: Color;
    private _size: Size;

    constructor (name: string, color: Color, size: Size) {
        this._name = name;
        this._color = color;
        this._size = size;
    }

    public get name(): string { return this._name; }
    public get color(): Color { return this._color; }
    public get size(): Size { return this._size; }
}
Enter fullscreen mode Exit fullscreen mode

违反

在以下代码中,该类同时ProductManager负责产品的创建和存储,违反了单一职责原则。

class ProductManager {
    private _products: Product[] = [];

    createProduct (name: string, color: Color, size: Size): Product {
        return new Product(name, color, size);
    }

    storeProduct (product: Product): void {
        this._products.push(product);
    }

    getProducts (): Product[] {
        return this._products;
    }
}

const productManager: ProductManager = new ProductManager();

const product: Product = productManager.createProduct('Product 1', Color.BLUE, Size.LARGE);
productManager.storeProduct(product);

const allProducts: Product[] = productManager.getProducts();
Enter fullscreen mode Exit fullscreen mode

解决

将产品创建和存储的处理分别放到两个不同的类中,可以减少类的职责数量ProductManager。这种方法进一步模块化了代码,使其更易于维护。

class ProductManager {
    createProduct (name: string, color: Color, size: Size): Product {
        return new Product(name, color, size);
    }
}

class ProductStorage {
    private _products: Product[] = [];

    storeProduct (product: Product): void {
        this._products.push(product);
    }

    getProducts (): Product[] {
        return this._products;
    }
}
Enter fullscreen mode Exit fullscreen mode

用法:

const productManager: ProductManager = new ProductManager();
const productStorage: ProductStorage = new ProductStorage();

const product: Product = productManager.createProduct("Product 1", Color.BLUE, Size.LARGE);

productStorage.storeProduct(product);
const allProducts: Product[] = productStorage.getProducts();
Enter fullscreen mode Exit fullscreen mode

2. O = 开闭原理 (OCP)

软件实体应该允许扩展,但不允许修改。

开闭原则(OCP)的核心思想是“一次编写,编写得足够好以使其可扩展,然后就不用管它了”。

这条原则的重要性在于,模块会根据新的需求不时发生变化。如果新需求是在模块编写、测试并上传到生产环境之后才出现的,那么修改该模块通常是一种糟糕的做法,尤其是在其他模块依赖于它的情况下。为了避免这种情况,我们可以使用开闭原则。

全球宣言

enum Color {
    BLUE = 'blue',
    GREEN = 'green',
    RED = 'red'
}

enum Size {
    SMALL = 'small',
    MEDIUM = 'medium',
    LARGE = 'large'
}

class Product {
    private _name: string;
    private _color: Color;
    private _size: Size;

    constructor (name: string, color: Color, size: Size) {
        this._name = name;
        this._color = color;
        this._size = size;
    }

    public get name(): string { return this._name; }
    public get color(): Color { return this._color; }
    public get size(): Size { return this._size; }
}

class Inventory {
    private _products: Product[] = [];

    public add(product: Product): void {
        this._products.push(product);
    }

    addArray(products: Product[]) {
        for (const product of products) {
            this.add(product);
        }
    }

    public get products(): Product[] {
        return this._products;
    }
}
Enter fullscreen mode Exit fullscreen mode

违反

让我们描述一下实现产品筛选类的场景。我们来添加按颜色筛选产品的功能。

class ProductsFilter {
    byColor(inventory: Inventory, color: Color): Product[] {
        return inventory.products.filter(p => p.color === color);
    }
}
Enter fullscreen mode Exit fullscreen mode

我们已经测试并将这段代码部署到生产环境。

几天后,客户要求增加一项新功能——按尺寸筛选。于是我们修改了类以满足这一新需求。

开闭原则已被违反!

class ProductsFilter {
    byColor(inventory: Inventory, color: Color): Product[] {
        return inventory.products.filter(p => p.color === color);
    }

    bySize(inventory: Inventory, size: Size): Product[] {
        return inventory.products.filter(p => p.size === size);
    }
}
Enter fullscreen mode Exit fullscreen mode

解决

实现过滤机制而不违反开放封闭原则的正确方法是使用“规范”类。

abstract class Specification {
    public abstract isValid(product: Product): boolean;
}

class ColorSpecification extends Specification {
    private _color: Color;

    constructor (color) {
        super();
        this._color = color;
    }

    public isValid(product: Product): boolean {
        return product.color === this._color;
    }
}

class SizeSpecification extends Specification {
    private _size: Size;

    constructor (size) {
        super();
        this._size = size;
    }

    public isValid(product: Product): boolean {
        return product.size === this._size;
    }
}

// A robust mechanism to allow different combinations of specifications
class AndSpecification extends Specification {
    private _specifications: Specification[];

    // "...rest" operator, groups the arguments into an array
    constructor ((...specifications): Specification[]) {
        super();
        this._specifications = specifications;
    }

    public isValid (product: Product): boolean {
        return this._specifications.every(specification => specification.isValid(product));
    }
}

class ProductsFilter {
    public filter (inventory: Inventory, specification: Specification): Product[] {
        return inventory.products.filter(product => specification.isValid(product));
    }
}
Enter fullscreen mode Exit fullscreen mode

用法:

const p1: Product = new Product('Apple', Color.GREEN, Size.LARGE);
const p2: Product = new Product('Pear', Color.GREEN, Size.LARGE);
const p3: Product = new Product('Grapes', Color.GREEN, Size.SMALL);
const p4: Product = new Product('Blueberries', Color.BLUE, Size.LARGE);
const p5: Product = new Product('Watermelon', Color.RED, Size.LARGE);

const inventory: Inventory = new Inventory();
inventory.addArray([p1, p2, p3, p4, p5]);

const greenColorSpec: ColorSpecification = new ColorSpecification(Color.GREEN);
const largeSizeSpec: SizeSpecification = new SizeSpecification(Size.LARGE);

const andSpec: AndSpecification = new AndSpecification(greenColorSpec, largeSizeSpec);
const productsFilter: ProductsFilter = new ProductsFilter();

const filteredProducts: Product[] = productsFilter.filter(inventory, andSpec); // All large green products
Enter fullscreen mode Exit fullscreen mode

过滤机制现在完全可扩展。现有的类不应再进行任何修改。

如果有新的过滤需求,我们只需创建一个新的规范。或者,如果需要更改规范组合,也可以通过使用AndSpecification类轻松实现。


3. L = 里氏替换原理 (LSP)

里氏替换原则(LSP)是保证软件组件灵活性和健壮性的重要规则。它由芭芭拉·里氏提出,并成为SOLID原则的基础要素。

生命周期服务原则(LSP)指出,超类的对象应该可以被子类的对象替换,且不影响程序的正确性。换句话说,子类应该扩展超类的行为,而不改变其原有功能。采用这种方法可以提高软件组件的质量,确保可重用性,并减少意外的副作用。

违反

以下示例说明了违反里氏替换原则 (LSP) 的情况。通过检查程序在Rectangle对象被替换为另一个Square对象时的行为,可以观察到这种违反情况。

声明:

class Rectangle {
    protected _width: number;
    protected _height: number;

    constructor (width: number, height: number) {
        this._width = width;
        this._height = height;
    }

    get width (): number { return this._width; }
    get height (): number { return this._height; }

    set width (width: number) { this._width = width; }
    set height (height: number) { this._height = height; }

    getArea (): number {
        return this._width * this._height;
    }
}

// A square is also rectangle
class Square extends Rectangle {
    get width (): number { return this._width; }
    get height (): number { return this._height; }

    set height (height: number) {
        this._height = this._width = height; // Changing both width & height
    }

    set width (width: number) {
        this._width = this._height = width; // Changing both width & height
    }
}

function increaseRectangleWidth(rectangle: Rectangle, byAmount: number) {
    rectangle.width += byAmount;
}
Enter fullscreen mode Exit fullscreen mode

用法:

const rectangle: Rectangle = new Rectangle(5, 5);
const square: Square = new Square(5, 5);

console.log(rectangle.getArea()); // Expected: 25, Got: 25 (V)
console.log(square.getArea()); // Expected: 25, Got: 25 (V)

// LSP Violation Indication: Can't replace object 'rectangle' (superclass) with 'square' (subclass) since the results would be different.
increaseRectangleWidth(rectangle, 5);
increaseRectangleWidth(square, 5);

console.log(rectangle.getArea()); // Expected: 50, Got: 50 (V)

// LSP Violation, increaseRectangleWidth() changed both width and height of the square, unexpected behavior.
console.log(square.getArea()); //Expected: 50, Got: 100 (X)
Enter fullscreen mode Exit fullscreen mode

解决

重构后的代码现在遵循 LSP 原则,确保超类的对象Shape可以被子类的对象替换Rectangle,而Square不会影响计算面积的正确性,也不会引入任何改变程序行为的副作用。

声明:

abstract class Shape {
    public abstract getArea(): number;
}

class Rectangle extends Shape {
    private _width: number;
    private _height: number;

    constructor (width: number, height: number) {
        super();
        this._width = width;
        this._height = height;
    }

    getArea (): number { return this._width * this._height; }
}

class Square extends Shape {
    private _side: number;

    constructor (side: number) {
        super();
        this._side = side;
    }

    getArea (): number { return this._side * this._side; }
}

function displayArea (shape: Shape): void {
    console.log(shape.getArea());
}
Enter fullscreen mode Exit fullscreen mode

用法:

const rectangle: Rectangle = new Rectangle(5, 10);
const square: Square = new Square(5);

// The rectangle's area is correctly calculated
displayArea(rectangle); // Expected: 50, Got: 50 (V)

// The square's area is correctly calculated
displayArea(square); // Expected: 25, Got: 25 (V)
Enter fullscreen mode Exit fullscreen mode

4. I = 接口隔离原则 (ISP)

接口隔离原则 (ISP) 强调创建客户端特定的接口,而不是一刀切的接口。

这种方法根据客户的需求集中类,消除了类必须实现它实际上没有使用或需要的方法的情况。

通过应用接口隔离原则,可以构建出更加灵活、易于理解和易于重构的软件系统。让我们来看一个例子。

违反

这里违反了 ISP 规则,因为Robot必须实现eat()完全不必要的功能。

interface Worker {
    work(): void;
    eat(): void;
}

class Developer implements Worker {
    public work(): void {
        console.log('Coding..');
    }

    public eat(): void {
        console.log('Eating..');
    }
}

class Robot implements Worker {
    public work(): void {
        console.log('Building a car..');
    }

    // ISP Violation: Robot is forced to implement this function even when unnecessary
    public eat(): void {
        throw new Error('Cannot eat!');
    }
}
Enter fullscreen mode Exit fullscreen mode

解决

下面的示例展示了我们之前遇到的问题的解决方案。现在的接口更加简洁,也更贴合客户端,允许客户端类只实现与自身相关的方法。

interface Workable {
    work(): void;
}

interface Eatable {
    eat(): void;
}

class Developer implements Workable, Eatable {
    public work(): void {
        console.log('Coding..');
    }

    public eat(): void {
        console.log('Eating...');
    }
}

class Robot implements Workable {
    public work(): void {
        console.log('Building a car..');
    }

    // No need to implement eat(), adhering ISP.
}
Enter fullscreen mode Exit fullscreen mode

ISP 前后对比:

重构前后的接口隔离原则


5. D = 依赖倒置原理 (DIP)

依赖倒置原则 (DIP) 是 SOLID 原则中的最后一个,其重点是通过使用抽象来减少底层模块(例如数据读取/写入)和高层模块(执行关键操作)之间的耦合。

DIP 对于设计能够适应变化、​​模块化且易于更新的软件至关重要。

DIP关键准则如下:

  1. 高层模块不应依赖于底层模块。两者都应依赖于抽象。这意味着应用程序的功能不应依赖于具体的实现,从而使系统更加灵活,更易于更新或替换底层实现。

  2. 抽象概念不应依赖于细节,细节应依赖于抽象概念。这促使设计者将注意力集中在实际需要的操作上,不是这些操作的实现方式上。

违反

让我们来看一个违反依赖倒置原则 (DIP) 的例子。

MessageProcessor(高级模块)与(低级模块)紧密耦合,直接依赖于FileLogger(低级模块),违反了该原则,因为它不依赖于抽象层,而是依赖于具体的类实现。

额外说明这里也违反了开闭原则(OCP)。如果我们想把日志机制从写入文件改为写入数据库,就必须直接修改函数MessageProcessor

import fs from 'fs';

// Low Level Module
class FileLogger {
    logMessage(message: string): void {
        fs.writeFileSync('somefile.txt', message);
    }
}

// High Level Module
class MessageProcessor {
    // DIP Violation: This high-level module is is tightly coupled with the low-level module (FileLogger), making the system less flexible and harder to maintain or extend.
    private logger = new FileLogger();

    processMessage(message: string): void {
        this.logger.logMessage(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

解决

以下重构后的代码展示了为了遵循依赖倒置原则 (DIP) 而需要进行的更改。与之前示例中高层类MessageProcessor持有底层具体类的私有属性不同FileLogger,现在它持有的Logger是抽象层接口类型的私有属性。

这种更好的方法减少了类之间的依赖关系,从而使代码更具可扩展性和可维护性。

声明:

import fs from 'fs';

// Abstraction Layer
interface Logger {
    logMessage(message: string): void;
}

// Low Level Module #1
class FileLogger implements Logger {
    logMessage(message: string): void {
        fs.writeFileSync('somefile.txt', message);
    }
}

// Low Level Module #2
class ConsoleLogger implements Logger {
    logMessage(message: string): void {
        console.log(message);
    }
}

// High Level Module
class MessageProcessor {
    // Resolved: The high level module is now loosely coupled with the low level logger modules.
    private _logger: Logger;

    constructor (logger: Logger) {
        this._logger = logger;
    }

    processMessage (message: string): void {
        this._logger.logMessage(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

用法:

const fileLogger = new FileLogger();
const consoleLogger = new ConsoleLogger();

// Now the logging mechanism can be easily replaced
const messageProcessor = new MessageProcessor(consoleLogger);
messageProcessor.processMessage('Hello');
Enter fullscreen mode Exit fullscreen mode

DIP 前后对比:

依赖倒置原则重构前后对比

结论

遵循 SOLID 原则,开发人员可以避免在开发或维护任何规模的软件系统时常见的陷阱,例如紧耦合、缺乏灵活性、代码重用性差以及维护困难等。掌握这些原则是成为更优秀软件工程师的又一重要步骤。

文章来源:https://dev.to/idanref/solid-the-5-golden-rules-to-level-up-your-coding-skills-2p82