SOLID 原则:编写 SOLID 程序;避免 STUPID 程序

2025-06-07

SOLID 原则:编写 SOLID 程序;避免 STUPID 程序

替代图像
“三思而后行,一次编码”

大家好!这是我个人博客上文章的修订版

之前,在我的上一篇文章中,我解释了一些必须了解的基本编程原则,这些原则适用于你遵循的任何编程范式。无论是函数式编程还是面向对象编程,这些原则都是基本原则

本文纯粹讨论了另外 5 条设计原则,其中大多数具体适用于可以使用 OOP 范式解决的问题。

随着 OOP 范式的兴起,带来了编写问题解决方案的新设计和技术。

类似地,从更大范围来看,这种技术导致我们设计和编写的解决方案中存在一些缺陷,而我们常常无法识别以STUPID 代码形式添加的错误

当我开始使用 Typescript 标准编程时,实现面向对象编程 (OOPS) 变得更加容易、更好、更小巧和简洁。从函数式范式转向面向对象编程 (OOP) 范式后,我意识到,我们最终会有意无意地在代码库中实现某种反模式。

什么是STUPID代码库?

愚蠢的代码库是指存在缺陷或故障的代码库,会影响可维护性、可读性或效率。

反模式代码 == 愚蠢的代码

什么原因导致了 STUPID 代码库?

AltImage
既然可以坚强,何必愚蠢

  • 单例模式:违反单例模式本质上会降低现有代码的灵活性和可复用性,这些代码处理对象创建机制。这是一种反模式,我们在同一个脚本/文件中定义一个类及其对象,并导出该对象以实现可复用性。这种模式本身并没有错,但如果到处不恰当地使用它,就会让代码库出现问题。
/**
*
*  Creating class Singleton, which is an Anti Pattern 
*  definition.
* 
*  WHY?
*  Let us see.
*/
class Singleton {
  private static instance: Singleton;
  private _value: number;

  /**
  * To avoid creating objects directly using 'new' 
  * operator
  * 
  * Therefore, the constructor is accessible to class 
  * methods only
  */
  private constructor() { } 

  /**
  * Defining a Static function, so to directly
  *  make it accessible without creating an Object
  */
  static makeInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
      Singleton.instance._value = 0;
    }
    return Singleton.instance;
  }

  getValue (): number {
    return this._value;
  }

  setValue(score) {
    this._value = score;
  }
  incrementValueByOne(): number {
    return this._value += 1;
  }
}


/**
*  Since the Singleton class's constructor is private, we  
*  need to create an instance using the static method 
*  makeInstance()
*  
*  Let us see what anomalies does that cause.
*    
*  Creating an instance using 'new' throw an Error
*  Constructor of class 'Singleton' is private and 
*  only accessible within the class declaration
*  const myInstance = new Singleton(); 
*/

const myInstance1 = Singleton.makeInstance();
const myInstance2 = Singleton.makeInstance();

console.log(myInstance1.getValue()); // OUTPUT: 0
console.log(myInstance2.getValue()); // OUTPUT: 0


myInstance1.incrementValueByOne(); // value = 1
myInstance2.incrementValueByOne(); // value = 2

console.log(myInstance1.getValue()); // OUTPUT: 2
console.log(myInstance2.getValue()); // OUTPUT: 2

/**
* This is the issue Singleton Anti-Pattern
* causing Issue with Singleton Pattern
*/
Enter fullscreen mode Exit fullscreen mode
  • 紧耦合:类之间或不同独立功能之间过度的耦合/依赖是一种代码异味,我们在开发或编程时需要非常小心。当一个方法访问另一个对象的数据比访问自身数据更多,或者出现某种函数链式调用的情况时,我们就可以认定为紧耦合。
/**
* A simple example for Tight-Coupling
*/

class Car {

  move() {
    console.log("Car is Moving");
  }

}

class Lorry {

   move(){
      console.log("Lorry is Moving");
   }

}

class Traveller1 {

  Car CarObj = new Car();

  travellerStatus(){
     CarObj.move();
  }    

}

class Traveller2 {

  Lorry LorryObj = new Lorry();

  travellerStatus(){
     CarObj.move();
  }    

}
Enter fullscreen mode Exit fullscreen mode
  • 不可测试性:单元测试是软件开发中非常重要的一部分,通过单元测试,您可以交叉检查并测试您构建的组件是否完全按照预期运行。始终建议在编写测试用例后再发布产品。发布未经测试的代码/产品与部署您不确定其行为的应用程序非常相似。
    除了单元测试之外,我们还有其他测试,例如集成测试、端到端测试等,这些测试是根据用例和必要性进行的。

  • 过早优化:如果重构代码毫无理由地无法提升系统的可读性或性能,则应避免。
    过早优化也可以定义为在没有足够数据支撑的情况下,仅仅依靠直觉,就试图优化代码,期望它能够提升性能或可读性。

  • 缺乏描述性的命名:描述性命名和命名约定是两个重要的标准。大多数时候,命名是最令人头疼的问题。
    一段时间后,当你或其他开发人员访问代码库时,你可能会问:“这个变量是做什么的?” 我们无法确定一个变量、类、类对象/实例或函数的最佳描述性名称。为了提高可读性和易理解性,命名一个描述性的名称非常重要。

