💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
曾用名:以明确函数取代参数(Replace Parameter with Explicit Methods) ![](https://box.kancloud.cn/f986e27dbd786db11b49908315c049b8_371x436.jpeg) ``` function setDimension(name, value) {  if (name === "height") {   this._height = value;   return;  }  if (name === "width") {   this._width = value;   return;  } } ``` ![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg) ``` function setHeight(value) {this._height = value;} function setWidth (value) {this._width = value;} ``` ### 动机 “标记参数”是这样的一种参数:调用者用它来指示被调函数应该执行哪一部分逻辑。例如,我可能有下面这样一个函数: ``` function bookConcert(aCustomer, isPremium) {  if (isPremium) {   // logic for premium booking  } else {   // logic for regular booking  } } ``` 要预订一场高级音乐会(premium concert),就得这样发起调用: `bookConcert(aCustomer, true);`标记参数也可能以枚举的形式出现: `bookConcert(aCustomer, CustomerType.PREMIUM);`或者是以字符串(或者符号,如果编程语言支持的话)的形式出现: `bookConcert(aCustomer, "premium");`我不喜欢标记参数,因为它们让人难以理解到底有哪些函数可以调用、应该怎么调用。拿到一份API以后,我首先看到的是一系列可供调用的函数,但标记参数却隐藏了函数调用中存在的差异性。使用这样的函数,我还得弄清标记参数有哪些可用的值。布尔型的标记尤其糟糕,因为它们不能清晰地传达其含义——在调用一个函数时,我很难弄清`true`到底是什么意思。如果明确用一个函数来完成一项单独的任务,其含义会清晰得多。 `premiumBookConcert(aCustomer);`并非所有类似这样的参数都是标记参数。如果调用者传入的是程序中流动的数据,这样的参数不算标记参数;只有调用者直接传入字面量值,这才是标记参数。另外,在函数实现内部,如果参数值只是作为数据传给其他函数,这就不是标记参数;只有参数值影响了函数内部的控制流,这才是标记参数。 移除标记参数不仅使代码更整洁,并且能帮助开发工具更好地发挥作用。去掉标记参数后,代码分析工具能更容易地体现出“高级”和“普通”两种预订逻辑在使用时的区别。 如果一个函数有多个标记参数,可能就不得不将其保留,否则我就得针对各个参数的各种取值的所有组合情况提供明确函数。不过这也是一个信号,说明这个函数可能做得太多,应该考虑是否能用更简单的函数来组合出完整的逻辑。 ### 做法 - 针对参数的每一种可能值,新建一个明确函数。 > 如果主函数有清晰的条件分发逻辑,可以用分解条件表达式(260)创建明确函数;否则,可以在原函数之上创建包装函数。 - 对于“用字面量值作为参数”的函数调用者,将其改为调用新建的明确函数。 ### 范例 在浏览代码时,我发现多处代码在调用一个函数计算物流(shipment)的到货日期(delivery date)。一些调用代码类似这样: `aShipment.deliveryDate = deliveryDate(anOrder, true);`另一些调用代码则是这样: `aShipment.deliveryDate = deliveryDate(anOrder, false);`面对这样的代码,我立即开始好奇:参数里这个布尔值是什么意思?是用来干什么的? `deliveryDate`函数主体如下所示: ``` function deliveryDate(anOrder, isRush) {  if (isRush) {   let deliveryTime;   if (["MA", "CT"]   .includes(anOrder.deliveryState)) deliveryTime = 1;   else if (["NY", "NH"].includes(anOrder.deliveryState)) deliveryTime = 2;   else deliveryTime = 3;   return anOrder.placedOn.plusDays(1 + deliveryTime);  }  else {   let deliveryTime;   if (["MA", "CT", "NY"].includes(anOrder.deliveryState)) deliveryTime = 2;   else if (["ME", "NH"] .includes(anOrder.deliveryState)) deliveryTime = 3;   else deliveryTime = 4;   return anOrder.placedOn.plusDays(2 + deliveryTime);  } } ``` 原来调用者用这个布尔型字面量来判断应该运行哪个分支的代码——典型的标记参数。然而函数的重点就在于要遵循调用者的指令,所以最好是用明确函数的形式明确说出调用者的意图。 对于这个例子,我可以使用分解条件表达式(260),得到下列代码: ``` function deliveryDate(anOrder, isRush) {  if (isRush) return rushDeliveryDate(anOrder);  else    return regularDeliveryDate(anOrder); } function rushDeliveryDate(anOrder) {  let deliveryTime;  if (["MA", "CT"] .includes(anOrder.deliveryState)) deliveryTime = 1;  else if (["NY", "NH"].includes(anOrder.deliveryState)) deliveryTime = 2;  else deliveryTime = 3;  return anOrder.placedOn.plusDays(1 + deliveryTime); } function regularDeliveryDate(anOrder) {  let deliveryTime;  if (["MA", "CT", "NY"].includes(anOrder.deliveryState)) deliveryTime = 2;  else if (["ME", "NH"] .includes(anOrder.deliveryState)) deliveryTime = 3;  else deliveryTime = 4;  return anOrder.placedOn.plusDays(2 + deliveryTime); } ``` 这两个函数能更好地表达调用者的意图,现在我可以修改调用方代码了。调用代码 `aShipment.deliveryDate = deliveryDate(anOrder,true);`可以改为 `aShipment.deliveryDate = rushDeliveryDate(anOrder);`另一个分支也类似。 处理完所有调用处,我就可以移除`deliveryDate`函数。 这个参数是标记参数,不仅因为它是布尔类型,而且还因为调用方以字面量的形式直接设置参数值。如果所有调用`deliveryDate`的代码都像这样: ``` const isRush = determineIfRush(anOrder); aShipment.deliveryDate = deliveryDate(anOrder, isRush); ``` 那我对这个函数的签名没有任何意见(不过我还是想用分解条件表达式(260)清理其内部实现)。 可能有一些调用者给这个参数传入的是字面量,将其作为标记参数使用;另一些调用者则传入正常的数据。若果真如此,我还是会使用移除标记参数(314),但不修改传入正常数据的调用者,重构结束时也不删除`deliveryDate`函数。这样我就提供了两套接口,分别支持不同的用途。 直接拆分条件逻辑是实施本重构的好方法,但只有当“根据参数值做分发”的逻辑发生在函数最外层(或者可以比较容易地将其重构至函数最外层)的时候,这一招才好用。函数内部也有可能以一种更纠结的方式使用标记参数,例如下面这个版本的`deliveryDate`函数: ``` function deliveryDate(anOrder, isRush) {  let result;  let deliveryTime;  if (anOrder.deliveryState === "MA" || anOrder.deliveryState === "CT")   deliveryTime = isRush? 1 : 2;  else if (anOrder.deliveryState === "NY" || anOrder.deliveryState === "NH") {   deliveryTime = 2;   if (anOrder.deliveryState === "NH" && !isRush)    deliveryTime = 3;  }  else if (isRush)   deliveryTime = 3;  else if (anOrder.deliveryState === "ME")   deliveryTime = 3;  else   deliveryTime = 4;  result = anOrder.placedOn.plusDays(2 + deliveryTime);  if (isRush) result = result.minusDays(1);  return result; } ``` 这种情况下,想把围绕`isRush`的分发逻辑剥离到顶层,需要的工作量可能会很大。所以我选择退而求其次,在`deliveryDate`之上添加两个函数: ``` function rushDeliveryDate (anOrder) {return deliveryDate(anOrder, true);} function regularDeliveryDate(anOrder) {return deliveryDate(anOrder, false);} ``` 本质上,这两个包装函数分别代表了`deliveryDate`函数一部分的使用方式。不过它们并非从原函数中拆分而来,而是用代码文本强行定义的。 随后,我同样可以逐一替换原函数的调用者,就跟前面分解条件表达式之后的处理一样。如果没有任何一个调用者向`isRush`参数传入正常的数据,我最后会限制原函数的可见性,或是将其改名(例如改为`deliveryDateHelperOnly`),让人一见即知不应直接使用这个函数。