每日反模式:键入键

2021-08-10 06:41:56

假设我们有一个创建用户的函数,并根据该用户是管理员还是客户来处理一些特定的设置: const createUser = ( attributes : UserAttributes , userType : ' admin ' | ' customer ' ): User => {用户 = 用户。创建(属性)开关(用户类型){案例'admin':setupAdmin(用户)案例'客户':setupCustomer(用户)}用户。 setupNotifications () return user } ... // 在其他文件中 const createAdmin = ( attributes : UserAttributes ): User => { return createUser ( attributes , 'admin ' ) } ... // 在另一个文件中 const createCustomer = ( attributes : UserAttributes ): User => { return createUser ( attributes , 'custom ' ) } 它的值在编译时总是已知的。我们没有将它作为来自 HTTP 请求的参数或来自数据库的值接收。它预期具有一组有限值中的一个,即它基本上是一个枚举。鉴于它基本上是一个枚举,它的实际字符串值是没有意义的:它永远不会被写入任何地方或与函数内的另一个变量的值进行比较,该参数仅用于 switch 语句(if 语句也算),即值只影响函数的控制流当你有这四种成分时,你就有了一个类型键。不是静态与动态类型中的“类型”,而是变体中的“类型”。它被称为类型键,因为它表明您的函数确实具有不同的变体,并且您想要键入特定的变体以获得您想要的行为。

这是一种反模式,因为您拥有不同类型的唯一时间是满足不同的用例,而不同的用例总是以不同的速率变化。也许管理员的设置过程在一年内没有改变,但客户的设置过程每月都在变化。每当由于不同原因或以不同速率需要更改两段代码时,您必须将这些段分开并最小化它们之间的依赖关系。否则,为了了解客户是如何创建的,您需要遍历一堆与管理相关的无关代码,使任何单个用例在不了解所有其他用例的情况下都无法理解。拆分函数并将类型键参数作为常量拉入函数中 const createAdmin = (attributes : UserAttributes): User => { const userType = 'admin' ;用户 = 用户。创建(属性); switch ( userType ) { case 'admin' : setupAdmin ( user );案例'客户':setupCustomer(用户); // 永远不会到达此代码路径 } user .设置通知();返回用户; }; const createCustomer = (attributes : UserAttributes): User => { const userType = 'customer';用户 = 用户。创建(属性); switch ( userType ) { case 'admin' : setupAdmin ( user ); // 永远不会到达此代码路径 case 'customer' : setupCustomer ( user );用户。设置通知();返回用户; }; const createAdmin = ( attributes : UserAttributes ): User => { user = User .创建(属性);设置管理员(用户);用户。设置通知();返回用户; }; const createCustomer = ( attributes : UserAttributes ): User => { user = User .创建(属性);设置客户(用户);用户。设置通知();返回用户; };您可能会发现某些操作是完全独立的,这意味着您可以打乱函数调用的顺序。假设在这种情况下 setupNotifications() 可以在 setupAdmin(user) 和 setupCustomer(user) 之前调用。然后我们可以将它们组合在一起: const createAdmin = ( attributes : UserAttributes ): User => { user = User 。创建(属性);用户。设置通知();设置管理员(用户);返回用户; }; const createCustomer = ( attributes : UserAttributes ): User => { user = User .创建(属性);用户。设置通知();设置客户(用户);返回用户; }; // 如果需要,可以内联此函数 const createAdmin = ( attributes : UserAttributes ): User => { user = createUserWithNotifications ( attributes );设置管理员(用户);返回用户; }; // 如果需要,可以内联此函数 const createCustomer = ( attributes : UserAttributes ): User => { user = createUserWithNotifications ( attributes );设置客户(用户);返回用户; }; const createUserWithNotifications = ( attributes : UserAttributes ): User => { user = User .创建(属性);用户。设置通知();返回用户; };

// 如果需要,可以内联此函数 const createAdmin = ( attributes : UserAttributes ): User => { return createUser ( attributes , setupAdmin ); }; // 如果需要,可以内联此函数 const createCustomer = ( attributes : UserAttributes ): User => { return createUser ( attributes , setupCustomer ); }; const createUser = ( attributes : UserAttributes , onCreate : ( user : User ) => void ): User => { user = User .创建(属性); onCreate ( 用户 );用户。设置通知();返回用户; };我们的类型键神奇地消失了,我们不再需要维护它了。但这不是我们进行这次重构的主要原因。这实际上是关于依赖关系。让我们比较重构前后的依赖关系:红色箭头表示从一般事物到特定事物的依赖关系。这些依赖性导致无法破译的臃肿抽象。从我们的 userType 类型键流出的迷你依赖箭头被缩小以表示这样一个事实,例如 setupAdmin 或 setupCustomer 中的更改可能不需要更改 userType,但添加/删除用户类型将需要更改。小人物代表变化的原因:也许对 setupAdmin 功能的所有变化都源于员工的功能请求,但 setupCustomer 的变化都源于产品团队。不管是什么,改变的原因是不同的。这不是更好吗?由于将 userType 排除在外,飞来飞去的箭头更少,但重要的是我们的 createUser 函数不依赖于我们的特定用例,这意味着在更改用例或添加新用例时(例如添加一个'vendor' 用户类型)我们不需要接触我们的 createUser 函数。这是开闭原则的基础:实体应该对扩展开放,对修改关闭。只有当特定实体依赖于一般实体时才能满足此原则,反之则不然。养成发现类型键并删除它们的习惯。如果你能想到一个适合类型键的例子,我想知道!