/**
* Example for adding two numbers: Avoid this
*/
function a(a1,a2) { // It is less descriptive in nature
  return a1 + a2;
}

console.log(a(1,2)); // It is less descriptive in nature


/**
* Example for adding two numbers: Better Approach
*/
function sum(num1,num2) {  // sum() is descriptive
  return num1 + num2;
}

console.log(sum(1,2)); 
// Statement is descriptive in nature
Enter fullscreen mode Exit fullscreen mode
  • 重复:有时,代码重复是复制粘贴的结果。违反 DRY 原则会导致代码重复。始终建议不要在整个代码库中复制代码,因为从长远来看会造成巨大的技术债务。从更大规模和更长远的角度来看,重复会使代码维护变得繁琐乏味。

这些缺陷经常被有意或无意地忽视,而 SOLID 原则则是解决这些缺陷的最佳方法。

那么,您现在可能想知道 SOLID 原则包含哪些内容,以及它如何解决由 STUPID 假设引起的问题。这些是所有开发人员必须充分理解的编程标准,才能创建具有良好架构的产品/系统。SOLID
原则可以被视为解决代码库中任何 STUPID 缺陷所导致问题的良方。
鲍勃大叔(Uncle Bob),又名罗伯特·C·马丁(Robert C Martin),是一位软件工程师和顾问,他在其著作《Clean Coder》中提出了助记符缩写 SOLID。让我们更详细地探讨一下 SOLID 原则。

单一职责原则(SRP)

一个类、方法或函数应该承担一项功能。简单来说,它应该只实现一项特性/功能。

一个类应该只具有单一职责,也就是说,只有对软件规范的一部分的更改才能够影响该类的规范。
- 维基百科

在面向对象编程范式中,一个类应该只服务于一个目的。这并不意味着每个类应该只有一个方法,而是你在类中定义的方法应该与该类的功能相关。

让我们用一个非常基本的例子来看一下,

/**
* Here, Class User bundled with functionalities which
* deals with business logic and DB calls defined 
* in the same class
*    
* STUPID Approach
*/

class User {

constructor() {...}

/**
* These methods deal with some business logic
*/

//Add New User
public addUser(userData:IUser):IUser {...}

//Get User Details Based on userID
public getUser(userId:number):IUser {...}

//Get all user details
public fetchAllUsers():Array<IUser> {...} 

//Delete User Based on userID
public removeUser(userId:number):IUser {...}


/**
* These methods deal with Database Calls
*/

//Save User Data in DB
public save(userData:IUser):IUser {...}

//Fetch User Data based on ID
public find(query:any):IUser {...}

//Delete User Based on query
public delete(query:any):IUser {...}

}
Enter fullscreen mode Exit fullscreen mode

上述实现中的问题是,处理业务逻辑和与数据库调用相关的方法在同一个类中耦合在一起,这违反了单一职责原则

可以编写相同的代码,通过分别划分处理业务逻辑和数据库调用的职责,确保不违反 SRP,如下例所示

/**
*  We will apply the SOLID approach for the 
*  previous example and divide the responsibility. 
*
* 'S'OLID Approach  
*/

/**
* Class UserService deals with the business logic 
* related to User flow
*/

class UserService {

constructor() {...}

/**
* These methods deal with some business logic
*/

//Add New User
public addUser(userData:IUser):IUser {...}

//Get User Details Based on userID
public getUser(userId:number):IUser {...}

//Get all user details
public fetchAllUsers():Array<IUser> {...} 

//Delete User Based on userID
public removeUser(userId:number):IUser {...}
}


/**
* Class UserRepo deals with the Database Queries/Calls
* of the User flow
*/
class UserRepo {

constructor() {...}

/**
* These methods deal with database queries
*/

//Save User Data in DB
public save(userData:IUser):IUser {...}

//Fetch User Data based on ID
public find(query:any):IUser {...}

//Delete User Based on query
public delete(query:any):IUser {...}

}
Enter fullscreen mode Exit fullscreen mode

在这里,我们确保特定的类解决特定的问题;UserService 处理业务逻辑,UserRepo 处理数据库查询/调用。

