Skip to content

线性代码可读性更好 ?

最近谷歌出了一文章 使用抽象来提高功能可读性,讲述的是如何将一个函数拆分成多个函数,以提高代码的可读性。

然后出来另一篇文章,线性代码可读性更好,和上面的文章争锋相对,认为线性代码可读性更好。

刚好我也遇到了类似的例子,所以我想讨论下在使用 composition-api 的情况下如何组织代码,能兼顾代码的可读性与可维护性。

例子

需求是想创建一个pizza,分烘烤 bake 和 装盒 box 两个步骤。如果按照线性的代码,就是这样:

js
function createPizza() {
  let pizza = ''

  // 烘烤
  pizza += ...

  // 装盒
  pizza += ...

  return pizza
}

如果封装出来就是这样:

js
function createPizza() {
  let pizza = ''

  pizza = bake(pizza)
  pizza = box(pizza)

  return pizza
}

function bake(pizza) {
  pizza += ...
  return pizza
}

function box(pizza) {
  pizza += ...
  return pizza
}

如果 bake :pizza += 'bake' box :pizza += 'box',那么这样的封装就是在浪费时间。

实际的业务复杂度与代码复杂度要多的多的多;所以抛开复杂度谈封装还是不可取的。所以我们需要讨论下什么情况下需要封装

代码封装的基本原则

我问了下GPT 它给出的答案还是蛮值得参考的:

代码封装的原则是将一组相关的数据和操作(或功能)组合成一个单元,以隐藏其内部实现细节,并提供一个清晰的接口供其他部分的代码使用。封装的目标是将复杂性隔离,提高代码的可维护性、可重用性和可理解性。以下是一些关于何时封装代码的一般原则:

  1. 信息隐藏:封装的核心原则是隐藏内部细节,只暴露必要的接口。这有助于降低代码的复杂性,减少了对代码内部实现的依赖,同时提供了更高层次的抽象。
  2. 单一职责原则(SRP):一个类或函数应该有一个明确的责任或功能,而不应该承担过多的任务。当一个类或函数变得过于庞大或复杂时,就应该考虑将其拆分成更小的部分,并通过封装这些部分来保持代码的清晰性。
  3. 接口分离原则(ISP):接口应该是小而专注的,而不应该包含不相关的方法。封装时,确保接口只包含与其功能相关的方法,以防止冗余和不必要的依赖。
  4. 开放-封闭原则(OCP):代码应该对扩展开放,对修改封闭。这意味着在添加新功能时,不应该修改现有的代码,而是通过扩展已有的代码或创建新的模块来实现新功能。
  5. 抽象原则:通过使用抽象类、接口或基类来定义通用的接口,以便不同的实现可以互相替换,这有助于实现多态性和代码的可扩展性。
  6. 封装变化:将可能发生变化的部分封装起来。如果您预计某些部分的实现可能会发生变化,封装这些部分可以减少对整个代码的影响,提高代码的稳定性。
  7. 保持一致性:在代码中保持一致的封装风格和命名约定,以使代码更容易理解和维护。

需要封装的代码或逻辑通常包括以下情况:

  • 数据访问:将数据访问逻辑封装到数据访问层,以隐藏数据库或文件系统的细节。
  • 复杂业务逻辑:将复杂的业务规则和计算封装到独立的业务逻辑组件中,以提高代码的可读性和可维护性。
  • 第三方依赖:对于与外部库、服务或API的交互,封装这些交互可以降低对外部变化的敏感性,并提供更好的错误处理。
  • 用户界面:将用户界面的逻辑封装到可重用的组件中,以便在不同部分的应用程序中共享。

但我说一件事情,就是封装其实是一种取舍;它是否真正的带来了遍历的收益,还是只是为了封装而封装,这是需要考虑的。比如,对于第三方库的封装,如果你的项目只是用了一两个方法,那么封装的收益就不大;但如果你的项目用了很多方法,那么封装的收益就很大。

还有就是封装会带来负面收益,就是心智负担,如果你是开发者你可以好理解一些,但对于未知的用户,那可能就是不可必要的学习成本了。而且过度的封装可能会带来负面的效应。

过度封装

