SOLID 原则:它们坚如磐石是有原因的!

2025-05-27

SOLID 原则:它们坚如磐石是有原因的!

刚开始学习面向对象编程,对SOLID有点迷茫?不用担心,在本文中,我将向您解释它,并提供如何在代码开发中使用它的示例。

什么是 SOLID?

在面向对象编程中,SOLID是五项设计原则的首字母缩写,旨在增强对软件的理解、开发和维护。

通过应用这套原则,你应该会注意到错误减少、代码质量提升、代码更有条理、耦合度降低、重构增强,以及代码复用得到促进。让我们开始吧。

1. S - 单一职责原则

SRP

SRP——单一职责原则

这一点非常简单,但非常重要:一个类应该有一个且只有一个改变的理由。

再也不用创建兼具多种功能和职责的类了,对吧?你可能遇到过,甚至创建过一个包罗万象的类,也就是所谓的“上帝类”。乍一看可能没什么问题,但当你需要修改这个类的逻辑时,问题就来了。

do上帝类:在OOP中,这是一个包含knows太多东西的类。



class ProfileManager {
  authenticateUser(username: string, password: string): boolean {
    // Authenticate logic
  }

  showUserProfile(username: string): UserProfile {
    // Show user profile logic
  }

  updateUserProfile(username: string): UserProfile {
    // Update user profile logic
  }

  setUserPermissions(username: string): void {
    // Set permission logic
  }
}


Enter fullscreen mode Exit fullscreen mode

这个ProfileManager类违反了 SRP 原则,因为它执行了四个不同的任务。它同时执行验证和更新数据、进行呈现,以及最重要的,设置权限。

这可能导致的问题

  • Lack of cohesion -一个类不应该承担不属于它自己的责任;
  • Too much information in one place -你的课程最终会面临许多依赖关系和变化困难;
  • Challenges in implementing automated tests -很难嘲笑这样的阶级。

现在,将SRP应用到ProfileManager课堂上,让我们看看这个原则能带来什么改进:



class AuthenticationManager {
  authenticateUser(username: string, password: string): boolean {
    // Authenticate logic
  }
}

class UserProfileManager {
  showUserProfile(username: string): UserProfile {
    // Show user profile logic
  }

  updateUserProfile(username: string): UserProfile {
    // Update user profile logic
  }
}

class PermissionManager {
  setUserPermissions(username: string): void {
    // Set permission logic
  }
}


Enter fullscreen mode Exit fullscreen mode

你可能会想,can I apply this only to classes?答案是:完全不是。你也可以(也应该)将它应用于方法和函数。



// ❌
function processTasks(taskList: Task[]): void {
  taskList.forEach((task) => {
    // Processing logic involving multiple responsibilities
    updateTaskStatus(task);
    displayTaskDetails(task);
    validateTaskCompletion(task);
    verifyTaskExistence(task);
  });
}

// ✅
function updateTaskStatus(task: Task): Task {
  // Logic for updating task status
  return { ...task, completed: true };
}

function displayTaskDetails(task: Task): void {
  // Logic for displaying task details
  console.log(`Task ID: ${task.id}, Description: ${task.description}`);
}

function validateTaskCompletion(task: Task): boolean {
  // Logic for validating task completion
  return task.completed;
}

function verifyTaskExistence(task: Task): boolean {
  // Logic for verifying task existence
  return tasks.some((t) => t.id === task.id);
}


Enter fullscreen mode Exit fullscreen mode

优美、优雅、井然有序的代码。此原则是其他原则的基础;运用此原则,您可以创建高质量、可读性强且易于维护的代码。

2. O - 开放-封闭原则

开路保护协议

OCP-开放封闭原则

对象或实体应该对扩展开放,但对修改关闭。如果需要添加功能,最好扩展它,而不是修改源代码。

想象一下,您需要一个类来计算一些多边形的面积。



