了解有关 TypeScript 中 SOLID 原则的更多信息
TypeScript 对用 JavaScript 编写干净的代码产生了巨大的影响。但总有改进的方法,编写更好、更简洁的代码的一个重要方法是遵循由 Robert C. Martin(也称为 Uncle Bob)发明的所谓 SOLID 设计原则。
在本文中,我将通过使用 TypeScript 编写的示例向您介绍这些原则。我已经在这个 Github 存储库上部署了所有示例。
单一职责原则 (SRP)
“一个类改变的原因不应该超过一个。”
一个类应该有一个目的/责任,因此只有一个改变的理由。遵循这一原则可以更好地维护代码并最大限度地减少潜在的副作用。
在以下不好的示例中,您会看到如何存在多重责任。首先,我们为一本书建模,而且,我们将这本书保存为一个文件。我们在这里遇到了两个目的:
class Book { public title: string; public author: string; public description: string; public pages: number; // constructor and other methods public saveToFile(): void { // some fs.write method to save book to file } }
第二个示例向您展示了如何通过遵循单一职责原则来处理这个问题。我们最终有两个类,而不是只有一个类。每个目的一个。
class Book { public title: string; public author: string; public description: string; public pages: number; // constructor and other methods } class Persistence { public saveToFile(book: Book): void { // some fs.write method to save book to file } }
开闭原则 (OCP)
“软件实体……应该对扩展开放,但对修改关闭。”
与其重写你的类,不如扩展它。通过不接触旧代码的新功能应该很容易扩展代码。例如,实现一个接口或类在这里非常有帮助。
在下一个示例中,您将看到错误的操作方式。我们使用了名为 AreaCalculator 的第三个类来计算 Rectangle 和 Circle 类的面积。想象一下我们稍后会添加另一个形状,这意味着我们需要创建一个新类,在这种情况下,我们还需要修改 AreaCalculator 类以计算新类的面积。这违反了开闭原则。
我们来看一下:
class Rectangle { public width: number; public height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } } class Circle { public radius: number; constructor(radius: number) { this.radius = radius; } } class AreaCalculator { public calculateRectangleArea(rectangle: Rectangle): number { return rectangle.width * rectangle.height; } public calculateCircleArea(circle: Circle): number { return Math.PI * (circle.radius * circle.radius); } }
那么,我们可以做些什么来改进这段代码呢?为了遵循开闭原则,我们只需添加一个名为 Shape 的接口,因此每个形状类(矩形、圆形等)都可以通过实现它来依赖该接口。这样,我们可以将 AreaCalculator 类简化为一个带参数的函数,而这个参数是基于我们刚刚创建的接口。
interface Shape { calculateArea(): number; } class Rectangle implements Shape { public width: number; public height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } public calculateArea(): number { return this.width * this.height; } } class Circle implements Shape { public radius: number; constructor(radius: number) { this.radius = radius; } public calculateArea(): number { return Math.PI * (this.radius * this.radius); } } class AreaCalculator { public calculateArea(shape: Shape): number { return shape.calculateArea(); } }
里氏替换原则 (LSP)
“使用指向基类的指针或引用的函数必须能够在不知情的情况下使用派生类的对象。”
使用指向上层类的指针或引用的下层类必须能够在不知情的情况下使用派生类的对象。这些低级类应该只是扩展上级类,而不是改变它。
那么我们在下一个坏例子中看到了什么?我们上了两节课。Square 类扩展了 Rectangle 类。但正如我们所见,这个扩展没有任何意义,因为我们通过覆盖属性宽度和高度来改变逻辑。
class Rectangle { public width: number; public height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } public calculateArea(): number { return this.width * this.height; } } class Square extends Rectangle { public _width: number; public _height: number; constructor(width: number, height: number) { super(width, height); this._width = width; this._height = height; } }
因此,我们不需要覆盖,而是简单地删除 Square 类并将其逻辑带到 Rectangle 类而不改变其用途。
class Rectangle { public width: number; public height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } public calculateArea(): number { return this.width * this.height; } public isSquare(): boolean { return this.width === this.height; } }
接口隔离原则
“许多特定于客户端的接口都比一个通用接口好。”
简单地说,更多的接口总比接口少的好。让我解释下一个不好的例子。
我们有一个名为 Troll 的类,它实现了一个名为 Character 的接口。但是由于我们的巨魔既不会游泳也不会说话,这个角色界面似乎不适合我们的类。
interface Character { shoot(): void; swim(): void; talk(): void; dance(): void; } class Troll implements Character { public shoot(): void { // some method } public swim(): void { // a troll can't swim } public talk(): void { // a troll can't talk } public dance(): void { // some method } }
那么我们可以通过遵循这个特定的原则来做些什么呢?我们删除了 Character 接口并将其功能拆分为四个接口,并且仅将我们的 Troll 类依赖于我们实际需要的这些接口。
interface Talker { talk(): void; } interface Shooter { shoot(): void; } interface Swimmer { swim(): void; } interface Dancer { dance(): void; } class Troll implements Shooter, Dancer { public shoot(): void { // some method } public dance(): void { // some method } }
依赖倒置原则(DIP)
“取决于抽象,[不是]具体。”
这到底是什么意思,对吧?嗯,其实很简单。让我们揭开它的神秘面纱!
在这个糟糕的例子中,我们有一个 SoftwareProject 类,它初始化 FrontendDeveloper 和 BackendDeveloper 类。但这是错误的方式,因为这两个类彼此非常相似,我的意思是,它们应该做类似的事情。因此,为了实现依赖倒置原则的目标,有更好的方法来满足需求。
class FrontendDeveloper { public writeHtmlCode(): void { // some method } } class BackendDeveloper { public writeTypeScriptCode(): void { // some method } } class SoftwareProject { public frontendDeveloper: FrontendDeveloper; public backendDeveloper: BackendDeveloper; constructor() { this.frontendDeveloper = new FrontendDeveloper(); this.backendDeveloper = new BackendDeveloper(); } public createProject(): void { this.frontendDeveloper.writeHtmlCode(); this.backendDeveloper.writeTypeScriptCode(); } }
那么我们该怎么办呢?正如我所说,它实际上非常简单,而且更容易,因为我们之前已经学习了所有其他原则。
首先,我们创建一个名为 Developer 的接口,由于 FrontendDeveloper 和 BackendDeveloper 是相似的类,我们依赖于 Developer 接口。
我们不是在 SoftwareProject 类中以单一方式初始化 FrontendDeveloper 和 BackendDeveloper,而是将它们作为一个列表来遍历它们,以便调用每个 develop() 方法。
interface Developer { develop(): void; } class FrontendDeveloper implements Developer { public develop(): void { this.writeHtmlCode(); } private writeHtmlCode(): void { // some method } } class BackendDeveloper implements Developer { public develop(): void { this.writeTypeScriptCode(); } private writeTypeScriptCode(): void { // some method } } class SoftwareProject { public developers: Developer[]; public createProject(): void { this.developers.forEach((developer: Developer) => { developer.develop(); }); } }
感谢您阅读我关于 Medium 的第一篇文章。我希望,我已经能够刷新你的知识。您可以在 Wikipedia 上阅读有关 SOLID 的更多信息。