前端编程之道系列,欢迎关注,github.com/501351981/H…
单一职责原则SRP
单一职责是著名软件开发大佬鲍勃大叔提出的SOLID原则中的第一个重要原则,它是一个听起来好像很简单明了,实际使用时却是十分不好掌握的一个原则,先看下Bob大叔对于单一职责的定义:
单一职责原则(Single Responsibility Principle,SRP)是面向对象设计中的一个原则,它指出一个类或模块应该只有一个引起它变化的原因,即一个类或模块应该只负责一项职责。
单一职责原则的核心思想是将一个类或模块的功能划分为独立的、高内聚的部分,每个部分只负责一个明确的职责,在我们前端领域可以将类或模块替换为组件、模块、函数等概念,也就是一个组件、模块或者函数应该只有一个明确的职责,在维护时只有一个引起它变化的原因。
我们通过两个实际的例子来看下什么是单一职责。
函数开发示例
假设我们现在要实现这样一个功能,获取页面上id为**的Dom元素,然后设置他的class类名,很自然的我们想到要实现一个名为setClass函数,仅仅实现这个需求非常简单,可能的实现如下:
<div id="test">测试内容</div>
<script>
function setClass(id, className){
let dom = document.getElementById(id)
dom.classList.add(className)
}
setClass('test', 'red')
</script>
可以看到这个函数做了两件事,首先获取元素,然后修改元素的类名,现在看起来一切正常,并没有什么不妥。
接着需求发生了变化,不光要支持根据元素id修改类名,还要支持class选择器和元素选择器,这时必须要修改setClass函数的实现了(假装不知道document.querySelector方法)
function setClass(selector, className){
let dom
if(selector.startsWith('#')){
//id选择器
dom = document.getElementById(selector.substr(1));
}else if(selector.startsWith('.')){
//类选择器
dom = document.getElementsByClassName(selector.substr(1));
}else {
//tag选择器
dom = document.getElementsByTagName(selector)
}
dom.classList.add(className)
}
setClass('#test', 'red')
没过多久,需求又发生了变化,上面的实现每次只能添加一个类,我们想要一次添加多个类,传递的类名要支持数组和字符串两种格式,继续修改setClass方法。
function setClass(selector, className){
let dom
if(selector.startsWith('#')){
//id选择器
dom = document.getElementById(selector.substr(1));
}else if(selector.startsWith('.')){
//类选择器
dom = document.getElementsByClassName(selector.substr(1));
}else {
//tag选择器
dom = document.getElementsByTagName(selector)
}
//增加对className格式的判断
if(Array.isArray(className)){
className.forEach(name => dom.classList.add(name))
}else{
dom.classList.add(className)
}
}
setClass('#test', ['red', 'blue'])
通过上面的示例我们可以看出,修改setClass函数有两个原因
- 如何选择要修改的元素发生了变化
- 如何修改元素的类名发生了变化
很明显这样设计不符合单一职责原则,但是不符合原则会怎样呢,目前来看仿佛一切正常,别急,那就继续变更需求。
现在有了新需求,想要通过id、类、元素选择器选中元素,然后删除元素的某个类名,也就是要增加一个removeClass方法,其中选中元素的逻辑和setClass是一样的。
function removeClass(selector, className){
let dom
if(selector.startsWith('#')){
//id选择器
dom = document.getElementById(selector.substr(1));
}else if(selector.startsWith('.')){
//类选择器
dom = document.getElementsByClassName(selector.substr(1));
}else {
//tag选择器
dom = document.getElementsByTagName(selector)
}
dom.classList.remove(className)
}
可以看到setClass和removeClass在选择元素方面存在重复,为后续的维护带来成本,之所以存在重复就是因为这两个方法职责并不单一,一个函数做了两件事,导致多个函数中的重复部分无法复用,原因找到了解决起来也就非常简单,我们只需封装三个职责单一的函数即可:
- selectDom:只负责选择Dom元素
- setClass:只为Dom元素添加类名
- removeClass:只为Dom元素移除类名
function selectDom(selector){
let dom;
if(selector.startsWith('#')){
//id选择器
dom = document.getElementById(selector.substr(1));
}else if(selector.startsWith('.')){
//类选择器
dom = document.getElementsByClassName(selector.substr(1));
}else {
//tag选择器
dom = document.getElementsByTagName(selector);
}
return dom;
}
function setClass(dom, className){
if(Array.isArray(className)){
className.forEach(name => dom.classList.add(name))
}else{
dom.classList.add(className)
}
}
function removeClass(dom, className){
dom.classList.remove(className)
}
经过这个修改之后,如果后续想要增加新的元素选择方式,则只需修改selectDom方法即可,不再会影响setClass和removeClass方法。
组件开发示例
封装组件是现在前端开发中最主要的工作,在Vue和React项目开发中,一切皆组件,大到一个页面,小到一个按钮,都可以看作是一个组件。
框架虽然鼓励用户进行组件化开发,然而却不能强制控制组件化的粒度,所以每个人写出来的页面差别甚大,特别是一些前端新手写出来的页面,动辄2000行以上,所有的UI、逻辑、样式杂糅在一起,让后面维护的人甚是头大,就算是修改一个小功能,都要去阅读大量的代码,带来极大的维护成本,究其原因,就是没有按照单一职责原则进行组件设计。
下面这样一个用户列表,如果交给你开发,你会如何划分组件?
我们先来根据UI图梳理下需求:
- 操作区:这里仅有1个添加用户按钮,点击之后一般会弹窗进行用户添加,或者跳转到添加用户界面
- 搜索区:根据用户角色进行筛选,用户角色下拉选项数据通过请求角色列表获取
- 表格展示区:
- 姓名:姓名可以点击,点击之后抽屉展示用户详细信息
- 角色:不同角色有不同的颜色标识
- 手机:隐藏手机号中间4位
- 表格操作区:
- 删除:点击删除后,二次确认是否确定删除,确定后删除数据,刷新表格
- 编辑:点击后弹窗进行用户信息修改
可以看到,这个用户列表是一个混合了多个功能的组件,至少包括了添加用户、搜索、用户信息展示(姓名展示、角色展示、手机号展示)、用户管理(删除、编辑)四个功能,如果所有功能杂糅到一个组件中,势必造成单个文件过大,难以复用的问题。
按照单一职责进行组件划分:
- 操作按钮组件 UserOperate:
- 组件职责:实现用户添加操作
- 属性:无
- 事件:对外暴露添加成功事件,主组件接收后刷新表格
- 搜索组件 UserSearch
- 组件职责:负责维护搜索表单
- 属性:默认筛选项的值
- 事件:对外暴露search事件,主组件接收后刷新表格
- 表格展示组件,一般都有第三方UI组件,无需封装
- 用户姓名 UserName
- 组件职责:展示用户姓名
- 属性:用户姓名、用户id
- 用户角色 UserRole
- 组件职责:根据不同角色类型展示不同icon和名称
- 属性:用户角色
- 用户手机 UserMobile
- 组件职责:隐藏手机号中间4位
- 用户管理 UserManager
- 组件职责:用户的管理,这里将删除和编辑混合到一起,虽然可能违反单一职责原则,但是太细的话也没有必要,如果操作很多,每个很复杂,可以酌情拆分
- 事件:对外暴露删除成功、编辑成功事件,主组件接收后刷新表格
- 主组件 UserList
- 组件职责:负责整合各个组件,根据搜索条件请求数据,赋值给用户表格
按照单一职责拆分后,主组件结构清晰,需要修改哪一块需求,可以迅速定位到相关子组件,不再像之前一样需要阅读大量代码才能定位到要修改的内容。
单一职责优缺点
通过上面两个示例,我们可以看到,符合单一职责的代码有以下几个优点:
- 增强代码的可维护性:单一职责代码一般比较精简,减少了复杂性,可读性更强;每个模块职责清晰,更加容易去除或者更换某个职责的模块,而不用担心影响其他职责的模块
- 提升代码的复用性:单一职责原则使得每个模块的功能更加独立和自治,可以更容易地被其他部分引用和复用。这样可以减少代码的重复,提高代码的可重用性,从而提高开发效率
- 提高代码的可扩展性:当一个模块只负责一项职责时,新增功能或修改现有功能时只需要修改相关的模块,而不会影响到其他部分。这使得系统更加灵活和易于扩展,可以更容易地应对变化和需求的变更。
- 提高代码的可测试性:单一职责原则使得每个模块的职责明确,因此可以更容易地编写针对每个职责的单元测试,而不必Mock大量的测试数据
虽然遵循单一职责会提升代码的可维护性、可复用性、可扩展性和可测试性,但是也存在以下几个问题,需要在编写代码时去平衡:
-
模块的数量/层级增加:遵循单一职责原则必然会导致模块的数量或层级增加,这可能会增加代码的复杂性和维护成本,取决于模块的粒度划分是否合理。
-
职责划分困难:有时候,将功能划分为独立的职责可能并不是一件容易的事情。在某些情况下,职责之间可能存在一些交叉和重叠,这可能导致职责的划分变得困难,比如上面的表格操作区域,是把删除按钮和编辑按钮封装到一起作为一个表格项管理组件,还是把他们拆开成两个组件,是比较难权衡的
-
跨职责的协调和通信:由于将一个大功能划分成不同的小模块,就比如会引起小模块之间通信的问题,这可能会增加代码的复杂性。
所以说在软件开发领域,并不存在"银弹",不能没有原则,也不能完全套用原则,就像咱们古人推崇的中庸之道,干啥都别过分,写代码就是平衡的艺术。