SOLID 原则:编写 SOLID 程序;避免 STUPID 程序
大家好!这是我个人博客上文章的修订版。
之前,在我的上一篇文章中,我解释了一些必须了解的基本编程原则,这些原则适用于你遵循的任何编程范式。无论是函数式编程还是面向对象编程,这些原则都是基本原则。
本文纯粹讨论了另外 5 条设计原则,其中大多数具体适用于可以使用 OOP 范式解决的问题。
随着 OOP 范式的兴起,带来了编写问题解决方案的新设计和技术。
类似地,从更大范围来看,这种技术导致我们设计和编写的解决方案中存在一些缺陷,而我们常常无法识别以STUPID 代码形式添加的错误。
当我开始使用 Typescript 标准编程时,实现面向对象编程 (OOPS) 变得更加容易、更好、更小巧和简洁。从函数式范式转向面向对象编程 (OOP) 范式后,我意识到,我们最终会有意无意地在代码库中实现某种反模式。
什么是STUPID代码库?
愚蠢的代码库是指存在缺陷或故障的代码库,会影响可维护性、可读性或效率。
反模式代码 == 愚蠢的代码
什么原因导致了 STUPID 代码库?
- 单例模式:违反单例模式本质上会降低现有代码的灵活性和可复用性,这些代码处理对象创建机制。这是一种反模式,我们在同一个脚本/文件中定义一个类及其对象,并导出该对象以实现可复用性。这种模式本身并没有错,但如果到处不恰当地使用它,就会让代码库出现问题。
/**
*
* 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
*/
- 紧耦合:类之间或不同独立功能之间过度的耦合/依赖是一种代码异味,我们在开发或编程时需要非常小心。当一个方法访问另一个对象的数据比访问自身数据更多,或者出现某种函数链式调用的情况时,我们就可以认定为紧耦合。
/**
* 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();
}
}
-
不可测试性:单元测试是软件开发中非常重要的一部分,通过单元测试,您可以交叉检查并测试您构建的组件是否完全按照预期运行。始终建议在编写测试用例后再发布产品。发布未经测试的代码/产品与部署您不确定其行为的应用程序非常相似。
除了单元测试之外,我们还有其他测试,例如集成测试、端到端测试等,这些测试是根据用例和必要性进行的。 -
过早优化:如果重构代码毫无理由地无法提升系统的可读性或性能,则应避免。
过早优化也可以定义为在没有足够数据支撑的情况下,仅仅依靠直觉,就试图优化代码,期望它能够提升性能或可读性。 -
缺乏描述性的命名:描述性命名和命名约定是两个重要的标准。大多数时候,命名是最令人头疼的问题。
一段时间后,当你或其他开发人员访问代码库时,你可能会问:“这个变量是做什么的?” 我们无法确定一个变量、类、类对象/实例或函数的最佳描述性名称。为了提高可读性和易理解性,命名一个描述性的名称非常重要。
/**
* 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
- 重复:有时,代码重复是复制粘贴的结果。违反 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 {...}
}
上述实现中的问题是,处理业务逻辑和与数据库调用相关的方法在同一个类中耦合在一起,这违反了单一职责原则。
可以编写相同的代码,通过分别划分处理业务逻辑和数据库调用的职责,确保不违反 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 {...}
}
在这里,我们确保特定的类解决特定的问题;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);
}
}
}
上述方法的主要缺点是,如果需要一种更新的发送通知或组合通知机制的方式,那么我们需要改变sendNotification()的定义。
这可以确保不违反 SOLID 原则,如下所示,
/**
* Simple Example for Notification System Class
*
* S'O'LID Approach of Programming
*
*/
class NotificationSystem {
sendMobileNotification() {...}
sendDesktopNotification() {...}
sendEmail() {...}
sendEmailwithMobileNotification() {
this.sendEmail();
this.sendMobileNotification()
}
}
正如您在上面的示例中看到的,当您需要另一个同时发送电子邮件和移动通知的需求时,我所做的就是添加另一个函数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?
*/
我们从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
*/
接口隔离原则(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();
}
这里我们看到,为商店中的商品/商品创建了一个接口 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();
}
该原则注重将一组动作划分为更小的部分,以便 Class 执行所需的部分。
- 依赖倒置原则(DIP)
该原则指出我们应该依赖于抽象。抽象不应该依赖于实现。我们功能的实现应该依赖于我们的抽象。
人们应该“依赖抽象,而不是具体。”
——维基百科
依赖注入与另一个术语“控制反转”密切相关。这两个术语可以在两种情况下进行不同的解释。
- 基于框架
- 基于非框架(通用)
依赖注入是基于框架编程的,是 IoC(即控制反转)的一种应用。从技术上讲,控制反转是一种编程原则,即反转程序流程的控制。
简而言之,程序的控制被反转了,也就是说,不再由程序员控制程序的流程。IOC 是框架内置的,也是区分框架和库的一个因素。Spring Boot就是最好的例子。
瞧!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
}
如果您研究上面的例子,对象的创建依赖于接口而不是类。
这些是 OOP 范式编程原则,可让您的代码更具可读性、可维护性和简洁性。
作为开发人员,我们应该避免编写肮脏或愚蠢的代码。这些是我们在开发过程中需要牢记的基本事项。
SOLID并非万能药,也无法解决所有问题。计算机科学中的一些问题可以通过基本的工程技术来解决。SOLID 正是其中一种技术,它帮助我们维护健康的代码库和干净的软件。这些原则的优势并非立竿见影,但随着时间的推移,以及在软件维护阶段,它们会逐渐被注意到并显现出来。
作为一名开发者,我建议你每次设计或编写解决方案时,都要问问自己“我是否违反了 SOLID 原则?”。如果你的答案是“是”,代码太长了,那么你应该知道自己做错了。
我可以保证的是,这些原则总能帮助我们编写出更好的代码。
如果您喜欢这篇文章,请点击“赞”按钮,分享文章并订阅博客。如果您想让我撰写一篇关于我所擅长的特定领域/技术的文章,请随时发送邮件至shravan@ohmyscript.com。
请继续关注我的下一篇文章。
就到这里吧。感谢您的阅读。
下次再见。
祝您学习愉快。