继续我们的旅程,介绍一个加强应用程序的灵活性的原则:开放/封闭原则。
许多不同的方法和原则可以实现代码的长期改进,其中一些在软件开发社区中广为人知,而另一些则仍然有些不为人知。
在我看来,这与开放/封闭原则(The Open/Closed Principle)的情况一样,由SOLID中的字母O代表。根据我的经验,只有那些真正对SOLID原则感兴趣的人才会理解这个原则的含义。
在某些情况下,我们可能在没有意识到的情况下就应用了这个原则,比如在使用策略模式时。然而,策略模式只是开放/封闭原则的一种应用。
在本文中,我们将深入研究这一原则的全部目的,并使用 TypeScript 中提供的所有示例。
怎么算违反开/闭原则
开放/封闭原则(OCP)指出,我们应该能够在不修改系统的情况下扩展系统的行为。
我们看到上面的OCP需求,是Uncle Bob在他的博客中提供的,乍一看,这似乎是一个很荒谬的需求,我们怎么能在不修改原有代码的情况下扩展它呢?
为了理解开/闭原则的真正目的,让我们检查下面的代码示例,在那里我们可以看到一些不遵守该原则的结构意味着什么,以及可能的后果:
使用 AuthenticationService
违反OCP
class AuthenticationService {
public authenticate(request: http.ClientRequest, authType: string): User {
switch (authType) {
case 'jwt':
return this.authenticateWithBearerToken(req);
case 'basic':
return this.authenticateWithBasicAuth(req);
case 'applicationKey':
return this.authenticateWithApplicationKey(req);
}
throw new InvalidAuthTypeException();
}
private authenticateWithBearerToken(req http.ClientRequest): User {
// ...
}
private authenticateWithBasicAuth(req http.ClientRequest): User {
// ...
}
private authenticateWithApplicationKey(req http.ClientRequest): User {
// ...
}
这个例子展示了一个类, AuthenticationService
。它应该找出谁是经过身份验证的用户,以访问一些资源,这取决于Web应用程序的 HTTP request
和 authType
。
方法 authenticate
检查 User
是否与 request
中的数据相关联。从 request
中检索 User
可能会因用户是否使用承运人令牌、基本授权或应用程序密钥进行授权而有所不同。
假设我们想要扩展身份验证逻辑并添加一些新的流,例如在会话中保存用户数据。在这种情况下,我们需要在 AuthenticationService
中进行调整。
这样的实施带来了一系列的问题:
-
AuthenticationService
使逻辑最初在其他地方处理。 -
任何授权逻辑的改编,可能在不同的模块中,都需要在
AuthenticationService
中进行改编。 -
为了添加提取
User
的新方法,我们总是需要修改AuthenticationService
。 -
AuthenticationService
中的逻辑不可避免地随着每个新的身份验证流而增长。 -
对
AuthenticationService
的单元测试包含了太多关于不同权限提取的技术细节。
如何遵循开放/封闭原则
开放/封闭原则指出,软件结构应该对扩展开放,但对修改封闭。
有些解决方案应该提供一些东西,以允许从外部进行扩展。在面向对象编程中,我们通过对相同的接口使用不同的实现来支持这种扩展。换句话说,我们使用多态性。
修改 AuthenticationService
interface AuthenticationProvider {
getType(): string;
authenticate(req http.ClientRequest) string[];
}
class AuthenticationService {
constructor(private providers: AuthenticationProvider[]) {}
public authenticate(request: http.ClientRequest, authType: string): User {
for (const provider of this.providers) {
if (authType === provider.getType()) {
return provider.authenticate(request);
}
}
throw new InvalidAuthTypeException();
}
}
在上面的例子中,我们可以看到一个候选者尊重了开放/封闭原则,类 AuthenticationSerivce
不再包含关于提取 User
的技术细节。
相反,我们引入了一个新的接口, AuthenticationProvider
,它包含了不同 User
提取的逻辑。例如,它可以是 BearerTokenProvider
,或者 ApiKeyProvider
,或者 BasicAuthProvider
。
现在,负责身份验证的模块也可以包含 Users
的提取器,另一方面,扩展 AuthenticationService
而无需修改它的主要目标现在是可能的。
函数的开闭原则
我们可以将开放/封闭原则应用于孤立的方法,而不仅仅是结构体/类。
使用 getCities
违反OCP
const getCities = async (sourceType: string, source: string): Promise<City[]> => {
let data: string;
if (sourceType === 'file') {
const buffer = fs.readFile(source);
data = buffer.toString();
} else if (sourceType === 'link') {
data = await fetch(source);
}
const list = JSON.parse(data);
const cities: City[] = [];
for (const item of list) {
cities.push(new City(
item.name,
item.longitude,
item.latitude,
item.countryCode
))
}
return cities;
}
函数 GetCities
从某个源读取城市列表,这个源可以是一个文件或者是互联网上的某个资源,当然,我们将来也可能从内存、Redis或者其他任何源读取数据。
因此,最好将读取原始数据的过程变得更加抽象。因此,我们可以从外部提供一个读取策略作为方法参数。
修改 getCities
type DataReader = (source: string) => Promise<string>;
const readFromFile = async (fileName: string): Promise<string> => {
const buffer = fs.readFile(fileName);
return buffer.toString();
}
const readFromLink = async (link: string): Promise<string> => {
return await fetch(link).then(response => response.text());
}
const getCities = async (reader: DataReader, source: string): Promise<City[]> => {
const data = await reader(source);
const list = JSON.parse(data);
const cities: City[] = [];
for (const item of list) {
cities.push(new City(
item.name,
item.longitude,
item.latitude,
item.countryCode
))
}
return cities;
}
正如你在上面的解决方案中看到的,我们定义了一个新的类型 DataReader
,表示从某个源读取原始数据的函数。
新的函数 ReadFromFile
和 ReadFromLink
是 DataReader
类型的实际实现。
GetCities
方法希望将 DataReader
的实际实现作为参数,然后在函数体内执行并获取原始数据。
结论
开放/封闭原则(OCP)确实是SOLID原则中的一个关键原则,强调了在不修改现有代码结构的情况下进行扩展的设计软件的重要性。
它促进了多态的使用和创建清晰的接口,以实现这种可扩展性。OCP有助于随着需求的变化和新功能的添加,使软件更具适应性和可维护性。
欢迎关注公众号:文本魔术,了解更多