面试官为啥总是嘴上挂着低耦合高复用?

2022-09-20  本文已影响0人  super可乐

引言

在实际开发的过程中,你是否经常遇到这样一种情形。需要用到一个组件,这个组件你抑或者其他小伙伴之前已经实现了,你内心窃喜,又可以使出拿来主义大法了。打开一看,发现之前的组件代码其中包含了很多强耦合的代码逻辑,导致不能够完全为你所用,不香不臭,弃之可惜食之无味。

这个时候,聪明的你,很快的想到了使出必杀技copy大法。但过来人的我相信,你内心深处是处于极度抗拒的,一方面又想赶快实现业务功能开发,另一方面又在质疑自己,为啥不能认真写出一个完美兼容所有场景的组件。你一边质疑自己,一边嘴上说着真香,画面最终定格在了control+v...

这篇文章,会从一个很简单的场景进行发散,逐渐延伸出如何写出高复用、低耦合的代码。同时这也是面试过程中,面试官喜欢提及的高频词汇,莫非其中暗藏玄机?

背景

最近在开发一个我司的后台功能需求,用的技术栈就是react+antd。在开发的过程中,涉及到一个select选择器,其中的选项是通过接口返回的,并且是要具备分页逻辑的。可以想象,这是一个很常见的业务场景。

最初的想法

由于该业务场景不是第一次出现了,所以别的功能中,肯定有类似的业务逻辑。最简单的做法就是直接把别的功能中的该部分代码粘贴过来,修修补补即可,这样还可以快点回家陪孩子玩~

就在我按下复制按钮的那一刹那,我在想,难道这就是我最终的归途吗?下次遇到同样的功能模块,依然这样做?这样一想,我不禁打了个冷颤。如果这样下去,那和咸鱼又有什么区别呢?你说你工作了多年,莫非只会copy大法...

这个时候,可能有不少同学已经在心里暗暗喷我了。因为本身就是个简单的功能,antd又这么好用,已经为我们封装了select组件,直接拿过来,再加上点自己的业务逻辑不就行了?还能玩出什么花样来?

带着上面提到的一堆疑惑,我们一一来看。在这之前,我们先大致画出组件大概的模样

从该图中我们可以得出以下几点重要信息

理解

如果我们从事编程工作,那么一定听过我在前面提到的低耦合高复用的概念。

对于它们,我是这样理解的:

将我们的常用业务模块,封装成一个组件,其中做了很多思考与实现,降低组件内部同具象业务的关联性,可以让我们在多个地方多次使用。

由于我们封装的组件,可能在多个地方需要用到,而不同的地方,可能会有不同的场景,比如上面的select框,可能会有以下不同的使用场景

面对上面的场景,如果你没有封装可复用组件的意识,那么很可能你在开发某一个具体功能的时候,都需要去基于定制化的需求重新实现一遍,这样长期下来,你回发现你的代码组件库中的结构可能会是这样

如果命名稍微规范点的话,后续我们看起来,可能还好理解点。否则,后续不管是你,还是组内的小伙伴看到的话,一定会一脸懵逼并对其嗤之以鼻...

回过头我们思考个问题,虽然它们在不同的业务场景中使用,我们也尝试进行了多个组件的封装。但是好像都是在围绕着select这个组件进行展开。而antd提供的select组件,已经为我们提供了很多不同场景下的选项prop,它们已经能够很好的为我们提供服务。那么是不是就没有必要,再去进行组件的二次封装呢?

说到这里,貌似我们一直忽略了一个问题,那就是数据源的问题

说这么多,感觉好空白。直接上代码,可能更加容易理解点~

接下来,我们再结合代码,去看看我们想要封装一个易用且高可复用的一个组件,需要去考虑哪些方面,需要解决哪些问题。

问题1:不同的场景下,为该组件提供数据源的接口构造各异

上述这样一个基础的组件,主要依赖后端返回的数据源以及分页token。但是不同的接口返回的结构却是各异的。举个例子

用户相关:

// users api response
{
  success:true,
  data:{
    userList:[
      {userName:'麻不烧',userId:'34523465234'},
      {userName:'小丁',userId:'54354656456'},
      {userName:'大马猴',userId:'78978978978'},
      ...
    ],
    nextPageToken:'xxxxxx-aaaaaa-bbbbb-xxxxxx'
  }
}

商品相关:

// products api response
{
  success:true,
  data:{
    productList:[
      {productName:'蓝鲫',productId:'lj'},
      {productName:'918腥香版',productId:'918xxb'},
      {productName:'速攻',productId:'sg'},
      ...
    ],
    nextPagingToken:'xxxxxx-aaaaaa-bbbbb-xxxxxx'
  }
}

可以看到在不同的业务中后端接口返回的作为数据源和分页token的key都是不一样的。用户接口中是userList和nextPageToken,而商品相关的却是productList和nextPagingToken。

我们并不能在这个组件中,将其固定,应该灵活多变。所以可以看到在上述代码中,我提供了一个resStructure字段,用户在具体场景中使用该组件时,自行根据接口规定传入合适的字段即可。

问题2:不同的接口数据源各异导致作为option的key和value字段并不能统一

我们都知道每一条option,都需要有自己的key和value。而它们确实依赖于我们各自的接口返回的数据源的。其实和上面这个问题类似,还是看上面用户和商品接口的返回值构造。用户接口中是userName和userId,而商品相关的却是productName和productId。这里我们同样需要定义一个optionStructure字段,方便使用者自行根据接口返回的数据源格式传入合适的字段即可。

