代码简洁之道

122 阅读13分钟
1、引言

   本期分享的背景:
   
      在一开始,我们都会想过要设计并写出来一段十分优美的代码来应对自己的需求,完美的解决问题,在一开始的时候,我觉得大家都是会这么想的,但是现实往往不尽人意,面对需求的变更、优化以及不断的迭代,你会慢慢的发现以往的代码不再能够适应新的变更和优化,你需要去不断的改进你的代码,可能是添加新的功能,可能是删除旧有逻辑,随着时间的增长,你的代码不断地堆积,如果开发时间短暂且需求紧急的情况下,代码的稳健性面临着考验,这个时候你可能会发现自己的代码存在着一些问题,比如说代码重复率高,代码可读性差,代码质量低,可维护性差,层次不清晰。

    本期分享的目的:
    
        写出高质量代码
        
        可读性高
        可维护性好
        无冗余和重复
        良好的命名规范
        可扩展性
        注重异常处理
        符合编码规范
        合适的设计模式
        
    根据我自己写代码遇到的一些问题和阅读书籍的总结,写了两个模块来实现以上的几点,一个模块代码书写的建议,一个模块是代码的重构.
    
    代码书写的建议可以帮助让你在写代码的时候少给自己挖坑,让你的代码看起来更加的清爽.
    
    重构则是在不改变代码外在行为或功能的前提下,对代码做出修改,以改进程序的内部结构,减少潜在的错误,提高其质量和可维护性.

