单一责任原则(SRP)倒指责被定义为“引起变化的原因”。如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。每个职责都是变化的一个轴线,如果一个方法承担了过多的指责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。
此时,这个方法通常是一个不稳定的方法,修改代码总是一件危险的时,特别是两个职责耦合在一起的时候,一个职责发生变化可能会影响到其他职责的实现,造成意想不到的破坏,这种耦合性得到的是低内聚和脆弱的设计。
因此,SRP原则体现为:一个对象(方法)只做一件事情。
举一个创建单例弹窗的例子
const createSingleDialog = (() => {
let instance = null
return () => {
if (!instance) {
instance = document.createElement('div')
instance.innerHTML = '弹窗的内容...'
instance.style.display = 'none'
document.body.appendChild(instance)
}
return instance
}
})()
const dialog = createSingleDialog()
根据上述示例,createSingleDialog
方法即做了管理单例,又做了创建单例的两个事,显然违背了SRP
原则。
我们把管理单例的职责和创建单例的职责分别封装在两个方法里,这两个方法可以独立变化而又互不影响,当它们连接在一起时,就完成了创建唯一弹窗的功能,下面代码显然是更好的做法:
// 获取单例
const getSingle = (creatorFn) => {
let result = null
return (...args) => {
return result || (result = creatorFn(...args))
}
}
// 创建弹窗
const createDialog = () => {
const instance = document.createElement('div')
instance.innerHTML = '弹窗的内容...'
instance.style.display = 'none'
document.body.appendChild(instance)
return instance
}
const createSingleDialog = getSingle( createDialog )
const dialog1 = createSingleDialog()
const dialog2 = createSingleDialog()
console.log(dialog1 === dialog2) // 输出:true
此时如果我还想创建一个单例的iframe,也只需添加一个创建iframe的职责即可
const createIframe = () => {
const iframe = document.createElement('iframe')
document.body.appendChild(iframe)
return iframe
}
const createSingleIframe = getSingle( createIframe )
const singleIframe = createSingleIframe()
singleIframe.src = 'https://www.sto.cn'
SRP
原则的优缺点SRP
原则的优点是降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响带其他的职责。
但SRP
原则也有一些缺点,最明显的是会增加编码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间的互相联系的难度。
SRP
原则是所有原则中最简单也是最难正确运用的原则之一。
要明确的是,并不是所有的职责都应该一一分离。
一方面,如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。比如在ajax
请求的时候,常见xhr
和请求几乎总是在一起的,那么创建xhr
对象的职责和发送请求的职责就没必要分开。
另一方面,职责的变化轴线仅当它们确定会发生变化时才有意义,即使两个职责已经被耦合在一起,但它们还没有变化的征兆,那么也许没有必要主动分离它们,在代码需要重构的时候再进行分离也不迟。
开放-封闭原则最早由Eiffel语言的设计者Bertrand Meyer在其原著作Object-Oriented Software Construction中提出。它的定义如下:
软件实体(类,模块,函数)等应该是可以扩展的,但是不可修改
在明白开放-封闭原则的定义之前,先看一个示例:
假设我们是一个大型Web项目的维护人员,在接手这个项目时,发现它已经有10万行以上的js代码和数百个js文件
不久后接到了一个新的需求,即在window.onload
函数中打印出页面中所有节点数量。这当然不难,于是我们打开编辑器,搜索出window.onload
函数的位置,在函数内部添加如下代码:
window.onload = () => {
// 省略原有代码
console.log( document.getElementsByTagName('*').length )
}
如果目前的window.onload
已经是一个拥有500行代码的函数,里面密布着何种变量和交叉的业务逻辑,而我们的需求又不仅仅是打印一个log这个简单。那么“改好一个bug,引发其他bug”这样的事情就真的可能发生。我们永远不知道刚刚的改动会有什么副作用,很可能会引发一些列的连锁反应。那么,有没有办法在不修改代码的情况下,就能满足新需求呢?通过添加代码,而不是修改代码的方式,来给window.onload
添加新的功能,代码如下:
Function.prototype.after = function (afterFn) {
const _self = this
return function(...args){
const ret = _self.apply(this, args)
afterFn.apply(this, args)
return ret
}
}
window.onload = ( window.onload || function () {} ).after(function(){
console.log( document.getElementsByTagName('*').length )
})
现在可以引出开放-封闭原则的思想:当需要改变一个程序的功能或者给这个程序添加新功能的时候,可以使用添加代码的方式,但是不允许改动程序的源代码。
让程序一开始就尽量遵循开放-封闭原则,并不是一件容易的事。一方面,我们需要尽快知道程序在哪些地方会发生变化,这要求我们有一些“未卜先知”的能力。另一方面,留给程序员的需求排期并不是无限的,所以我们可以说服自己去接受不合理的代码带来的第一次折腾。在最初编写代码的时候,先假设变化永远不会发生,这有利于我们迅速完成需求。当变化发生时并且队我们接下来的工作造成影响的时候,可以再回过头来封装这些变化的地方。然后确保我们不会掉进同一个坑里面。
Copyright © Chenyz的知识星球🌍 2025.豫ICP备2024045759号