V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ScottHU
V2EX  ›  程序员

封装的三重境界,理解你就是骨灰级程序员了

  •  
  •   ScottHU · 18 天前 · 1399 次点击

    你好,我是 alova 的作者胡镇。alova 是一个请求策略库,一行代码解决复杂的请求问题,访问官网

    今天就想分享点干货出来。

    封装一直是一个老生常谈的话题了,什么程序员都绕不开,也是衡量一个程序员水平高低的重要标准。

    多年的开发经历告诉我,这是一种对世界认知的体现,而不仅仅只是复用性的问题,理解你就是骨灰级程序员啦。

    让我们先来看几个例子,鉴于前端同学较多,我就以前端举例吧。

    常见封装问题来一波

    我们以几个简单的示例来开始吧,以下几个封装例子都不够好,你来看看存在哪些可以改进的地方吗?

    示例 1

    假设我们有一个业务场景,需要展示一个用户信息卡片,卡片上包含用户的姓名、年龄和头像。同时,根据用户的年龄,卡片上会显示不同的背景颜色,来看一个 vue 组件。

    <template>  
      <div :class="['user-card', ageClass]">  
        <img :src="user.avatar" alt="User Avatar" />  
        <h2>{{ user.name }}</h2>  
        <p>Age: {{ user.age }}</p>  
      </div>  
    </template>  
    
    <script>  
    export default {  
      props: {  
        user: {  
          type: Object,  
          required: true 
        }  
      },  
      computed: {  
        ageClass() {  
          if (this.user.age < 18) {  
            return 'teenager';  
          } else if (this.user.age < 60) {  
            return 'adult';  
          } else {  
            return 'elderly';  
          }  
        }  
      }  
    };  
    </script>  
      
    <style scoped>  
    .user-card {  
      padding: 20px;  
      border-radius: 10px;  
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);  
    }  
      
    .teenager {  
      background-color: lightblue;  
    }  
      
    .adult {  
      background-color: lightgreen;  
    }  
      
    .elderly {  
      background-color: lightpink;  
    }  
    </style>
    

    示例 1 分析

    这个组件存在两个问题:

    问题 1: 内部使用了计算属性ageClass,来根据用户的年龄返回不同的背景颜色类名。这是一种将业务逻辑(根据年龄设置背景颜色)和展示逻辑混合在了一起,初一看其实没啥问题啊,但这里我想说的是,一切设计都是跟着需求走的,如果其他场景需要展示类似的卡片但不需要根据年龄设置背景颜色,那么这个组件就无法直接复用。当然,也有可能你们产品就说了,我们产品里就这三种情况,你当然可以按上面这样写,但谁又能保证过几天你们产品改变主意,或者换人了呢?

    在实际项目中,我们通常会尽量将业务逻辑和展示逻辑分离,以提高代码的复用性和可维护性。

    问题 2: 用户参数是以 object 的形式传递的,这一般是某个大哥图方便,直接把接口数据往里扔造成的,实际情况是业务中大多开发人员都不喜欢写文档,对团队其他人来说是难以理解的,他们需要去翻看组件源码,到底包含了啥东西,很痛苦,这里只是举了个简单的nameage的例子,实际情况组件更复杂,参数更多。

    示例 2

    我们有一个函数,它旨在验证用户的输入计算一些值,并更新 UI 。

    function handleUserInputAndUpdateUI(inputString, uiElement) {  
      // 验证输入字符串 
      if (typeof inputString !== 'string' || inputString.trim() === '') {  
        console.error('Invalid input: Input must be a non-empty string.');  
        return;  
      }  
      
      // 计算输入字符串的长度
      const inputLength = inputString.length;  
      console.log('Input length:', inputLength);  
      
      // 根据输入字符串的长度更新 UI
      if (uiElement) {  
        uiElement.innerText = `Input length: ${inputLength}`;  
      } else {  
        console.error('UI element is not provided.');  
      }
    }
    

    示例 2 分析

    这个函数的问题是,混合了多个不相关的职责,导致它难以理解,如果你只是为你的某一处表单验证编写这个函数当然没问题,但我们这边说的是封装,也可以理解为粒度太大,下次我们有一个地方也要用到验证输入字符串功能,我们是复制好还是复制好呢?

    更好的做法是将这些职责分解为单独的函数,每个函数只负责一个单一职责。这样,代码将更容易阅读、测试和维护。

    但上面这段验证输入字符串代码即使单独拆出来还是存在封装的问题,请问你知道是什么吗?评论区说说。

    示例 3

    你有一个组件 css 是这样的。

    .my-component {  
      border: solid 1px #ff3478;  
    
      .my-component__button {
        color: #ff3478;  
        border-bottom: solid 1px #ff3478; 
          
        &:hover {
          background-color: #ff3478;  
        }  
      }  
        
      .my-component__title {  
        fontsize: 1.5rem;
        color: #ff3478;  
      }  
    }
    

    示例 3 分析

    大家能看出上面的 css 有什么问题吗?

    答案是多处使用了#ff3478这个颜色,但都是硬编码上去的,各位有多少人是这样写过,请点个赞。

    好的办法当然是统一到一个 css 变量里,然后统一引用,这其实是一种小封装,只封装并复用了一个值而不是一个模块,其他例子还有多处操作缓存:

    // constant.js
    const USER_INFO_STORAGE_KEY = 'user_info';
    
    // login.js
    localStorage.setItem(USER_INFO_STORAGE_KEY, JSON.stringify(userInfo));
    
    // user.js
    const userInfo = JSON.parse(localStorage.getItem(USER_INFO_STORAGE_KEY));
    

    示例 4

    🌝🌝🌝

    1    <template>
    ...
    ...
    ...
    1253 <template>
    1254 <script>
    ...
    ...
    ...
    3549 </script>
    3550 <style scoped lang="scss">
    ...
    ...
    ...
    5263 </style>
    

    示例 4 分析

    一个复杂的组件里密密麻麻写满了代码,就不能分开来写嘛...

    封装的三重境界

    复用性

    在上面的例子中,其实还是以复用性为主的封装,这是我们大多数人所理解的封装,但这只是第一重境界。

    职责划分

    这点在上面的示例 2 中有所体现,将一个粒度较大的封装分为职责单一的小块,这听上去怎么还是跟复用性有关,但在实际业务中,你可能会遇到通用组件的各种参数,如何设计这些参数才能保持这个组件的单一职责呢?这是封装的第二层理解。

    与机器形成默契

    这是对封装的第三层理解,这一层理解是属于认知层面的,我们通过代码与机器交流,本质上和人交流是想通的,当我们跟别人说苹果的时候,别人为什么可以很快就知道你指的是那个“圆圆的有点红红的水果”,而不是“黑黑的方块”呢?如果与机器达成这样的共识,是不是不用那么啰嗦它也能知道我们让它干什么呢?

    或者,创造一种东西作为连接人与机器的桥梁,桥梁的一边是开发者易于理解的指令,然后转换成机器可以理解的指令,这样可以更好地与机器形成默契。

    如果你对这些感兴趣,4 月 14 日(周天) 20:00 ,欢迎来我的直播间共同探讨这些,欢迎你的预约,纯分享啥也不卖

    image.png

    内容大纲

    在直播间我们一同来探讨以下的话题,如果你也感兴趣,诚挚邀请你来一起聊聊。

    live-fccb0040843cb560841c3c5e7a2990fa.jpg

    后序

    如果想了解 alova 的话,可以访问官网,在这里,你可以找到更详细的文档和示例代码,帮助你更好地理解和使用这个工具。

    有任何问题,你可以加入以下群聊咨询,也可以在github 仓库中发布 Discussions,如果遇到问题,也请在github 的 issues中提交,我们会在最快的时间解决。

    同时也欢迎贡献你的一份力量,请移步贡献指南

    2 条回复    2024-04-13 10:24:52 +08:00
    LanhuaMa
        1
    LanhuaMa  
       18 天前   ❤️ 1
    理你一下
    kneo
        2
    kneo  
       17 天前 via Android   ❤️ 1
    点开是为了加深点影响,下次不用再点开了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   868 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 22:17 · PVG 06:17 · LAX 15:17 · JFK 18:17
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.