开放封闭原则(OCP)

这个原则强调的是代码的灵活性。顾名思义,这个原则表明你编写的解决方案/代码应该始终对扩展开放,但对修改封闭。

软件实体……应该对扩展开放,但对修改关闭。——
维基百科

简单地说,为问题陈述编写的代码/程序(无论是类、方法还是函数)都应该这样设计:为了改变它们的行为,不需要更改它们的源代码/重新编程。

如果您获得了额外的功能,我们需要添加该额外的功能,而无需更改/重新编程现有的源代码。


/**
* Simple  Notification System Class Example for 
* violating OCP
*
* STUPID Approach of Programming
*
*/

class NotificationSystem {

 // Method used to send notification
  sendNotification = (content:any,user:any,notificationType:any):void => {

    if( notificationType == "email" ){
      sendMail(content,user); 
    }

    if( notificationType == "pushNotification" ){
      sendPushNotification(content,user); 
    }

    if( notificationType == "desktopNotification"  ){
      sendDesktopNotification(content,user); 
    }

  }

}

Enter fullscreen mode Exit fullscreen mode

上述方法的主要缺点是,如果需要一种更新的发送通知或组合通知机制的方式,那么我们需要改变sendNotification()的定义。

这可以确保不违反 SOLID 原则,如下所示,

/**
* Simple Example for Notification System Class  
*
* S'O'LID Approach of Programming
*
*/

class NotificationSystem {

    sendMobileNotification() {...}

    sendDesktopNotification() {...} 

    sendEmail() {...}

    sendEmailwithMobileNotification() {
      this.sendEmail();
      this.sendMobileNotification()
    }
}
Enter fullscreen mode Exit fullscreen mode

正如您在上面的示例中看到的,当您需要另一个同时发送电子邮件和移动通知的需求时,我所做的就是添加另一个函数sendEmailwithMobileNotification(),而无需更改先前现有函数的实现。这就是功能扩展的简单之处。

现在,我们来讨论下一个重要原则,即里氏替换原则

里氏替换原则(LSP)

这个原则是最棘手的。里氏替换原则是由 Barbara Liskov 在她的论文《数据抽象》中提出的。
现在,你一定已经知道这个原则与我们实现抽象的方式有关。

回想一下,什么是抽象/数据抽象?简而言之,就是隐藏某些细节,只展现本质特征。
例如:水是由氢和氧组成的,但我们看到的是液态物质(抽象)。

“程序中的对象应该可以被其子类型的实例替换,而不会改变该程序的正确性。”
- 维基百科

根据面向对象编程范式中的局部局部性原则 (LSP),子类永远不应破坏父类的类型定义。
简而言之,所有子类/派生类都应该可以替换其基类/父类。如果使用基类型,那么应该能够使用子类型而不会破坏任何内容。

图像

/**
* Simple hypothetical example that violates  
* Liskov Principle with real-time situation
*
* STUPID Approach
*/

class Car {
  constructor(){...}

  public getEngine():IEngine {...}  
  public startEngine():void {...}
  public move():void {...}
  public stopEngine():IEngine {...}
}
/* 
* We are extending class Car to class Cycle
*/
class Cycle extends Car {  
    constuctor(){...}
    public startCycle() {...}
    public stopCycle() {...}  
}
/**
* Since Cycle extends Car; 
* startEngine(), stopEngine() methods are also
* available which is incorrect and inaccurate abstraction
*
* How can we fix it?
*/
Enter fullscreen mode Exit fullscreen mode

我们从LSP违规中可以得出的结论是,它会导致紧耦合,并且在处理需求变更时缺乏灵活性。此外,从上述示例和原则中,我们还可以得出一个结论:OOP 不仅仅是将现实世界的问题映射到对象,它还涉及创建抽象

/**
* Simple hypothetical example that follows the 
* Liskov Principle with real-time situation
*
* SO'L'ID approach
*/

class Vehicle {
  constructor(){...}

  public move():void {...}
}

class Car extends Vehicle {
  constructor(){...}

  public getEngine():IEngine {...}  
  public startEngine():void {...}
  public move():void {...}
  public stopEngine():IEngine {...}

}

/* 
* We are extending class Car to class Cycle
*/
class Cycle extends Car {  
    constructor(){...}

    public startCycle() {...}
    public move() {...}   
    public stopCycle() {...}  
}
/**
* Since class Cycle extends Vehicle; 
* move() method is only also available and applicable
* which is precise level of abstraction
*/
Enter fullscreen mode Exit fullscreen mode

接口隔离原则(ISP)

该原则处理实现大接口时引起的缺点和问题。

“多个客户端特定接口比一个通用接口更好。”
- 维基百科

