前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计原则

设计原则

作者头像
一粒小麦
发布2019-11-21 14:51:53
5280
发布2019-11-21 14:51:53
举报
文章被收录于专栏:一Li小麦一Li小麦

设计原则

有句古老的谚语说:“愚弄我一次,应该羞愧的是你。再次愚弄我,应该羞愧的是我。”这也是一种有效的对待软件设计的态度。为了防止软件背着不必要的复杂性,我们会允许自己被愚弄一次。 ——《敏捷软件开发原则、模式与实践》

好的代码永远是需要设计的。一名高级程序员,更应该有设计师的潜质,不只是关注怎么用代码实现业务,更关注的是代码的设计。而在设计之初,应当说服自己去接受不合理的代码带来的第一次愚弄。在频繁的变化面前,抽取不变的地方,健壮自身的设计。

JavaScript用得上设计原则有单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、合成复用原则和最少知识原则等。本文将选取三个常用的阐述之。

需要明确的是:设计原则只是一种指导,没有哪条原则是在实际开发中必须遵守的。但善用原则,可以帮助更好地设计代码。

1 单一职责原则(srp)

概述

定义:就一个类而言,应该仅有一个引起它变化的原因。

在JavaScript中,需要用到类的场景并不太多,单一职责原则更多地是被运用在对象或者方法级别上。

单一职责原则(SRP)的职责被定义为“引起变化的原因”。如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。每个职责都是变化的一个轴线,如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。此时,这个方法通常是一个不稳定的方法,修改代码(尤其是修改别人的代码)总是一件危险的事情,特别是当两个或多个职责耦合在一起的时候,代码作者自己特别清楚,但是不意味着别人能很好看懂。一个职责发生变化可能会影响到其他职责的实现,造成意想不到的破坏,这种耦合性得到的是低内聚和脆弱的设计。

因此,SRP原则体现为:一个对象(方法)只做一件事情。

比如说有一个迭代器渲染列表:

代码语言:javascript
复制
const appendDiv=(data)=>{
  data.forEach((item,index)=>{
   const div=document.createElement('div');
   div.innerHTML=item;
   document.body.appendChild(div);
  })
}
appendDiv([1,2,3,4,5]);

按照作者的观点,这就不是一个好的代码。迭代器的处理和渲染的逻辑耦合在一起了。思路是写一个类似jquery的each方法来提供遍历逻辑,兼容对象和for循环。届时需求有修改,只需要调整each即可。

使用要点

并非所有职责都需要分离。如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。比如在ajax请求的时候,创建xhr对象和发送xhr请求几乎总是在一起的,那么创建xhr对象的职责和发送xhr请求的职责就没有必要分开。

此外,职责经常发生变动(改需求)时,处理才有意义。如果是铁需求,那也没必要分离。

比如jquery很多方法(attr,css),既可以赋值也可以取值,就是违反srp原则的。但是它事实上简化了开发者的写法,因而被接受。

优缺点

SRP原则的优点是降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响到其他的职责。但SRP原则也有一些缺点,最明显的是会增加编写代码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。

2 最少知识原则(lkp)

概述

最少知识原则(LKP,迪米特法则)说的是一个软件实体应当尽可能少地与其他实体发生"关系"。这里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等。

《面向对象设计原理与模式》一书举过一个例子:

某军队中的将军需要挖掘一些散兵坑。下面是完成任务的一种方式:将军可以通知上校让他叫来少校,然后让少校找来上尉,并让上尉通知一个军士,最后军士唤来一个士兵,然后命令士兵挖掘一些散兵坑。

看上去十分荒谬。但这种事情在现实代码世界都是存在的。

而最少知识原则就是,将军根本不需要关心/介入下属怎么挖坑的细节。他只要发命令即可了,"怎么挖坑我不管!"

要点

单一职责原则要求减小每个功能实体的粒度,但随之带来的就是代码复杂度的增加。

而最少知识原则要求我们在设计程序时,应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系。常见的做法是引入一个第三者对象,来承担这些对象之间的通信作用。如果一些对象需要向另一些对象发起请求,可以通过第三者对象来转发这些请求。、

最少知识原则的作用主要有两点。

  • 为一组子系统提供一个简单便利的访问入口。
  • 隔离客户与复杂子系统之间的联系,客户不用去了解子系统的细节。
封装与最少知识的关系

封装在很大程度上表达的是数据的隐藏。一个模块或者对象可以将内部的数据或者实现细节隐藏起来,只暴露必要的接口API供外界访问。

但对象之间难免发生"关系",当一个对象必须引用另外一个对象的时候,我们可以让对象只暴露必要的接口,让对象之间的联系限制在最小的范围之内。同时,封装也用来限制变量的作用域。JavaScript对变量作用域的规定是:

  • 变量在全局声明,或者在代码的任何位置隐式申明(不用var),则该变量在全局可见;
  • 变量在函数内显式申明(使用var),则在函数内可见