2、 代码书写建议
    
    a、方法要单一职责:每个方法应该只做一件事情,做好一件事情。
       一个方法(或函数)应该有一个清晰而有限的任务,只专注于完成这个任务而不涉及其他不相关的职责
       
       这样能够让我们阅读这段代码时就能更快地理解它的功能,这对于团队协作和代码维护来说都非常有利的。
       
       当需要修改某个功能时,你只需关注与这个功能相关的方法,而不必担心其他不相关的部分。
       
       当方法负责多个不相关的任务时,它们之间可能存在不必要的耦合。
       
       单一职责原则有助于减少耦合,提高代码的模块化程度。
            
            示例:
            // 不遵循单一职责原则的例子
            function processDataAndRenderUI(data) {
              // 处理数据
              // 渲染用户界面
            }

            // 遵循单一职责原则的例子
            function processData(data) {
              // 处理数据
              return processedData;
            }

            function renderUI(data) {
              // 渲染用户界面
            }

            // 使用
            const processedData = processData(rawData);
            renderUI(processedData);
            
    上述代码中processDataAndRenderUI函数并不遵守单一职责,它做了两件事情,处理数据和渲染用户界面,我们可以将它拆分为processData和renderUI两个函数,一个用于处理数据,一个用户渲染用户界面,这样区分任务能够让每一个函数都有一个清晰的职责,提高代码的可维护性和可读性。

    b、固定参数要写到配置中去
    
       在开发中有些需求我们可能需要用到一些固定的参数,将固定参数集中存储在配置中,使得它们更容易管理和维护.在需要修改这些参数时,只需要更新配置文件,避免需要修改复数的文件.
    
       灵活性和可配置性:将固定参数写入配置文件中使得这些参数变得更加灵活,可以在不修改代码的情况下进行更改。这样的设计使得应用更容易适应不同的环境或需求变化。
       
       维护性:将固定参数集中存储在配置中,使得它们更容易管理和维护。在需要修改这些参数时,只需要更新配置文件。
       
       分离关注点:将固定参数从代码中抽离出来,有助于分离关注点。代码负责实现逻辑,而配置负责存储数据。
       不同环境适配:在不同的环境中可能需要不同的配置,如开发环境、测试环境和生产环境。将固定参数写入配置文件中,可以根据不同的环境加载不同的配置,而不需要修改代码。
       
       动态性:将固定参数放入配置文件,使得这些参数可以在运行时动态加载。这样可以在不重新编译代码的情况下更改应用的行为。
            
    c、每个方法写出1.2.3.4的逻辑步骤解析。
    
       在编写方法时,建议遵循一种清晰、有序的逻辑结构,使得方法的功能容易理解、调试和维护。这样的逻辑步骤解析有助于提高代码的可读性、可维护性和可测试性。
       
       **准备阶段 (Setup):**

          -   初始化变量和数据结构。
          -   设置初始状态。
          -   执行一些必要的前置操作。

       **处理阶段 (Processing):**

          -   执行主要的业务逻辑。
          -   对数据进行处理、转换或计算。
          -   实现方法的核心功能。

       **结束阶段 (Finalization):**

          -   完成主要处理步骤后的收尾工作。
          -   清理资源或执行必要的清理操作。
          -   准备返回值或最终结果。

       **返回阶段 (Return):**

          -   返回最终的结果或值。
          -   结束方法的执行。
        
            示例:
            function processUserData(name, age) {
              // 1. 准备阶段
              let formattedName = formatName(name);

              // 2. 处理阶段
              let ageGroup = determineAgeGroup(age);

              // 3. 结束阶段
              displayResult(formattedName, ageGroup);

              // 4. 返回阶段(这里可以没有返回值,或者返回一些状态信息)
            }

            function formatName(name) {
              // 实现格式化名字的逻辑
              return formattedName;
            }

            function determineAgeGroup(age) {
              // 实现判断年龄组的逻辑
              return ageGroup;
            }

            function displayResult(name, ageGroup) {
              // 实现展示结果的逻辑
            }

       d、注意命名,见名知义,不要用简写,使用一致的命名约定
       
          变量、函数和类的命名应该反映其用途和功能。通过读取变量名或函数名,其他开发者应该能够理解其含义,而不必查看其具体实现。使用清晰的、描述性的命名有助于代码的可读性。
            
       e、方法内要用trycatch确保异常情况下程序可以往下执行.
       
          使用 try-catch 块是一种良好的实践,可以确保在方法执行过程中遇到异常情况时,程序可以优雅地处理异常而不导致整个应用崩溃.
          
          但是也要注意不能够过度的使用trycatch,过度使用嵌套 `try-catch` 可能会使代码变得复杂难懂.
          
          合理使用try-catch:只在需要捕获异常的地方使用 `try-catch`,而不是在整个方法体都包裹一层。将 `try-catch` 限制在真正可能发生异常的区域。
          function example() {
              try {
                // 有可能抛出异常的代码块
              } catch (error) {
                // 异常处理逻辑
              }
            }
            
          层次化处理:如果有多层嵌套,可以在更高层次捕获异常,而在更低层次只处理必要的异常。这样可以让代码更清晰.
          function outerFunction() {
              try {
                // 可能抛出异常的代码块
                innerFunction();
              } catch (error) {
                // 外层异常处理逻辑
              }
            }

          function innerFunction() {
              try {
                // 可能抛出异常的代码块
              } catch (error) {
                // 内层异常处理逻辑
              }
            }
            
       错误传递和处理:在捕获异常后,可以选择继续抛出或者处理异常。有时候,将异常传递给上一层,以便在更高层次处理,可能更合适.
       function example() {
          try {
            // 有可能抛出异常的代码块
          } catch (error) {
            // 处理异常
            throw error; // 或者做其他处理
          }
        }
        
      统一的异常处理层:在应用的某个层次,可以设置一个统一的异常处理层,负责捕获和处理所有未被处理的异常。这样可以集中管理异常处理逻辑.
      function errorHandler(error) {
          // 统一的异常处理逻辑
        }

      function example() {
          try {
            // 有可能抛出异常的代码块
          } catch (error) {
            errorHandler(error);
          }
        }
        
   f、注意做好空判断,不要出现undefined of property,确保在访问对象属性之前,首先检查对象是否存在,并逐级检查嵌套属性,有助于提高代码的健壮性,防止因为未定义的属性而导致程序错误.
   
      以下是一些处理空判断的方法:
      
      使用条件语句进行判断
         if (obj && obj.property) {
             // 执行操作,确保 obj 和 obj.property 都不为 undefined 或 null
          } else {
             // 处理未定义的情况
          }
          
      使用短路运算符 `&&`
         const value = obj && obj.property && obj.property.value;
         if (value !== undefined && value !== null) {
            // 执行操作,确保所有属性都存在且不为 undefined 或 null
          } else {
            // 处理未定义的情况
          }
         
       使用 Optional Chaining(可选链式调用)
          const value = obj?.property?.value;
          if (value !== undefined && value !== null) {
             // 执行操作,确保所有属性都存在且不为 undefined 或 null
          } else {
             // 处理未定义的情况
          }
          
       使用默认值和空合并运算符 `??`
          const value = obj && obj.property && obj.property.value ?? defaultValue;
          // 使用 defaultValue 处理未定义的情况
          
       这行代码使用了逻辑与 (`&&`) 和空合并运算符 (`??`),目的是从一个对象 (`obj`) 中获取嵌套属性 (`property.value`) 的值,如果任何一级属性不存在或者其值为 `null``undefined`,则使用默认值 (`defaultValue`)。
       
       `?? defaultValue`:这是空合并运算符,如果左侧表达式的结果为 `null``undefined`,则使用右侧的默认值 `defaultValue`3、 重构

    a、什么是重构
    
    一般在开发一个需求之前,首先得有一个良好的设计,然后才能开始编码,而重构就是「在代码写好之后改进它的设计」.
    
    示例:
    function calculateTotalPrice(items) {
      let total = 0;
      for (let i = 0; i < items.length; i++) {
        total += items[i].price * items[i].quantity;
      }
      return total;
    }
    这个函数计算购物车中商品的总价,但它可能存在一些问题:
    可读性差: 随着计算逻辑的复杂化,这个函数的可读性可能下降。
    扩展性差: 如果需要添加折扣或税收等功能,修改该函数可能变得困难。
    
    function calculateTotalPrice(items) {
      return items.reduce((total, item) => total + calculateItemPrice(item), 0);
    }

    function calculateItemPrice(item) {
      return item.price * item.quantity;
    }
    在这个重构后的版本中,我们将计算单个商品价格的逻辑抽离成了一个独立的函数 `calculateItemPrice`。这样的重构带来了以下好处:
    可读性提高: 独立的函数使得代码更加模块化和易于理解。
    扩展性提高: 现在如果需要修改商品价格计算的逻辑,只需要修改 calculateItemPrice 函数,而不需要修改主函数。
    
    b、在重构之前
    
    需要明确一点重构是一个渐进的过程,可以在开发的同时逐步进行,确保代码始终保持可维护和可理解的状态,如果代码和设计在最初就非常的优质,后期的重构维护会十分的轻松。
    
    重构具有风险。它必须修改运作中的程序,这可能引入一些幽微的错误,做好副本之类的保守操作.
    
    c、怎么去重构
    
    这里针对几个点来说明
    
        -  重复的代码
            如果说你在一个以上的地点看到相同的程序结构,那么可以肯定,设法将它们合而为一,程序会变得更好。
           示例:
                // 邮箱验证函数
                function validateEmailFormat(email) {
                  // 正则表达式验证电子邮件格式
                  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                  return emailRegex.test(email);
                }

                // 电话号码验证函数
                function validatePhoneNumberFormat(phoneNumber) {
                  // 正则表达式验证电话号码格式
                  const phoneRegex = /^\d{10}$/;
                  return phoneRegex.test(phoneNumber);
                }
            
            优化:
                // 共享的逻辑:正则表达式验证
                function validateFormat(value, regex) {
                  return regex.test(value);
                }

                // 邮箱验证函数
                function validateEmailFormat(email) {
                  // 正则表达式验证电子邮件格式
                  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                  return validateFormat(email, emailRegex);
                }

                // 电话号码验证函数
                function validatePhoneNumberFormat(phoneNumber) {
                  // 正则表达式验证电话号码格式
                  const phoneRegex = /^\d{10}$/;
                  return validateFormat(phoneNumber, phoneRegex);
                }
                
      -  过长函数
          程序愈长愈难理解。
          如果函数内有大量的参数和临时变量,它们会对你的函数提炼形成阻碍。
          
          示例:
              这里只展示了修改后的函数
              function processShoppingCart(cart, user) {
                  const products = getProducts(cart);
                  const total = calculateTotal(products);
                  const discountedTotal = applyDiscount(total, user);
                  const isValidUser = validateUser(user);

                  if (isValidUser) {
                    const order = generateOrder(products, discountedTotal, user);
                    saveOrderToDatabase(order);
                    sendOrderConfirmationEmail(user, order);
                  } else {
                    // 验证用户信息失败逻辑
                  }
                }

                function getProducts(cart) {
                  // 获取购物车中商品的具体逻辑
                }

                function calculateTotal(products) {
                  // 计算商品总价的具体逻辑
                }

                function applyDiscount(total, user) {
                  // 应用折扣的具体逻辑
                }

                function validateUser(user) {
                  // 验证用户信息的具体逻辑
                }

                function generateOrder(products, total, user) {
                  // 生成订单的具体逻辑
                }

                function saveOrderToDatabase(order) {
                  // 将订单保存到数据库的具体逻辑
                }

                function sendOrderConfirmationEmail(user, order) {
                  // 发送订单确认邮件的具体逻辑
                }
                
       -  过长参数列
           
           指的是函数或方法的参数列表过于庞大,包含了过多的参数。
           
           一个过长的参数列可能会使函数的调用和理解变得困难,同时也增加了维护和测试的难度。
           
           示例:
               function processOrder(productName, quantity, price, discount, shippingAddress, billingAddress, paymentMethod, isGift, isExpressShipping) {
                  // 处理订单的逻辑
                }
           在这个例子中,`processOrder` 函数有着较长的参数列表,包含了订单处理所需的各种信息。这样的设计可能使得调用函数时的代码变得复杂,也可能导致容易混淆参数的顺序,难以维护。
           
           我们将相关的参数组织成一个对象,然后将该对象作为单一的参数传递给函数。
           
               function processOrder(orderDetails) {
                  // 处理订单的逻辑
                }
           
           思考一下,如果函数的参数过多,可能表示函数承担了过多的职责,例如过长函数的例子。
           考虑将函数分解为多个小函数,每个函数专注于一个特定的子任务,这样每个函数的参数列表就能更合理。

    -  数据泥团
        
        指代码中多个地方重复使用相同的数据集合,这可能表明这些数据应该被封装成一个独立的数据结构或对象
                
        示例:
      
            function orderUserInfo() {
                const username = 'JohnDoe';
                const email = 'johndoe@example.com';
                const userId = 123456;

                // 显示用户信息
                console.log(`Username: ${username}, Email: ${email}, UserID: ${userId}`);
            }

            function shopCarUserInfo() {
                const username = 'JohnDoe';
                const email = 'johndoe@example.com';
                const userId = 123456;

                // 显示用户信息
                console.log(`Username: ${username}, Email: ${email}, UserID: ${userId}`);
            }
            
            为了优化这个数据泥团,可以将用户信息封装成一个对象或数据结构,然后在需要的地方引用该对象
            const userInfo = {
                username: 'JohnDoe',
                email: 'johndoe@example.com',
                userId: 123456
            };

            function displayUserInfo(userInfo) {
                // 显示用户信息
                console.log(`Username: ${userInfo.username}, Email: ${userInfo.email}, UserID: ${userInfo.userId}`);
            }

            // 使用优化后的方式显示用户信息
            displayUserInfo(userInfo);


           
   -  基本型别偏执
   
       指在编程中过度依赖基本数据类型(例如整数、字符串、数组等),而不是使用自定义的对象来表示领域概念。这可能导致代码中充斥着原始数据类型,而不是更具表达性和安全性的自定义类型。
       
       示例:
       
           一个在线商店的购物车管理系统,使用了基本型别
           
           let cartItemNames = ["Laptop", "Headphones", "Mouse"];
           let cartItemPrices = [999.99, 149.99, 29.99];
           let cartItemQuantities = [1, 2, 3];

            function calculateTotal() {
              let total = 0;
              for (let i = 0; i < cartItemNames.length; i++) {
                total += cartItemPrices[i] * cartItemQuantities[i];
              }
              return total;
            }

            function displayCart() {
              for (let i = 0; i < cartItemNames.length; i++) {
                console.log(`${cartItemNames[i]} - $${cartItemPrices[i]} x ${cartItemQuantities[i]}`);
              }
              console.log(`Total: $${calculateTotal()}`);
            }

            // 显示购物车内容
            displayCart();

           
           let cartItems = [
              { name: "Laptop", price: 999.99, quantity: 1 },
              { name: "Headphones", price: 149.99, quantity: 2 },
              { name: "Mouse", price: 29.99, quantity: 3 }
            ];

            function calculateTotal() {
              return cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
            }

            function displayCart() {
              for (let item of cartItems) {
                console.log(`${item.name} - $${item.price} x ${item.quantity}`);
              }
              console.log(`Total: $${calculateTotal()}`);
            }

            // 显示购物车内容
            displayCart();
            
           重构后每个商品都是一个包含名称、价格和数量属性的对象,而这些对象组成了 `cartItems` 数组。这样做的好处是可以更容易地添加、删除或修改商品属性,而不必修改多个数组。

   -  冗赘函数
   
       指的是在代码中存在的、不必要的、多余的函数。这些函数可能由于代码重构、修改或其他原因而失去了作用,但仍然存在于代码中。
       
       假设原本有一个处理支付的函数,后来,由于业务需求变更,原有的支付逻辑不再适用。可能会创建一个新的函数,而原有的 `processPayment` 函数却没有被删除或更新。
       
           function processPaymentV2(amount) {
              // 新的支付逻辑
              console.log(`支付成功:${amount}元`);
            }

            // 冗赘函数,因为新逻辑中并没有调用
            function processPayment(amount) {
              console.log('这个函数已经不再使用');
            }
            
      虽然它曾经是网站支付的关键函数,但由于升级,它变得多余。在这种情况下,最好的做法是删除或注释掉冗赘的函数,以避免引起混淆,并确保未来代码维护时不会误用这个已经废弃的函数。

   -  令人迷惑的暂时值域
   
       有时你会看到这样的对象:其内某个变量仅为某种特定情势而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有变量。在变量未被使用的情况下猜测当初其设置目的。
       
       示例:
       假设有一个购物车对象,其中存在只在特殊情况下使用的变量
           const shoppingCart = {
              items: [],
              discount: 0,
              isDiscountApplied: false,
              specialDiscount: 0.2, // 仅在特殊情况下使用的变量
            };

            function addItem(item) {
              shoppingCart.items.push(item);
            }

            function applyDiscount() {
              shoppingCart.discount = shoppingCart.isDiscountApplied ? shoppingCart.specialDiscount : 0;
            }

            function calculateTotal() {
              let total = 0;
              for (const item of shoppingCart.items) {
                total += item.price;
              }

              // 应用折扣
              total *= 1 - shoppingCart.discount;

              return total;
            }

            // 特殊情况下应用折扣
            shoppingCart.isDiscountApplied = true;
            applyDiscount();

            // 显示购物车内容
            console.log("Shopping Cart:");
            for (const item of shoppingCart.items) {
              console.log(`${item.name} - $${item.price}`);
            }
            console.log(`Total Price: $${calculateTotal()}`);
        
            这段代码中,`specialDiscount` 是一个只在特殊情况下使用的变量,但它始终存在于对象中。为了优化它,我们可以将这个变量提取到函数内部,只在需要的时候创建。
            
           const shoppingCart = {
              items: [],
              discount: 0,
              isDiscountApplied: false,
            };

            function addItem(item) {
              shoppingCart.items.push(item);
            }

            function getSpecialDiscount() {
              const specialDiscount = 0.2
              return shoppingCart.isDiscountApplied ? specialDiscount : 0;
            }

            function applyDiscount() {
              shoppingCart.discount = getSpecialDiscount();
            }

            function calculateTotal() {
              let total = 0;
              for (const item of shoppingCart.items) {
                total += item.price;
              }

              // 应用折扣
              total *= 1 - shoppingCart.discount;

              return total;
            }

            // 特殊情况下应用折扣
            shoppingCart.isDiscountApplied = true;
            applyDiscount();

            // 显示购物车内容
            console.log("Shopping Cart:");
            for (const item of shoppingCart.items) {
              console.log(`${item.name} - $${item.price}`);
            }
            console.log(`Total Price: $${calculateTotal()}`);
            
        通过这种方式,`specialDiscount` 不再是对象的一部分,而是在需要时通过 `getSpecialDiscount` 函数获得。这样能够避免了在对象中存在不必要的变量
       