它指出我们应该将接口分解成更小的粒度,以便更好地满足需求。这对于减少未使用的代码量是必要的。

/**
*  Simplest Example that violates Interface 
*  Segregation Principle 
*
*  STUPID Approach
*
*  Interface for Shop that sells dress and shoes 
*/

interface ICommodity {
   public updateRate();
   public updateDiscount();

   public addCommodity();
   public deleteCommodity();

   public updateDressColor();
   public updateDressSize();

   public updateSoleType();

}
Enter fullscreen mode Exit fullscreen mode

这里我们看到,为商店中的商品/商品创建了一个接口 ICommodity;这是不正确的。

/**
*  Simplest Example that supports Interface 
*  Segregation Principle 
*
*  SOL'I'D Approach
*
*  Separate Interfaces for Shop that sells dress and shoes 
*/

interface ICommodity {
   public updateRate();
   public updateDiscount();
   public addCommodity();
   public deleteCommodity();
}


interface IDress {
   public updateDressColor();
   public updateDressSize();
}

interface IShoe {
   public updateSoleType();
   public updateShoeSize();
}
Enter fullscreen mode Exit fullscreen mode

该原则注重将一组动作划分为更小的部分,以便 Class 执行所需的部分。

  • 依赖倒置原则(DIP)

该原则指出我们应该依赖于抽象。抽象不应该依赖于实现。我们功能的实现应该依赖于我们的抽象。

人们应该“依赖抽象,而不是具体。”
——维基百科

依赖注入与另一个术语“控制反转”密切相关。这两个术语可以在两种情况下进行不同的解释。

  1. 基于框架
  2. 基于非框架(通用)

依赖注入是基于框架编程的,是 IoC(即控制反转)的一种应用。从技术上讲,控制反转是一种编程原则,即反转程序流程的控制。

简而言之,程序的控制被反转了,也就是说,不再由程序员控制程序的流程。IOC 是框架内置的,也是区分框架和库的一个因素。Spring Boot就是最好的例子。

altimagee瞧!Spring Boot 开发者们!控制反转说得通!不是吗?

注意:对于所有 Spring Boot 开发人员来说,就像注释如何控制你的程序流程一样,

从一般视角来看,我们可以将 IoC 定义为确保“一个对象不会创建其工作所依赖的其他对象”的原则。
同样,从一般视角来看,DIP 是 IoC 的一个子集原则,它规定定义接口是为了方便传入实现。

/**
* Simple Example for DIP
*
* STUPID Approach
*/

class Logger {
   debug(){...}

   info(){...}
}

class User {
  public log: Logger;

  constructor(private log: Logger){...} // =>Mentioning Type Logger Class

  someBusinessLogic(){...} //uses that this.log
}


/**
* Simple Example for DIP
*
* SOLI'D' Approach
*/

interface ILogger {
  debug();
  info();
  error();
}

class Logger implements ILogger{
   debug(){...}

   info(){...}
}

class User {
 public log: ILogger;

 constructor(private log: ILogger){...}
        //=>Mentioning Type Logger Interface

  someBusinessLogic(){...} //uses that this.log
}
Enter fullscreen mode Exit fullscreen mode

如果您研究上面的例子,对象的创建依赖于接口而不是类。

这些是 OOP 范式编程原则,可让您的代码更具可读性、可维护性和简洁性。

作为开发人员,我们应该避免编写肮脏或愚蠢的代码。这些是我们在开发过程中需要牢记的基本事项。

SOLID并非万能药,也无法解决所有问题。计算机科学中的一些问题可以通过基本的工程技术来解决。SOLID 正是其中一种技术,它帮助我们维护健康的代码库和干净的软件。这些原则的优势并非立竿见影,但随着时间的推移,以及在软件维护阶段,它们会逐渐被注意到并显现出来。

作为一名开发者,我建议你每次设计或编写解决方案时,都要问问自己“我是否违反了 SOLID 原则?”。如果你的答案是“是”,代码太长了,那么你应该知道自己做错了。
我可以保证的是,这些原则总能帮助我们编写出更好的代码。


如果您喜欢这篇文章,请点击“赞”按钮,分享文章并订阅博客。如果您想让我撰写一篇关于我所擅长的特定领域/技术的文章,请随时发送邮件至shravan@ohmyscript.com。

请继续关注我的下一篇文章。

就到这里吧。感谢您的阅读。

下次再见。
祝您学习愉快。

文章来源:https://dev.to/shravan20/solid-principles-write-solid-programs-avoid-stupid-programs-508h
PREV
使用 AWS Lambda 扩展为您的无服务器应用程序提供支持 Lambda 扩展示例 Lambda 扩展服务安装 Lambda 扩展示例
NEXT
在您的网络服务器上使用的安全标头