同理,有的场景下,下拉列表中的选项,需要展示选项图片,方便用户辨别。我们可以在optionStructure prop中提供一个optionImgKey字段,如果传入的话,则代表需要展示,同时该字段也作为去匹配数据源中图片地址的key。

其它同理的,这里不再赘述,比如是否支持多选、是否支持多选、分页pageSize等...

问题3:请求接口query中的字段组装逻辑各异

直接看代码,假设两种情形,这里稍微改下上面的用户列表返回接口构造,给每条数据新增一个昵称字段

情形1:这个通过搜索内容获取用户列表的接口,可根据用户的输入进行查询,假设你输入内容麻不烧,则去数据库中查询用户名或者昵称中包含麻不烧的用户进行返回。这个时候,我们看下filter如何拼装的,同时看下返回值。

// getUserList获取用户接口(支持根据输入模糊匹配昵称或用户名)
axios.get('/user-list', {
  params: {
    filter:'userName:"*麻不烧*" OR nickName:"*麻不烧*"' // google grpc风格
  }sdf
})

// users api response,可以看到接口返回了两条数据
{
  success:true,
  total:2,
  data:{
    userList:[
      {userName:'麻不烧',userId:'34523465234',nickName:'麻不烧的前端大白话'},
      {userName:'麻不烧是个乖宝宝',userId:'34523465234',nickName:'乖宝宝'}
    ],
    nextPageToken:null
  }
}

情形2:同样的接口,只支持根据输入内容,模糊搜索中包含麻不烧的数据

// getUserList获取用户接口(只支持根据输入模糊匹配昵称)
axios.get('/user-list', {
  params: {
    filter:'nickName:"*麻不烧*"' // google grpc风格
  }
})

// users api response,可以看到接口仅返回了昵称中包含麻不烧的一条数据
{
  success:true,
  total:1,
  data:{
    userList:[
      {userName:'麻不烧',userId:'34523465234',nickName:'麻不烧的前端大白话'}
    ],
    nextPageToken:null
  }
}

通过上面这个例子,我们可以很明确地知道一个问题,对于支持搜索的select组件来说,它仅仅抛出了一个value值麻不烧,而真正去获取数据源的搜索条件filter是依赖于我们的后端和业务决定的。

同时该组件在不同的场景下使用时,filter字段都是跟着业务走的,比如说产品接口,我们可能最终拼装的filter是这样的

// getProductList获取商品(只支持根据输入精确匹配商品名)
axios.get('/product-list', {
  params: {
    filter:'productName="速攻"' // google grpc风格
  }
})

对于这样一个高耦合具体业务场景的需求,我们最常用的方式,就是利用函数传递来实现自定义逻辑,想要怎么玩,你自己说了算。而我组件本身,只抛出对应的值即可。

所以可以看到我在组件中,接受了一个fetchFilterprop字段,它的类型是一个函数。使用该组件的时候,我们传递包含我们想要的逻辑代码在函数中即可。

就拿上面几个简单的例子来说,传递的fetchFilter分别可以这样写

// getUserList获取用户接口(支持根据输入模糊匹配昵称或用户名)
fetchFilter={val => `userName:"*${val}*" OR nickName:"*${val}*`}

// getUserList获取用户接口(只支持根据输入模糊匹配昵称)
fetchFilter={val => `nickName:"*${val}*`}

// getProductList获取商品(只支持根据输入精确匹配商品名)
fetchFilter={val => `nickName="${val}`}

这样下来,我们的组件针对这个关注点,就可以和我们的业务,完全的进行解耦,可以轻松地应对多种使用场景。

总结

1.要善于归纳和抽象

我们在封装常用组件之前,一定要想好这个组件的应用场景,抽丝剥茧的识别出其中的不变点灵活点,然后对症下药。

就拿这个组件来说,可以看出来,其中的不变点就是整个组件的行为逻辑。不管你怎么变着花样玩,它其中肯定包含几个固定的逻辑,比如:

而灵活的地方,我们一般都是通过传递不同类型的prop进行实现,比如:

2.要学会不断地丰富和完善组件

很多时候,我们在做一件事的时候,关注点都在如何快速解决当下的问题。考虑自然不够全面,下次再用该组件时,会发现之前实现它时,并没有考虑周全,不管是功能还是边界条件的判断等。这是很正常的,没有人能够一口起实现一个近乎完美的组件。同时,要学会向上兼容,不要改了或新增了点逻辑之后,影响了其它使用该组件的地方,这就得不偿失了。

要学会从使用场景着手,对组件进行功能上的修饰。比如这个组件来说,当初我实现的时候,并没有第一时间想到要对触发搜索的动作做防抖,这就会导致用户搜索内容时,会频繁地触发查询接口。别看不起它,你发出的请求都是在走带宽,不然为啥那么多面试官喜欢问你防抖的意义和实现呢...

3.要学会去写好注释和文档

这个就不用多说了,团队之间我拿到你封装好的组件是用来的快速实现我的功能。通过注释,我能够快速的上手使用才是我最关注的点。此时此刻,我真的没有太大的兴趣和时间先去看你其中的实现~

要善于借用typescript提供的类型提示的能力。你写起来爽,我用起来更爽。同时,在书写规范上,一定要有所重视,尽量做到命名正规点,做到见名知意。

作者:麻不烧
链接:https://juejin.cn/post/7143873919412355109

上一篇下一篇

猜你喜欢

热点阅读