class Circle {
  radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Square {
  sideLength: number;

  constructor(sideLength: number) {
    this.sideLength = sideLength;
  }

  calculateArea(): number {
    return this.sideLength ** 2;
  }
}

class areaCalculator {
  totalArea(shapes: Shape[]): number {
    let total = 0;

    shapes.forEach((shape) => {
      if (shape instanceof Square) {
        total += (shape as any).calculateArea();
      } else {
        total += shape.area();
      }
    });

    return total;
  }
}


Enter fullscreen mode Exit fullscreen mode

这个areaCalculator类的任务是计算不同多边形的面积,每个多边形都有各自的面积逻辑。如果你,小开发者,需要添加新的形状,比如三角形或矩形,你得修改这个类才能实现这些变化,对吧?这时你就会遇到问题,违反了Open-Closed Principle……

想到什么解决方案了?可能是在类中添加另一个方法,问题就解决了🤩。不完全是,小徒弟😓,问题就在这里!

修改现有的类来添加新的行为会带来严重的风险,可能会给已经运行的程序带来错误。

记住:OCP 坚持认为一个类应该对修改关闭,对扩展开放。

看看重构代码带来的美妙之处:



interface Shape {
  area(): number;
}

class Circle implements Shape {
  radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Square implements Shape {
  sideLength: number;

  constructor(sideLength: number) {
    this.sideLength = sideLength;
  }

  area(): number {
    return this.sideLength ** 2;
  }
}

class AreaCalculator {
  totalArea(shapes: Shape[]): number {
    let total = 0;

    shapes.forEach((shape) => {
      total += shape.area();
    });

    return total;
  }
}


Enter fullscreen mode Exit fullscreen mode

看看这个AreaCalculator类:它不再需要知道要调用哪些方法来注册这个类。它可以通过调用接口强加的契约来正确地调用 area 方法,而这才是它唯一需要做的事情。

只要它们实现 Shape 接口,一切就可以正常运行。

将可扩展行为分离到接口后面并反转依赖关系。

鲍勃叔叔

  • Open for extension:您可以向类添加新功能或行为,而无需更改其源代码。
  • Closed for modification:如果您的类已经具有正常运行的功能或行为,请不要更改其源代码来添加新内容。

3. L-里氏替换原则

语言服务提供商

LSP - 里氏替换原则

里氏替换原则指出,派生类必须可以替换其基类。

这项原则由 Barbara Liskov 于 1987 年提出,单凭她的解释可能有点难以理解。不过不用担心,我会提供另一个解释和一个例子来帮助你理解。

如果对于每个类型 S 的对象 o1,都有一个类型 T 的对象 o2,并且对于所有以 T 定义的程序 P,当 o1 替换为 o2 时,P 的行为保持不变,则 S 是 T 的子类型。

芭芭拉·利斯科夫,1987年

你明白了吧?不,可能还没明白。是啊,我第一次读的时候没明白(之后一百遍也没明白),不过等等,还有另一种解释:

如果 S 是 T 的子类型,那么程序中类型 T 的对象可以被类型 S 的对象替换,而不会改变该程序的属性。

维基百科

如果你更倾向于视觉学习,请不要担心,这里有一个例子:



class Person {
  speakName() {
    return "I am a person!";
  }
}

class Child extends Person {
  speakName() {
    return "I am a child!";
  }
}

const person = new Person();
const child = new Child();

function printName(message: string) {
  console.log(message);
}

printName(person.speakName()); // I am a person!
printName(child.speakName()); // I am a child!


Enter fullscreen mode Exit fullscreen mode

父类和派生类作为参数传递,代码仍然按预期运行。神奇吗?没错,这就是我们的朋友 Barb 的神奇之处。

违规示例:

  • 覆盖/实现不执行任何操作的方法;
  • 从基类返回不同类型的值。
  • 引发意外异常;

4. I - 接口隔离原则

互联网服务提供商

ISP——接口隔离原则

这句话的意思是,不应该强迫一个类实现它不需要的接口和方法。与其创建一个庞大而通用的接口,不如创建更具体的接口。

在下面的示例中,创建了一个Book接口来抽象书籍行为,然后类实现该接口:



interface Book {
  read(): void;
  download(): void;
}

class OnlineBook implements Book {
  read(): void {
    // does something
  }

  download(): void {
    // does something
  }
}

class PhysicalBook implements Book {
  read(): void {
    // does something
  }