过度封装是一个常见的问题,它可能导致代码复杂性的增加,降低了代码的可读性和可维护性。以下是一些常见的导致过度封装的场景和情况:

  1. 无意义的中间层:有时,开发人员可能会引入不必要的中间层或抽象,这些中间层没有实际的功能,只是增加了代码的复杂性。例如,当一个简单的数据访问操作被封装成多个无谓的方法或类时。
  2. 过多的接口:定义过多的接口或抽象类可能会使代码难以理解,特别是当这些接口之间存在复杂的继承和实现关系时。
  3. 细粒度的方法:将一个简单的操作拆分成多个极其细小的方法可能会导致方法数量的激增,使代码难以管理和理解。
  4. 合理的配置参数:过度封装还可能表现为大量不必要的配置参数。这些参数使得使用代码变得繁琐,而且可能会导致错误配置的问题。
  5. 过多的层次嵌套:如果嵌套层次太深,代码可能会变得难以跟踪和调试。这种情况通常发生在递归或回调等情况下。
  6. 过多的注释:虽然注释在代码中很重要,但过度注释可能是对过度封装的一种迹象。如果每一行代码都伴随着详尽的注释,可能意味着代码本身不够清晰。

要避免过度封装,开发人员应该保持代码的简洁性和可读性。在决定是否引入新的抽象层时,需要权衡代码的实际需求和复杂性。好的实践包括:

  • 仅在有实际需要时引入抽象。
  • 保持代码的单一职责,确保每个组件或方法都有明确的目的。
  • 合理而明确的出入参,避免不必要的配置参数。
  • 适量的可扩展性,但避免过度设计。

这里我还想说一点,就是现在开源的东西已经有很多了,你封装的不一定比别人好,那为什么不用别人和一起维护呢;对于一些开源的东西,你可以在上面提 issue,或者提 pr,这样对于你的学习也是有帮助的。

能不卷咱就不要卷了,写了一堆代码,最后发现别人已经写好了,那就是在浪费时间。

命名

当使用抽象时,确保使用清晰、一致的命名约定。比如方法名代表的是你这个功能的意义。以及可能会返回的结果,入参代表的是的我的输入。

样例中的 bakebox 就是很好的命名,它们代表的是这个功能的意义。但还是相对简单的业务。如果是复杂的业务,那么命名就是一个很大的学问了。

这里就没有什么规范,还是用一些翻译工具会比较好,我个人建议方法名变量名可以复杂一点,让人一看能够知道是什么功能。

抽象 or not 抽象

拿一个实际业务的代码出发吧,我们在使用 hmap的时候一般会使用到,创建图层,创建点,设置点样式等操作;我那创建图层来说。

所以我最终的代码是

js
export const useDrawAirPlots = (mapApi) => {
  let plotLayer = null
  const generateLayerOnMap = () => {
    // generateLayer 1.方法一开始我写在这
    if (!plotLayer) {
      mapApi.addLayer(plotLayer = generateLayer(plotLayer, '-航点编辑图层', {
        isRTE: true,
        enableHashCode: true,
      }))
    }
  }
  ... // 其他方法
  return {
    generateLayerOnMap,
  }
}

// 2 后来我写在这,也就是 composables 中,最后有多个方法都需要用到,所以我把它抽离到 utils/map 中了
const generateLayer = (layer: any, layerName: string, options?: any) => {
  if (layer && layer.id) layer.destroy()
  layer = new hmap.layer.VectorLayer(layerName, options)
  return layer
}

那么其实封装是根据不同业务情况来看的,如果你的业务只是用到一次,那么封装的收益就不大;但如果你的业务用到多次,那么封装的收益就很大。所以封装的话也是动态的。之前我的文献中也提到了这块:使用 composition-api 的一个误区

one more thing

但更重要的是随着业务的发展,更加可怕的是业务的复杂度增加,那么势必会导致代码的复杂度增加;一直在考虑加法的同时,如何正确的做减法,可能还是最有学问的课题。

总结

所以是否封装,封装成如何都是一种选择;就像有些人喜欢干净,房间打扫了很干净;有些人很邋遢基本不收拾房子。但最终上班是无法看得出2个人的差距的。也并不会对产品有着最终的影响。但对于 2/8 原则来说,只有 2 才是体现人与人之间细微的差别。

May the shit mountain code not be with you 👻👻👻