paint-brush
简洁代码:TS 中的单一职责、开放/封闭、里氏替换 SOLID 原则 [第 4 部分]经过@alenaananich
4,771 讀數
4,771 讀數

简洁代码:TS 中的单一职责、开放/封闭、里氏替换 SOLID 原则 [第 4 部分]

经过 Alena Ananich7m2023/11/09
Read on Terminal Reader

太長; 讀書

在本文中,我们将通过实际例子来了解三个 SOLID 原则:单一责任原则、开闭原则和里氏替换原则。
featured image - 简洁代码:TS 中的单一职责、开放/封闭、里氏替换 SOLID 原则 [第 4 部分]
Alena Ananich HackerNoon profile picture


以前的部分:


我们正在继续考虑使我们的代码干净、灵活和可维护的方法。现在,让我们开始研究干净的架构解决方案。


SOLID 原则由 Robert C. Martin 提出,被广泛认为是面向对象编程和软件设计的最佳实践。


在本文中,我们将通过实际示例来了解三个 SOLID 原则:单一职责原则、开放/封闭原则和里氏替换原则。

目录:

  1. 单一责任原则
  2. 开闭原理
  3. 里氏替换原则

1.单一职责原则(SRP)

一个类应该只有一个改变的理由


换句话说,一个类应该有单一的职责或工作。这很重要,因为具有单一职责的类更容易理解、修改和维护。代码某一区域的更改不会波及整个系统,从而降低了引入错误的风险。


让我们看一个实际的例子以及遵循单一职责原则的方法:

 // Bad class UserSettingsService { constructor(user: IUser) { this.user = user; } changeSettings(settings: IUserSettings): void { if (this.isUserValidated()) { // ... } } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): Promise<boolean> { const userInfo = await this.getUserInfo(); // ... } }

在此示例中,我们的类在不同方向上执行操作:它设置上下文、更改上下文并验证它。


为了遵循 SRP,我们需要划分这些不同的职责。

 // Better class UserAuth { constructor(user: IUser) { this.user = user; } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): boolean { const userInfo = await this.getUserInfo(); // ... } } class UserSettings { constructor(user: IUser) { this.user = user; this.auth = new UserAuth(user); } changeSettings(settings: IUserSettings): void { if (this.auth.isUserValidated()) { // ... } } }


它为什么如此重要?

SRP 旨在增强代码模块化,最大限度地减少相互依赖性带来的挑战。通过将代码组织成函数、分离的类并促进模块化,它变得更加可重用,从而节省了可能花费在重新编码现有功能上的时间。

2. 开闭原理(OCP)

软件实体(类、模块、函数)应该对扩展开放,但对修改关闭


您应该能够在不更改现有代码的情况下添加新功能。这很重要,因为通过遵守这一原则,您可以向系统引入新功能或组件,而不会危及现有代码的稳定性。


它提高了代码的可重用性,并减少了扩展功能时进行大量更改的需要。

 // Bad class Product { id: number; name: string[]; price: number; protected constructor(id: number, name: string[], price: number) { this.id = id; this.name = name; this.price = price; } } class Ananas extends Product { constructor(id: number, name: string[], price: number) { super(id, name, price); } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } } class HttpRequestCost { constructor(product: Product) { this.product = product; } getDeliveryCost(): number { if (product instanceOf Ananas) { return requestAnanas(url).then(...); } if (product instanceOf Banana) { return requestBanana(url).then(...); } } } function requestAnanas(url: string): Promise<ICost> { // logic for ananas } function requestBanana(url: string): Promise<ICost> { // logic for bananas }

在这个例子中,问题出在HttpRequestCost类中,该类的getDeliveryCost方法中包含不同类型产品的计算条件,并且我们对每种类型的产品使用单独的方法。所以,如果我们需要添加新类型的产品,我们应该修改HttpRequestCost类,这是不安全的;我们可能会得到意想不到的结果。


为了避免这种情况,我们应该在Product类中创建一个不带实现的抽象方法request 。具体实现将继承类:Ananas 和 Banana。他们会为自己实现这个要求。


HttpRequestCost将采用Product类接口后面的product参数,当我们在HttpRequestCost中传递特定的依赖项时,它已经为自己实现了request方法。

 // Better abstract class Product { id: number; name: string[]; price: string; constructor(id: number, name: string[], price: string) { this.id = id; this.name = name; this.price = price; } abstract request(url: string): void; } class Ananas extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for ananas } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for bananas } } class HttpRequestCost { constructor(product: Product) { this.product = product; } request(): Promise<void> { return this.product.request(url).then(...); } }


它为什么如此重要?

遵循这一原则,您将减少代码耦合并使系统免受不可预测的行为的影响。

3.里氏替换原理(LSP)

超类的对象应该可以用其子类的对象替换,而不会破坏应用程序。


为了理解这个原理,我们来看一下这个例子:

 // Bad class Worker { work(): void {/../} access(): void { console.log('Have an access to closed perimeter'); } } class Programmer extends Worker { createDatabase(): void {/../} } class Seller extends Worker { sale(): void {/../} } class Designer extends Worker { access(): void { throwError('No access'); } }

在此示例中,我们遇到了Contractor类的问题。 DesignerProgrammerSeller都是Worker,他们继承自父类Worker 。但与此同时,设计师无权进入封闭边界,因为他们是承包商,而不是雇员。我们重写了access方法并打破了里氏替换原则。


这个原则告诉我们,如果我们用它的子类(例如Designer类)替换超类Worker ,功能不应该被破坏。但如果我们这样做, Programmer类的功能将被破坏 - access方法将具有来自Designer类的意外实现。


遵循里氏替换原则,我们不应该重写子类中的实现,而是需要创建新的抽象层,在其中为每种类型的抽象定义特定的实现。


让我们纠正一下:

 // Better class Worker { work(): void {/../} } class Employee extends Worker { access(): void { console.log('Have an access to closed perimeter'); } } class Contractor extends Worker { addNewContract(): void {/../} } class Programmer extends Employee { createDatabase(): void {/../} } class Saler extends Employee { sale(): void {/../} } class Designer extends Contractor { makeDesign(): void {/../} }

我们创建了新的抽象层EmployeeContractor ,并将access方法移至Employee类并定义了具体的实现。如果我们用子类Contractor替换Worker类, Worker功能将不会被破坏。

它为什么如此重要?

如果您遵循 LSP,则可以在需要基类实例的任何地方替换任何子类实例,并且程序仍应按预期工作。这可以促进代码重用、模块化,并使您的代码更能适应变化。


在下一篇文章中,我们将了解接口隔离和依赖倒置 SOLID 原则。