SOLID:提升编程技能的 5 条黄金法则
在软件开发领域,这个领域以其多元化和强烈的个人观点而闻名,但很少有实践能像 SOLID 原则那样,被公认为成为更优秀软件工程师的可靠途径。
罗伯特·C·马丁在 2000 年代初期正式提出的 5 条黄金法则,极大地影响了软件开发行业,并为提高代码质量和决策过程设定了新的标准,至今仍然具有现实意义。
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; }
}
违反
在以下代码中,该类同时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();
解决
将产品创建和存储的处理分别放到两个不同的类中,可以减少类的职责数量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;
}
}
用法:
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();
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;
}
}
违反
让我们描述一下实现产品筛选类的场景。我们来添加按颜色筛选产品的功能。
class ProductsFilter {
byColor(inventory: Inventory, color: Color): Product[] {
return inventory.products.filter(p => p.color === color);
}
}
我们已经测试并将这段代码部署到生产环境。
几天后,客户要求增加一项新功能——按尺寸筛选。于是我们修改了类以满足这一新需求。
开闭原则已被违反!
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);
}
}
解决
实现过滤机制而不违反开放封闭原则的正确方法是使用“规范”类。
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));
}
}
用法:
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
过滤机制现在完全可扩展。现有的类不应再进行任何修改。
如果有新的过滤需求,我们只需创建一个新的规范。或者,如果需要更改规范组合,也可以通过使用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;
}
用法:
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)
解决
重构后的代码现在遵循 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());
}
用法:
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)
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!');
}
}
解决
下面的示例展示了我们之前遇到的问题的解决方案。现在的接口更加简洁,也更贴合客户端,允许客户端类只实现与自身相关的方法。
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.
}
ISP 前后对比:
5. D = 依赖倒置原理 (DIP)
依赖倒置原则 (DIP) 是 SOLID 原则中的最后一个,其重点是通过使用抽象来减少底层模块(例如数据读取/写入)和高层模块(执行关键操作)之间的耦合。
DIP 对于设计能够适应变化、模块化且易于更新的软件至关重要。
DIP关键准则如下:
-
高层模块不应依赖于底层模块。两者都应依赖于抽象。这意味着应用程序的功能不应依赖于具体的实现,从而使系统更加灵活,更易于更新或替换底层实现。
-
抽象概念不应依赖于细节,细节应依赖于抽象概念。这促使设计者将注意力集中在实际需要的操作上,而不是这些操作的实现方式上。
违反
让我们来看一个违反依赖倒置原则 (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);
}
}
解决
以下重构后的代码展示了为了遵循依赖倒置原则 (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);
}
}
用法:
const fileLogger = new FileLogger();
const consoleLogger = new ConsoleLogger();
// Now the logging mechanism can be easily replaced
const messageProcessor = new MessageProcessor(consoleLogger);
messageProcessor.processMessage('Hello');
DIP 前后对比:
结论
遵循 SOLID 原则,开发人员可以避免在开发或维护任何规模的软件系统时常见的陷阱,例如紧耦合、缺乏灵活性、代码重用性差以及维护困难等。掌握这些原则是成为更优秀软件工程师的又一重要步骤。
文章来源:https://dev.to/idanref/solid-the-5-golden-rules-to-level-up-your-coding-skills-2p82