把变量的可见性限制在一个尽可能小的范围内,这个变量对其他不相关模块的影响就越小,变量被改写和发生冲突的机会也越小。这也是广义的最少知识原则的一种体现。

还记得缓存池吗?无论是在nodejs的module中,或是用函数自执行封装到一个闭包中,都是无可厚非的做法。

3 开放-封闭原则(ocp)

概述

有一家生产肥皂的大企业,从欧洲花巨资引入了一条生产线。这条生产线可以自动完成从原材料加工到包装成箱的整个流程,但美中不足的是,生产出来的肥皂有一定的空盒几率。于是老板又从欧洲找来一支专家团队,花费数百万元改造这一生产线,终于解决了生产出空盒肥皂的问题。 另一家企业也引入了这条生产线,他们同样遇到了空盒肥皂的问题。但他们的解决办法很简单:用一个大风扇在生产线旁边吹,空盒肥皂就会被吹走。

在面向对象的程序设计中,开放封闭原则(OCP)是最重要的一条原则。很多时候,一个程序具有良好的设计,往往说明它是符合开放封闭原则的。开放封闭原则最早由Eiffel语言的设计者BertrandMeyer在其著作 ObjectOrientedSoftwareConstruction 中提出。它的定义如下:

软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。

换个角度说:

当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。

举例

很多时候,当需求有了变动,我们就会去修改具体的实现方法。比如你接手了一个项目,大量使用了某个aaa方法。你想在每次调用时都添加一段逻辑。这时你有两种选择:

  • 修改源码
  • 扩展一个after方法

这种场景修改源码可以说是最不划算的,没人乐意主动去碰别人代码,因为往往导致bug越改越多。

而after方法在装饰者模式中已经学习到:

代码语言:javascript
复制
Function.prototype.after=function(fn){
  const _this=this;
  return function(){
    const ret=_this.apply(this,arguments);
    fn.apply(this,arguments);
    return ret;
  }
}

aaa=aaa.after(function(){
  // 你的逻辑
})

于是作为维护者,你就不用关心前任的逻辑写的丑陋与否。重要的是功能被你优雅地加上去了。

使用要点

开放封闭原则是最明显的就是找出程序中将要发生变化的地方,然后把变化封装起来。通过封装变化的方式,可以把系统中稳定不变的部分和容易变化的部分隔离开来。

在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经被封装好的,那么替换起来也相对容易。而变化部分之外的就是稳定的部分。在系统的演变过程中,稳定的部分是不需要改变的。

这是工作中遇到的需求:进入页面前请求一个接口,然后渲染不同的视图,原来的代码是这样的:

代码语言:javascript
复制
http.post(api,(result)=>{
  if(result.role=='teacher'){
    // 渲染老师页面
  }else if(result.role=='student'){
    // 渲染学生页面
  }else if(result.role=='default'){
    // 渲染默认页面
  }
})

如何处理?

利用对象的多态性来处理ifelse:

代码语言:javascript
复制
const page={
  teacher:()=>{
    // 渲染老师页面
  },
  student:()=>{
    // 渲染学生页面
  },
  default:()=>{
    // 渲染默认页面
  }
}

const loadPage=(role)=>(
    page[role]();
)

http.post(api,(result)=>{
  const {role}=result;
  loadPage(role);
})

这样就优雅很多了。接下来需求没完,假设页面中有一个add按钮,点击需要后端判断条件,要么根据后端返回结果做三个操作:

  • 条件可以满足,但提示确认信息后,用户确认即可调用add接口
  • 条件不满足,返回调用失败
  • 条件满足,直接调用operate接口

这时候逻辑应该是:

代码语言:javascript
复制
const judge=(callback)=>{
  http.post('/judge',(reuslt)=>{
    callback(result);
  })
}

const add=(callback)=>{
  http.post('/add',(reuslt)=>{
    callback(result);
  })
}

btnAdd.addEventListener('click',()=>{
  jude((judeResult)=>{
    // ..处理逻辑
    add((addResult)=>{
      // 添加完的处理逻辑
    })
  })
})

通过设置回调函数,可以把不怎么变的内容封装起来。

当然这种写法容易进入"回调地狱"。

优缺点

更有一些代码是无论如何也不能完全封闭的,总会存在一些无法对其封闭的变化。作为程序员,我们可以做到的有下面两点:

  • 挑选出最容易发生变化的地方,然后构造抽象来封闭这些变化。
  • 在不可避免发生修改的时候,尽量修改那些相对容易修改的地方。拿一个开源库来说,修改它提供的配置文件,总比修改它的源代码来得简单。
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 设计原则
    • 1 单一职责原则(srp)
      • 概述
      • 使用要点
      • 优缺点
    • 2 最少知识原则(lkp)
      • 概述
      • 要点
      • 封装与最少知识的关系
    • 3 开放-封闭原则(ocp)
      • 概述
      • 举例
      • 使用要点
      • 优缺点
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档