d、什么时候才是重构的好时机

   这里我罗列了几个情况,大家可以看看:
   
       代码可读性差:当代码难以理解的时候,或者需要大量的注释才能够解释。
       
       代码复用性差:如果有大量的代码重复,或者说有很多类似的代码块在不同的地方被使用。
       
       代码维护困难:如果修改一个部分的代码会导致其他部分的代码出现问题,或者需要花费大量的时间去修复bug,那么这可能表明代码的结构需要优化。
       
       新需求的引入:当新的需求被引入,而现有的代码结构无法很好地适应这些需求时,这可能是一个需要进行代码重构的时机。
       
       前期快速迭代:在项目的初期,可能会为了快速迭代而牺牲代码的质量。当项目进入更为稳定的阶段时,这就是一个进行代码重构的好时机。
       
   需要强调的是,在任何情况下,重构都应该是一个持续的过程,而不仅仅是一个一次性的任务。
   
   需要注意的是,我们对既有代码并不添加新功能,而是对现有代码进行改进。改进之后,应该有充足的测试,以确保重构不会破坏现有的功能。
 
   备份和回退策略:在进行重构之前,确保代码有适当的版本控制,并制定好回退策略,以便在必要时能够快速回滚到之前的稳定状态。

f、何时不该重构?
    
    如果项目已经非常接近最后期限,你不应该再分心于重构,因为己经没有时间了。
    
    重构的确能够提高生产力。如果最后你没有足够时间,通常就表示你其实早该进行重构。
    

最后分享一个方法:STAR 它可以让你理清楚自己的情况 情境(Situation):你所面临的具体情境或场景 任务(Task):你在那个情境中具体负责或需要完成的任务 行动(Action):你采取了哪些具体行动来完成任务 结果(Result):你的行动导致了什么结果