  download(): void {
    // This implementation doesn't make sense for a book
    // it violates the Interface Segregation Principle
  }
}


Enter fullscreen mode Exit fullscreen mode

通用Book接口强制类PhysicalBook具有无意义的行为(或者我们是否在 Matrix 中下载实体书籍?)并且违反了ISPLSP原则。

使用ISP解决此问题



interface Readable {
  read(): void;
}

interface Downloadable {
  download(): void;
}

class OnlineBook implements Readable, Downloadable {
  read(): void {
    // does something
  }

  download(): void {
    // does something
  }
}

class PhysicalBook implements Readable {
  read(): void {
    // does something
  }
}


Enter fullscreen mode Exit fullscreen mode

现在好多了。我们download()Book接口中移除了该方法,并将其添加到派生接口中Downloadable。这样,该行为就在我们的上下文中被正确隔离了,并且我们仍然遵循接口隔离原则

5. D - 依赖倒置原则

蘸

DIP——依赖倒置原则

这个是这样的:依赖于抽象而不是实现。

高级模块不应该依赖于低级模块。两者都应该依赖于抽象。

抽象不应该依赖于细节。细节应该依赖于抽象。

鲍勃叔叔

现在我将展示一段简单的代码来说明DIP。在这个例子中,有一个从数据库获取用户的服务。首先,让我们创建一个与数据库连接的具体类:



// Low-level module
class MySQLDatabase {
  getUserData(id: number): string {
    // Logic to fetch user data from MySQL database
  }
}


Enter fullscreen mode Exit fullscreen mode

现在,让我们创建一个依赖于具体实现的服务类:



// High-level module
class UserService {
  private database: MySQLDatabase;

  constructor() {
    this.database = new MySQLDatabase();
  }

  getUser(id: number): string {
    return this.database.getUserData(id);
  }
}


Enter fullscreen mode Exit fullscreen mode

在上面的例子中,UserService直接依赖于 的具体实现MySQLDatabase。这违反了DIP,因为高级类 UserService 直接依赖于低级类。

如果我们想切换到不同的数据库系统(例如,PostgreSQL),我们需要修改UserService类,它是AWFUL

让我们使用DIP来修复这段代码。高级类不应该依赖于具体的实现,UserService而应该依赖于抽象。让我们创建一个Database接口作为抽象:



// Abstract interface (abstraction) for the low-level module
interface Database {
  getUserData(id: number): string;
}


Enter fullscreen mode Exit fullscreen mode

现在,具体的实现MySQLDatabase应该PostgreSQLDatabase实现这个接口:



class MySQLDatabase implements Database {
  getUserData(id: number): string {
    // Logic to fetch user data from MySQL database
  }
}

// Another low-level module implementing the Database interface
class PostgreSQLDatabase implements Database {
  getUserData(id: number): string {
    // Logic to fetch user data from PostgreSQL database
  }
}


Enter fullscreen mode Exit fullscreen mode

最后,UserService 类可以依赖于Database抽象:



class UserService {
  private database: Database;

  constructor(database: Database) {
    this.database = database;
  }

  getUser(id: number): string {
    return this.database.getUserData(id);
  }
}


Enter fullscreen mode Exit fullscreen mode

这样,UserService类就依赖于Database抽象,而不是具体的实现,从而满足依赖倒置原则

结论

通过采用这些原则,开发人员可以创建更能适应变化的系统,使维护更容易,并随着时间的推移提高代码质量。

本文内容源自其他多篇文章、我的个人笔记以及我在深入研究面向对象编程 (OOP)时遇到的数十个在线视频🤣。示例中使用的代码片段是基于我对这些原则的理解和诠释而创建的。我的小徒弟,我真心希望我的贡献能够帮助你加深理解,并在学习中取得进步。

我真的希望你喜欢这篇文章,别忘了关注!

注:图片取自本文

文章来源:https://dev.to/lukeskw/solid-principles-theyre-rock-solid-for-good-reason-31hn
PREV
孙子兵法——如何更快更有效地消灭虫子
NEXT
初学者编程游戏 初学者编程最佳五款游戏!简介 1- Pong 2 - Space Race 3 - Jet Fighter 4 - Space Invaders 5 - Monaco GP 一些值得一提的游戏:然后呢?谢谢