以前的部分:
我们正在继续考虑使我们的代码干净、灵活和可维护的方法。现在,让我们开始研究干净的架构解决方案。
SOLID 原则由 Robert C. Martin 提出,被广泛认为是面向对象编程和软件设计的最佳实践。
在本文中,我们将通过实际示例来了解三个 SOLID 原则:单一职责原则、开放/封闭原则和里氏替换原则。
一个类应该只有一个改变的理由
换句话说,一个类应该有单一的职责或工作。这很重要,因为具有单一职责的类更容易理解、修改和维护。代码某一区域的更改不会波及整个系统,从而降低了引入错误的风险。
让我们看一个实际的例子以及遵循单一职责原则的方法:
// 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 旨在增强代码模块化,最大限度地减少相互依赖性带来的挑战。通过将代码组织成函数、分离的类并促进模块化,它变得更加可重用,从而节省了可能花费在重新编码现有功能上的时间。
软件实体(类、模块、函数)应该对扩展开放,但对修改关闭
您应该能够在不更改现有代码的情况下添加新功能。这很重要,因为通过遵守这一原则,您可以向系统引入新功能或组件,而不会危及现有代码的稳定性。
它提高了代码的可重用性,并减少了扩展功能时进行大量更改的需要。
// 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(...); } }
遵循这一原则,您将减少代码耦合并使系统免受不可预测的行为的影响。
超类的对象应该可以用其子类的对象替换,而不会破坏应用程序。
为了理解这个原理,我们来看一下这个例子:
// 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
类的问题。 Designer
、 Programmer
、 Seller
都是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 {/../} }
我们创建了新的抽象层Employee
和Contractor
,并将access
方法移至Employee
类并定义了具体的实现。如果我们用子类Contractor
替换Worker
类, Worker
功能将不会被破坏。
如果您遵循 LSP,则可以在需要基类实例的任何地方替换任何子类实例,并且程序仍应按预期工作。这可以促进代码重用、模块化,并使您的代码更能适应变化。
在下一篇文章中,我们将了解接口隔离和依赖倒置 SOLID 原则。