前端编程之道7-1:做人要有原则,那写代码呢?

前端编程之道系列,欢迎关注,github.com/501351981/H...

写代码也要有原则?

在编码过程中,有时候可能会遇到一些比较纠结的情况,比如应不应该抽取组件,怎么抽取组件,颗粒度究竟要到什么程度,组件或者方法调用怎么传参更好;有时候在review别人代码时,感觉代码写的有问题,但是又说不出来到底哪里有问题,不知道如何评价一段代码是好或者不好,这时候你应该了解下常见的编程原则了。

所谓编程原则,就是前人们经过大量的实践和试错,为我们总结的一些特别有价值的基本准则、规范或原理,这些编程原则能够指导我们如何进行编码以及如何进行决策(取舍),能够用来评价一段代码的质量高低。

编程原则和我们做人的原则道理是一样的,你的种种行为只是做人原则的一种外化,而原则是行为的内在驱动力。就像一个人把如果诚信和道义作为自己的做人原则,那么他就不会去欺骗朋友换取自己的利益,而如果一个人以维护自己最大利益为做人原则,那么他可能就会为了晋升去伤害身边的人。而当面对利益还是道义的取舍时,我们通过做人原则来进行决策。 树立正确的编程原则就像树立正确的做人原则一样重要,正确的编程原则可以指导我们写出高质量的代码,错误的原则或者没有原则,则会让我们的代码陷入混乱。

