继续我们的 SOLID 原则之旅,讨论对代码设计有深远影响的接口隔离原则。
当初学者开始他们的编程旅程时,最初的重点通常是算法和适应一种新的思维方式。一段时间后,他们深入研究面向对象编程(OOP)。
如果这个转变被延迟,那么从函数式编程思维模式的转变可能会有挑战性,然而,最终,他们会接受对象的使用,并在必要时将它们合并到代码中,有时甚至在不需要它们的地方。
当他们学习抽象并努力使代码更可重用时,他们可能会过度概括,导致抽象应用到所有地方,这可能会阻碍未来的开发。
在某种程度上,他们开始意识到为过度泛化设置边界的重要性。幸运的是,接口隔离原则已经为此提供了指导方针,代表了SOLID中的“I”。
怎么算违反接口隔离原则
接口隔离原则(ISP)指出,我们应该保持接口小,这样用户就不会依赖于他们不需要的东西。
Uncle Bob提出了这个原则,你可以在他的博客上找到更多细节,这个原则的简单表述就是:尽可能地缩小接口,我们不应该只把接口理解为一个方法接口,而是应该更多地关注接口所包含的特性的内聚性。
重大声明:JavaScript本身并不支持接口,但它们作为TypeScript的概念存在,以便应用接口隔离原则。
为了理解这个原则是如何被违背的,让我们检查一下下面处理 User
接口的代码块:
User接口
interface User {
addToShoppingCart(product: Product);
isLoggedIn(): boolean;
pay(money: Money);
hasPremium(): boolean;
}
Guest类
class Guest implements User {
constructor(
private cart: ShoppingCart
) {}
public addToShoppingCart(product: Product) {
this.cart.Add(product);
}
public isLoggedIn(): boolean {
// should this be obvious?
return false;
}
public pay(money: Money) {
// should unathenticated user be able to pay?
throws new Error("user is not logged in");
}
public hasPremium(): boolean {
// should this be obvious?
return false;
}
}
Customer类
class Customer implements User {
constructor(
private cart: ShoppingCart,
private wallet: Wallet,
private isPremium: boolean
) {}
public addToShoppingCart(product: Product) {
this.cart.Add(product);
}
public isLoggedIn(): boolean {
// should this be obvious?
return true;
}
public pay(money Money) {
this.wallet.Deduct(money);
}
public hasPremium(): boolean {
this.isPremium;
}
}
假设我们想要交付一个购物应用程序,其中一种方法是定义一个接口 User
,就像我们在代码示例中所做的那样。这个接口包含用户可以拥有的许多特性。
我们平台上的 User
可以为 ShoppingCart
添加 Product
,他们可以购买,如果他们有高级账户,他们可以获得折扣。
这个接口的实际实现有两个类。第一个是 Guest
。它应该是一个没有登录到我们的系统的 User
,但至少他们可以向 ShoppingCart
添加一个 Product
。第二个实现是 Customer
,它可以使用我们系统的所有功能。
方法 HasPremium
和 Pay
对于 Guest
没有任何意义。如果那个类代表那些没有登录的 Users
,为什么我们要考虑实现折扣的方法呢?
方法 IsLoggedIn
对于这两种实现都没有任何意义,如果它应该支持为授权用户保留的特性,那么它就是实现的实际决策者。
为了更好地泛化,我们在代码中引入了一些不必要的实现,我们得到了:
-
类中没有使用的方法。
-
我们需要标记的方法,以便其他人不要使用它们。
-
单元测试的大量额外代码。
-
非自然的多态性。
-
…
让我们重构一下。
如何遵守接口隔离原则
为了让代码遵守接口隔离原则,必须围绕最小内聚特性组构建接口。
user接口
interface User {
addToShoppingCart(product: Product);
}
interface LoggedInUser implements User {
pay(money: Money);
hasPremium(): boolean;
}
现在,我们有两个接口,而不是一个接口,LoggedInUser
代表会话中完全授权的用户,User
代表网站上的所有用户。
注意:我们确实增加了一个接口,但我们删除了一个方法:IsLoggedIn
.那些方法不是我们接口签名的一部分。
Guest类
class Guest implements User {
constructor(
private cart: ShoppingCart
) {}
public addToShoppingCart(product: Product) {
this.cart.Add(product);
}
}
Customer类
class Customer implements LoggedInUser {
constructor(
private cart: ShoppingCart,
private wallet: Wallet,
private isPremium: boolean
) {}
public addToShoppingCart(product: Product) {
this.cart.Add(product);
}
public pay(money Money) {
this.wallet.Deduct(money);
}
public hasPremium(): boolean {
this.isPremium;
}
}
以前所有的类现在都更轻量了,而不是每个类都有五个方法,其中许多方法根本就没有用到,现在它们只有真正需要的方法。
结论
接口隔离原则是SOLID原则中的第四条,由SOLID中的字母“I”代表。它教导我们尽可能地保持接口的最小化。
欢迎关注公众号:文本魔术,了解更多