不健康的代码:过度使用原始代码
原始过度使用的识别
欺骗性布尔值
如何保持代码健康
保持联系
导航你的软件开发职业通讯
经典的“代码异味”之一被称为原始过度使用。
它看似简单。
注意:这是我的书《重构 TypeScript:保持代码健康》的摘录。
原始过度使用的识别
以此代码为例:
const email: string = user.email;
if(email !== null && email !== "") {
// Do something with the email.
}
注意到我们正在处理电子邮件的原始数据吗?
或者,考虑一下:
const firstname = user.firstname || "";
const lastname = user.lastname || "";
const fullName: string = firstname + " " + lastname;
注意到所有额外的检查,以确保用户名不被篡改了吗null
?你肯定见过这样的代码。
这里出了什么问题?
这段代码有什么问题?有几点需要思考:
-
这种逻辑是不可共享的,因此会在各处被复制
-
在更复杂的场景中,很难看出底层业务概念代表什么(这导致代码难以理解)
-
如果存在一个潜在的商业概念,那么它是隐含的,而不是明确的
偶然的商业理念
上述代码示例中的业务概念类似于用户的显示名称或全名。
然而,这个概念只是暂时存在于一个恰好被正确命名的变量中。它会在其他地方被同样命名吗?如果你的团队里还有其他开发人员——很可能不会。
我们的代码从业务角度来看可能难以掌握,在复杂场景中难以理解,并且无法与应用程序中的其他地方共享。
我们该如何处理这个问题?
欺骗性布尔值
原始类型应该是我们在代码中创建更有用的面向业务的概念/抽象的构建块。
这有助于每个特定的业务概念将其所有逻辑集中在一个地方(这意味着我们可以更轻松地共享和推理它),实现更强大的错误处理,减少错误等。
我想看看我经历过的导致原始过度使用的最常见原因。我经常看到这种情况。
设想
假设我们正在开发一个网络应用程序,帮助客户在线销售二手物品。
我们被要求在系统中验证用户身份的部分添加一些额外的规则。
目前,系统仅检查用户是否已成功验证。
const isAuthenticated: boolean = await userIsAuthenticated(username, password);
if(isAuthenticated) {
redirectToUserDashboard();
} else {
returnErrorOnLoginPage("Credentials are not valid.");
}
新商业规则
我们公司现在要求我们检查用户是否处于活跃状态。不活跃的用户将无法登录。
许多开发人员会做这样的事情:
const user: User = await userIsAuthenticated(username, password);
const isAuthenticated: boolean = user !== null;
if(isAuthenticated) {
if(user.isActive) {
redirectToUserDashboard();
} else {
returnErrorOnLoginPage("User is not active.");
}
} else {
returnErrorOnLoginPage("Credentials are not valid.");
}
哦不!我们引入了代码异味,我们知道这会导致可维护性问题!
我们现在有一些空检查和嵌套条件(它们都是不健康代码的标志,在《重构 TypeScript》一书中有所提及。)
因此,让我们首先通过应用(a)特殊情况模式和(b)保护子句来重构它(这两种技术在书中都有详细解释。)
// This will now always return a User, but it may be a special case type
// of User that will return false for "user.isAuthenticated()", etc.
const user: User = await userIsAuthenticated(username, password);
// We've created guard clauses here.
if(!user.isAuthenticated()) {
returnErrorOnLoginPage("Credentials are not valid.");
}
if(!user.isActive()) {
returnErrorOnLoginPage("User is not active.");
}
redirectToUserDashboard();
好多了。
更多规则...
现在您的经理已经看到您能够多快地添加新的业务规则,他们还需要一些其他规则。
-
如果用户的会话已经存在,则将用户发送到特殊的主页。
-
如果用户由于登录尝试次数过多而锁定了他们的帐户,则将他们发送到一个特殊页面。
-
如果这是用户首次登录,则将他们发送到特殊的欢迎页面。
哎呀!
如果您已经在这个行业工作了几年,那么您就会知道这种情况有多么普遍!
乍一看,我们可能会做一些天真的事情:
// This will now always return a User, but it may be a special case type
// of User that will return false for "user.isAuthenticated()", etc.
const user: User = await userIsAuthenticated(username, password);
// We've created guard clauses here.
if(!user.isAuthenticated()) {
returnErrorOnLoginPage("Credentials are not valid.");
}
if(!user.isActive()) {
returnErrorOnLoginPage("User is not active.");
}
if(user.alreadyHadSession()) {
redirectToHomePage();
}
if(user.isLockedOut()) {
redirectToUserLockedOutPage();
}
if(user.isFirstLogin()) {
redirectToWelcomePage();
}
redirectToUserDashboard();
注意到了吗,因为我们引入了保护子句,所以在这里添加新逻辑变得容易多了?这是提高代码质量的一大好处——它能让将来的代码修改和添加新逻辑变得更容易。
但是,在这种情况下,存在一个问题。你能发现吗?
我们的User
课程正在成为所有身份验证逻辑的垃圾场。
真的那么糟糕吗?
有那么糟糕吗?是的。
想想看:你的应用中还有哪些地方需要这些数据?没有地方——都是身份验证逻辑。
一种重构方法是创建一个名为的新类,AuthenticatedUser
并在该类中只放置与身份验证相关的逻辑。
这将遵循单一责任原则。
但是,针对这个特定场景,我们可以采取更简单的解决方法。
只使用枚举
每当我看到这种模式(方法的结果是一个布尔值,或者是一个具有布尔值的对象,这些布尔值会被立即检查/测试)时,用枚举替换布尔值是一种更好的做法。
从上面的最后一个代码片段中,让我们将方法更改userIsAuthenticated
为更准确地描述我们正在尝试做的事情:tryAuthenticateUser
。
并且,我们不会返回 aboolean
或 a User
- 我们将返回一个枚举,告诉我们确切的结果是什么(因为这就是我们感兴趣的)。
enum AuthenticationResult {
InvalidCredentials,
UserIsNotActive,
HasExistingSession,
IsLockedOut,
IsFirstLogin,
Successful
}
我们的新枚举将指定尝试验证用户的所有可能结果。
接下来,我们将使用该枚举:
const result: AuthenticationResult = await tryAuthenticateUser(username, password);
if(result === AuthenticationResult.InvalidCredentials) {
returnErrorOnLoginPage("Credentials are not valid.");
}
if(result === AuthenticationResult.UserIsNotActive) {
returnErrorOnLoginPage("User is not active.");
}
if(result === AuthenticationResult.HasExistingSession) {
redirectToHomePage();
}
if(result === AuthenticationResult.IsLockedOut) {
redirectToUserLockedOutPage();
}
if(result === AuthenticationResult.IsFirstLogin) {
redirectToWelcomePage();
}
if(result === AuthenticationResult.Successful) {
redirectToUserDashboard();
}
注意到这样可读性提高了吗?而且,我们不再User
用一堆不必要的额外数据来污染我们的类了!
我们返回一个值。这是简化代码的好方法。
这是我最喜欢的重构之一!希望你也会觉得它有用。
奖励:策略模式
每当我使用这种重构时,我就会自然而然地知道策略模式可能会对我们提供更多帮助。
想象一下上面的代码有更多的业务规则和路径。
我们可以使用策略模式的一种形式进一步简化它:
const strategies: any = [];
strategies[AuthenticationResult.InvalidCredentials] =
() => returnErrorOnLoginPage("Credentials are not valid.");
strategies[AuthenticationResult.UserIsNotActive] =
() => returnErrorOnLoginPage("User is not active.");
strategies[AuthenticationResult.HasExistingSession] =
() => redirectToHomePage();
strategies[AuthenticationResult.IsLockedOut] =
() => redirectToUserLockedOutPage();
strategies[AuthenticationResult.IsFirstLogin] =
() => redirectToWelcomePage();
strategies[AuthenticationResult.Successful] =
() => redirectToUserDashboard();
strategies[result]();
如何保持代码健康
这篇文章摘自《重构 TypeScript》,它旨在成为一种平易近人且实用的工具,帮助开发人员更好地构建高质量的软件。
保持联系
不要忘记通过以下方式与我联系:
导航你的软件开发职业通讯
一封电子邮件简报,助您提升软件开发职业水平!您是否想过:
✔ 软件开发人员通常经历哪些阶段?
✔ 我如何知道自己处于哪个阶段?如何进入下一个阶段?
✔ 什么是技术领导者?如何成为技术领导者?
✔ 有人愿意陪伴我并解答我的疑问吗?
听起来很有趣?加入社区吧!
文章来源:https://dev.to/jamesmh/unhealthy-code-primitive-overuse-7mh