当然,世界上没有绝对正确的原则,但是经过多年的实践,前人也给我们留下了一些相对正确的编程原则,值得我们去学习、实践,有了这些原则,就相当于有了思想上的武器,由内而外地驱动你写出高质量的代码,这些原则包括不限于:

  • 最少知识原则LOD:一个对象应该尽可能少地了解其他对象的内部结构和实现细节
  • DRY原则(Don't Repeat Yourself):避免重复代码,尽量使用函数、类或模块来封装可复用的代码片段
  • KISS原则(Keep It Simple, Stupid):保持代码简单易懂,避免过度复杂化,尽量使用简洁的解决方案
  • YAGNI原则(You Ain't Gonna Need It):不要过度设计
  • SOLID原则:这是一组面向对象设计的原则,包括单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)和依赖倒置原则(DIP),旨在提高代码的可维护性、可扩展性和可重用性。
  • 高内聚低耦合:模块内部的组件之间应该紧密相关,模块之间的依赖应该尽量减少,以提高代码的可维护性和可测试性。
  • 测试驱动开发(TDD):先编写测试用例,再编写代码来满足测试用例,以确保代码的正确性和可靠性。

最少知识原则LOD

最少知识原则是在1987年秋天由美国Northeastern University的Ian Holland提出,被UML的创始者之一Booch等普及。后来,因为在经典著作《 The Pragmatic Programmer》(程序员编程之道)而广为人知。

最少知识原则(The Least Knowledge Principle),也被称为迪米特法则(Law of Demeter),是一种面向对象设计原则,强调对象之间应该尽量减少彼此的依赖关系,即一个类对于其他类知道的越少越好,一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

在平时前端开发中,面向对象的业务代码可能写的不够多,这里我们可以将对象的概念平替为函数或者组件,比如一个组件应该只和自己的子组件进行直接通信,而不能试图调用孙子组件的data、state或者method,一旦你直接和孙子组件进行通信,说明你就要了解孙子组件的实现,这就增大了维护当前组件的知识量,本来维护一个组件只需要知道当前组件的内部设计和子组件的接口,现在又必须了解孙子组件的接口,无形之中增加了维护的困难。

这是我在微信朋友圈看到的一段真实代码:

javascript 复制代码
function setEditorContent(content){
    this.$refs['editor'].$refs['innerEditor'].setContent(content);
}

可以大概猜测出这段代码的意图,这是一个设置编辑器内容的方法,首先通过 <math xmlns="http://www.w3.org/1998/Math/MathML"> r e f s [ ′ e d i t o r ′ ] 获取到某个子组件实例,然后再继续调用 refs['editor']获取到某个子组件实例,然后再继续调用 </math>refs[′editor′]获取到某个子组件实例,然后再继续调用refs['innerEditor']来获取孙子组件实例,最后调用孙子组件的setContent方法来修改编辑器内容。

这段代码为以后的维护带来风险,为了维护当前这个组件,首先我们要知道子组件内有个ref为innerEditor的孙子组件,这个孙子组件有个setContent方法,这无疑增加了维护时的学习成本;其次如果子组件内部发生了调整,比如内部的ref不再叫innerEditor了,那这里就会出错了,有人可能说了,那子组件不改这里,不就没问题了?如果你现在维护一个组件,你会对什么内容负责?是不是肯定只对暴露的接口负责,也就是只要我保证组件对外提供的props、method、event保持不变,我们就可以认为我们的变更不会影响到别的组件,你肯定不会想到仅仅是修改了内部实现中的某个组件的ref名称,居然导致父组件出错了。如果父组件真的依赖子组件的实现,那你维护这个子组件的时候,是不是就要知道我的某个内部实现是不能变的,这无疑也增加了维护所需的知识量,试想如果团队来了一个新人,这个知识他怎么会知道呢,也就是说因为这种耦合的代码写法,导致维护一个组件的知识量大量上升。

正确的做法应该是子组件提供一个setContent方法,我们只要直接调用子组件方法即可,至于子组件内部怎么实现这个setContent,父组件并不关心。

javascript 复制代码
function setEditorContent(content){
    this.$refs['editor'].setContent(content);
}

更佳的做法是,我根本不需要知道子组件需要通过setContent方法修改其内容,我们只修改子组件的属性即可,子组件内部来通过监听value变化来设置编辑器内容,让父组件无感知。

vue 复制代码
<template>
    <Editor v-model="content"/>
</template>
<script>
export default {
    data(){
        return {
            content: '' 
        }
    },
    methods:{
        setEditorContent(content){
            this.content = content;
        }
    }
}
</script>

我还曾经见到同事写过这样的代码:

javascript 复制代码
 let codeMirrorDom = this.$refs['code-mirror'].$el;
 let scrollTop = codeMirrorDom.querySelector('.CodeMirror-scroll').scrollTop;

这里虽然没有依赖孙子组件,但是你却必须要知道子组件的详细实现,即子组件内部有个class为"CodeMirror-scroll"的元素,一旦子组件的这个元素类名发生变化,则会导致不可预期的bug发生,这都是违反最小知识原则的。

再举个函数调用的示例:

javascript 复制代码
function getData(rowData){
    let type = rowData.row.type;
    let status = rowData.row.detail.status; //比如知道参数下的子属性、孙子属性对象结构
    //根据id获取数据
}

在这个函数实现中,你必须要知道参数rowData的具体结构,一般我们可能需要了解某个对象参数有哪些属性,但是我们不应该还去了解参数的某个孙子属性、重孙属性结构,假设我们要根据类型type和status去获取数据,那么直接给我传递过来type和status即可,而不必额外去学习参数对象的具体格式。

假如你的同事让你来实现这个getData方法,你觉得下面那种更好理解,需要的知识量更少?

javascript 复制代码
function getData(rowData){
    
}

function getData2(type, status){
    
}

最小知识原则的最大价值就是解耦,在组件开发时,要和子组件、孙子组件的的具体实现解耦;在类开发时,要和子类、孙子类的具体实现解耦;在函数开发时,要和参数的子属性、孙子属性实现解耦。所有的道理都是相通的,都是避免两块代码或者数据耦合太深,减少维护时所需的知识量。

狭义最少知识 VS 广义最少知识

以上关于最少知识原则的概念,我们可以称为狭义最少知识原则,即只规范了对象和子对象、孙子对象的依赖关系,而在编程中,只关注父子孙对象的依赖关系是远远不够的。

广义的最少知识原则我们可以理解为,在开发、维护、使用某段代码时,尽可能的需要最少的知识,简单的说就是别人怎么样只通过少量的业务知识储备,就可以去轻松地维护或者使用你写的代码。

我们通过几个简单的示例来说明什么是广义的最少知识原则。

最常见的就是硬编码一些状态值,比如下面这段代码,当你要维护这段代码时,你需要哪些知识储备?至少要知道不同数字代表的状态含义吧,这就是维护这段代码的前提,维护一段代码的前提肯定越少越好。

javascript 复制代码
function sync(app){
    if(app.status === 0){
        
    }else if(app.status === 1){
        
    }
}

修改方式大家肯定也都知道,只需要用常量字符串代替这些硬编码数字即可。

再比如前端最常见的API调用,我们一般会把所有的业务请求封装起来,简化调用时对后端接口的了解。

我们以封装一个获取新闻详情的调用为例:

javascript 复制代码
//serve.js
//把各种资源的操作封装起来
import axios from 'axios'
import url from './const/url.js'
export function getNewsDetail(newsId){
    return axios.get(url.newsDetail, {
        params:{
            id: newsId
        }
    })
}

在需要获取新闻详情时,只需要调用getNewsDetail方法,并把新闻id传递进去即可,这样在开发相关业务时,不需要知道获取新闻的具体url是什么,也不需要知道后端要求的参数结构是什么,只要知道获取新闻详情调用这个方法即可,减少了业务维护的知识储备量。

javascript 复制代码
import {getNewsDetail} from './serve.js'

getNewsDetail(1)

各种示例不再赘述,总结起来就一点,当你在开发一个组件、一个函数、一个模块时,思考下,如果一个新人来维护,他需要多少知识储备才能顺利上手,需要的知识储备越少越好,这就是我们说的广义最少知识原则。

DRY原则

DRY原则(Don't Repeat Yourself)是一种重要的编程原则,强调避免代码重复。它的核心思想是,任何一段代码都应该在系统中只有唯一的一份存在,而不应该重复出现。

关于代码重复,前面的文章已经花了一个章节来介绍如果提升代码复用性,有兴趣的可以翻看之前的文章。

代码重复无外乎三个原因:

  • 不知道有重复:加强团队成员之间的沟通,基础组件和方法进行周知和宣贯,建立前端团队的文档库。
  • 知道有重复,懒着抽取:提升对DRY原则的重视,减少技术债,不要只顾开发爽,维护才是耗时最长的阶段
  • 知道有重复,没法复用:大概率设计不符合单一职责原则,编码粒度太粗

同时在复用时要注意一个原则,同一个业务可以复用,不同业务即使UI差不多也不能复用,不用因为复用而造成不同业务的深度耦合,得不偿失。

KISS原则

KISS原则是"保持简单和愚蠢(Keep It Simple and Stupid)"的缩写,强调在解决问题或设计产品时要保持简单性和易用性。

经常听到一种说法是要把用户当"傻子",这里并不是对用户人格的一种贬低,而是说用户在使用产品之前,很可能并不具备较深的知识储备,不能要求用户有过高的专业性,这个问题在我们过往的产品反馈中经常遇到,有时候你认为用户应该了解某些专有名词,事实往往事与愿违。

同时由于每个用户的过往知识、经验、学习能力、性格(急躁)等各不相同,为了满足绝大部分用户使用产品的易用性,必须以用户的水平下限作为我们产品设计的依据,而不是看用户的水平上限。只有一个"傻子"一样的人都能用明白,才说明产品真的做到了易用。这就和白居易把自己写的诗读给一个目不识丁的老奶奶听一样,只要老奶奶听不懂他就改,最终白居易的诗才真的达到了通俗易懂。

这个原则不只是应用在产品设计上,编码上也是一样的,大到一个开源项目,小到一个公共组件/函数,只要你的代码是有使用用户的,都要记住,用户是"傻子",不要对用户有过高的预期。

以我的开源项目 vue-office为例,在此之前如果要进行文件预览(docx、excel、pdf)是比较麻烦的,虽然代码不是那么复杂,但是对于一个新手来说,还是有很多学习成本的,以使用pdf.js进行pdf预览为例。

vue 复制代码
<template>
  <div>
    <div v-for="pageNum in numPages" :key="pageNum">
      <canvas :ref="`pdfCanvas-${pageNum}`"></canvas>
    </div>
  </div>
</template>

<script>
import pdfjsLib from 'pdfjs-dist';

export default {
  name: 'PdfPreview',
  props: {
    pdfUrl: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      numPages: 0
    };
  },
  mounted() {
    this.renderPdf();
  },
  methods: {
    renderPdf() {
      pdfjsLib.getDocument(this.pdfUrl).promise.then(pdf => {
        this.numPages = pdf.numPages;

        for (let pageNum = 1; pageNum <= this.numPages; pageNum++) {
          pdf.getPage(pageNum).then(page => {
            const canvas = this.$refs[`pdfCanvas-${pageNum}`][0];
            const context = canvas.getContext('2d');
            const viewport = page.getViewport({ scale: 1 });

            canvas.width = viewport.width;
            canvas.height = viewport.height;

            page.render({
              canvasContext: context,
              viewport: viewport
            });
          });
        }
      });
    }
  }
};
</script>

这还是简化之后的例子,里面没有包括引用Worker文件、cmaps文件等内容,而且针对Vue2和3还需要做一些示例修改,虽然不到100行代码,但是如果让一个新手去做,不一定能做出很好的效果,考虑的也不一定全面,比如大文件的懒加载、宽高自适应、低版本浏览器的兼容等,也正是基于此,才有了vue-office项目,虽然本质上还是借助第三方库来实现,但是简化了实现细节,可以让小白用户几分钟内完成多种文件的预览。

vue-office进行pdf预览:

vue 复制代码
<vue-office-pdf
    src="https://**.pdf"
/>

可以看出,使用非常简单,只要传递一个pdf文件url,即可完成预览,为了支持多种场景属性src支持多种格式,包括url、Blob、ArrayBuffer、Response,同时支持Vue2和Vue3,自动根据用户环境切换不同版本代码。正是基于KISS原则的指导,才有了这个开源项目的成功,短时间内收获1.7k star。

保持简单的重要手段是封装,就像空调遥控器上往往都会有个睡眠模式,按了遥控模式后空调通常会进行以下几个操作:

  • 调整温度:自动调整空调的温度设置,使室内温度逐渐升高或降低,以提供更适合睡眠的环境。
  • 调整风速:减小风速,以避免直接吹向人体。
  • 调整噪音:减小空调的运转声音,以提供更安静的睡眠环境。
  • 调整定时功能:空调在一定时间后自动关闭或调整温度,以节省能源并确保你在睡眠过程中保持舒适。 这不就是我们通常说的一键操作嘛,一次按键进行多个动作,在我们设计模式中也可以称为外观模式。

外观模式并不神奇,以项目中常用的Button按钮封装为例,我们项目中通常有这几种操作,添加、编辑、导入、导出、删除等,不同操作按钮通常有不同的样式和图标,如添加按钮通常type设为primary,并且配置一个加号icon,删除按钮type则为danger,并配一个删除icon,然后如果每个人都要记住不同按钮怎么配置则比较麻烦,我们可以通过外观模式来简化这个配置。

vue 复制代码
// 添加按钮
<el-button type="primary" icon="el-icon-plus"></el-button>

//编辑按钮
<el-button type="warning" icon="el-icon-edit"></el-button>

//删除按钮
<el-button type="danger" icon="el-icon-delete"></el-button>

我们可以封装一个MyButton按钮,提供一个operate属性,当operate为add时,将按钮的type设为primary同时icon设为el-icon-plus,其他操作也一样逻辑,这样使用起来就变的非常简单了。

vue 复制代码
//添加按钮
<my-button operate="add"/>

//编辑按钮
<my-button operate="edit"/>

//删除按钮
<my-button operate="delete"/>

好了,这样封装后,你觉得简单吗?

当然了易用性和扩展性天然会存在一些冲突,在提升易用性的同时,需要考虑特殊场景如何简单地进行扩展,编程就是平衡的艺术。

YAGNI原则

YAGNI原则是软件开发中的一种原则,它代表着"You Ain't Gonna Need It",意思是"你不会需要它"。这个原则的核心思想是在开发过程中,不要去实现那些当前并不需要的功能。

YAGNI原则的目的是避免过度设计和开发,以及避免浪费时间和资源在不必要的功能上。根据这个原则,开发人员应该专注于当前需要解决的问题,而不是预测未来可能出现的需求。

YAGNI原则的应用可以帮助开发团队更加敏捷地开发软件,减少不必要的复杂性和开发成本。它鼓励开发人员保持简单和精简,只实现当前需要的功能,并在未来需要时再进行扩展和改进。

然而,YAGNI原则并不意味着完全忽视未来的需求。它只是提醒开发人员在做决策时要权衡利弊,并避免过度设计和开发。在实践中,开发人员需要根据具体情况和项目需求来决定是否应用YAGNI原则。

过度设计 VS 不设计

在实际编程工作中,很多人可能拿YAGNI作为挡箭牌,以不要过度设计为由,从而不进行设计,最常见的就是组件抽取。

有的同学可能觉得只要没有其他页面复用,就不用抽取组件,抽取了就是过度设计,个人认为这是不正确的,提前抽取组件,以应对未来可能的复用需求,不一定属于过度设计,就算未来没有复用需求,我们也应该根据业务模块进行组件划分,以简化整个项目的结构,让项目更加清晰易读可维护,如果抽取的组件未来复用了更好,不复用也并没有坏处。

以掘金首页为例,假如我现在接到这样一个页面开发任务:

我会考虑将列表项封装成一个组件ArticleItem,这样我列表的开发逻辑就很清晰,只需要请求相应数据,然后将文章列表循序传递给ArticleItem组件即可渲染出整个列表,其次我可以预见列表展示可能会在多个场景出现,比如未来的文章详情底部可能会有相似文章推荐等,就算没有也没关系,现在的抽取本身就有价值。

但是我不会在此刻把文章标题抽取成为一个组件,首先我现在还没看到别的页面有这个复用场景,其次抽取文章标题并不会对我现在的代码结构带来多大的简化效果,最后如果真的有一天要进行文章标题的抽取我也可以轻松实现,并不会带来很大的成本,综合这些考虑,我决定暂不进行如此细粒度的组件抽取。

是否过度设计取决于抽取的颗粒度,在需求没有出现之前,没有必要进行特别细粒度的组件抽取,但是不进行组件抽取则是一种懒政的行为。

开发中遇到的另一个常见场景是,接到需求后直接进入了开发,而不进行设计,更不编写设计文档,代码写到哪算哪,怎么说呢,写代码和建设大楼一样,就算不是盖一个摩天大楼,哪怕是盖个猪圈不提前设计也有推到重来的可能。

面向现在 VS 面向未来

YAGNI原则告诉我们应该面向现在编程,设计方案应该是刚刚好解决了现在的问题,不多也不少,不要去实现那些当前并不需要的功能。

比如我们实现一个根据状态返回色值的方法,假设现在只有两个状态(成功和失败),那么我们只需要通过if else来实现即可,没有必要因为你觉得以后状态可能会变成3个或者更多,而在当下使用switch或者策略模式来实现当前这个需求。

javascript 复制代码
//这就够了,恰到好处
function getColorByStatus(status){
    if(status === 'success'){
        return 'green'
    }
    return 'red'
}

// 仅仅两个状态没必要
function getColorByStatus2(status){
    switch (status){
        case 'success':
            return 'green';
        default:{
            return 'red'
        }
    }
}

虽然编码应该着重基于当前需求,但是我们仍然要重视未来可能的需求变化,确保未来需求变化之后,你是有应对策略的,特别是针对一些老数据升级相关的策略,这些想明白了之后才能大胆的基于当前需求编码,否则适当做一些提前工作,也不失是一种未雨绸缪的好方法。

就像上面我举的这个是用if else 还是 switch的问题,你完全可以大胆的基于当前需求选择最合适的编码方案,等以后真的status变成了多个,你再换成switch也不迟,并不会有多大的重构成本,相比之下,保持当前代码的可读性才是最紧迫的。

前端编程之道系列,欢迎关注,github.com/501351981/H... 欢迎前端同学微信交流: hit757 ,添加务必注明掘金

相关推荐
慧一居士12 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead14 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina6 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
伍哥的传说8 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409198 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app