企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] # 单一职责 就一个类而言,应该仅有一个引起它变化的原因。 SRP原则体现为:一个对象(方法)只做一件事情。 ## 设计模式中的SRP原则 ### 代理模式 图片预加载,通过增加虚拟代理的方式,把预加载图片的职责放到代理对象中,而本体仅仅负责往页面添加img标签。 ```javascript var myImage = (function(){ var imgNode = document.createElement('img'); document.body.appendChild(imgNode); return { setSrc: function(src){ imgNode.src = src; } } })(); // 创建proxyImage var proxyImage = (function(){ var img = new Image; img.onload = function(){ myImage.setSrc(this.src); }; return { setSrc: function(src){ myImage.setSrc('file://xxx'); img.src = src; } } })(); proxyImage.setSrc('http://sxxx'); ``` 把添加img标签的功能和预加载图片的职责分开放到两个对象中,这两个对象各自都只有一个被修改的动机。在它们各自发生改变的时候,也不会影响另外的对象。 ### 迭代器模式 ```javascript var appendDiv = function(data){ for(var i=0, l=data.length; i < l; i++){ var div = document.createElement('div'); div.innerHTML = data[i]; document.body.appendChild(div); } }; appendDiv([1,2,3,4,5,6]); ``` `appendChild`函数本来只是负责渲染数据,有必要吧遍历data的职责提取出来。 ```javascript var appendDiv = function(data){ each(data, function(i, n){ // each 未实现 var div = document.createElement('div'); div.innerHTML = n; document.body.appendChild(div); }) }; appendDiv([1,2,3,4,5,6]); ``` ### 单例模式 ```javascript var createLoginLayer = (function(){ var div; return function(){ if(!div){ div = docuement.createElement('div'); div.innerHTML = 'login'; div.style.display = 'none'; document.body.appendChild(div); } return div; } })(); ``` 应该要把管理单例的职责和创建登录浮窗的职责分别封装在两个方法里。 ```javascript var getSingle = function(fn){ // 获取单例 var result; return function(){ return result || (result = fn.apply(this, arguments)); } }; var createLoginLayer = function(){ // 创建登录浮窗 var div = document.createElement('div'); div.innerHTML = '我是登录浮窗'; document.body.appendChild(div); return div; }; var createSingleLoginLayer = getSingle(createLoginLayer); var loginLayer1 = createSingleLoginLayer(); var loginLayer2 = createSingleLoginLayer(); ``` ### 装饰者模式 通常让类或者对象一开始只具有一些基础的职责,更多的职责在代码运行时被动态装饰到对象上面。装饰者模式可以为对象动态增加职责,从另一个角度来看,这也是分离职责的一种方式。 ```javascript Function.prototype.after = function(afterFn){ var _self = this; return function(){ var ret = _self.apply(this,arguments); afterFn.apply(this, arguments); return ret; } }; var showLogin = function(){ console.log('open'); }; var log = function(){ console.log('log'); }; document.getElementById('button').onclick = showLogin.after(log); ``` ## 何时应该分离职责 要明确,并不是所有的职责都应该一一分离。 如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。 职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们,在代码需要重构的时候再进行分离也不迟。 ## 违法SRP原则 *This is sometime hard to see*,未必要在任何时候都一成不变地遵守原则。在实际开发中,因为种种原因违法SRP的情况并不少见。在方便性与稳定性之间要有一些取舍。 ## SRP原则的优缺点 优点: - 降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响到其它的职责。 缺点: - 最明显的是会增加编写代码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。 # 最少知识原则 最少知识原则(LKP)说的是一个软件实体应当尽可能少地与其他实体发生相互作用。也叫迪米特法则。 ## 减少对象之间的联系 最少知识原则要求我们在设计程序时,应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系。常见的做法是引入一个第三者对象,来承担这些对象之间的通信作用。如果一些对象需要向另一些对象发起请求,可以通过第三者来转发这些请求。 ## 设计模式中的最少知识原则 ### 中介者模式 中介者模式很好滴体现了最少知识原则。通过增加一个中介者对象,让所有的相关对象都通过中介者对象来通信,而不是互相引用。所以,当一个对象发生改变时,只需要通知中介者对象即可。 ### 外观模式 外观模式的作用是对客户屏蔽一组子系统的复杂性。外观模式对客户提供一个简单易用的高层接口,高层接口会把客户的请求转发给子系统来完成具体的功能实现。大多数客户都可以通过请求外观接口来达到访问子系统的目的。 ```javascript var A = function(){ a1(); a2(); }; var B = function(){ b1(); b2(); }; var facade = function(){ A(); B(); }; facade(); ``` 外观模式的作用主要有两点: - 为一组子系统提供一个简单便利的访问入口。 - 隔离客户与复杂子系统之间的联系,客户不用去了解子系统的细节。 外观模式是符合最少知识原则的。 ## 封装在最少知识原则中的体现 封装在很大程度上表达的是数据的隐藏。一个模块或者对象可以将内部的数据或者实现细节隐藏起来,只暴露必要的接口API供外界访问。对象之间难免产生联系,当一个对象必须引用另外一个对象的时候,我们可以让对象只暴露必要的接口,让对象之间的联系限制在最小的范围之内。 把变量的可见性限制在一个尽可能小的范围内,这个变量对其他不相关模块的影响就越小,变量被改写和发生冲突的机会也越小。这也是广义的最少原则的一种体现。 # 开放-封闭原则 在面向对象的程序设计中,开放-封闭原则(OCP)是最重要的一条原则。很多时候,一个程序具有良好的设计,往往说明它是符合开放-封闭原则的。软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改的。 ## 拓展window.onload函数 ```javascript Function.prototype.after = function(afterFn){ var _self = this; return function(){ var ret = _self.apply(this, arguments); afterFn.apply(this, arguments); return ret; } }; window.onload = (window.onload || function(){}).after(function(){...}); ``` 通过动态装饰函数的方式,我们完全不用理会从前`window.onload`函数的内部实现。 ## 开放和封闭 开放-封闭原则的思想:当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。 ## 用对象的多态性消除条件分支 利用多态的思想,可以把程序中不变的部分隔离出来(动物都会叫),然后把可变的部分封装起来(不同类型的动物发出不同的叫声),这样一来程序就具有了可扩展性。 ```javascript var makeSound = function(animal){ animal.sound(); }; var Duck = function(){}; Duck.prototype.sound = function(){...}; var Dog = function(){}; Dog.prototype.sound = function(){...}; makeSound(new Duck); makeSound(new Dog); ``` ## 找出变化的地方 最明显的就是找出程序中将要发生变化的地方,然后把变化封装起来。 通过封装的方式可以把系统中稳定不变的部分和容易变化的部分隔离开来。在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,那么替换起来也相对容易。而变化部分之外的就是稳定的部分。在系统的演变过程中,稳定的部分是不需要改变的。 ### 放置挂钩 放置挂钩(hook)也是分离变化的一种方式。我们在程序有可能发生变化的地方放置一个挂钩,挂钩的返回结果决定了程序的下一步走向。 ### 使用回调函数 回调函数是一种特殊的挂钩。可以把一部分易于变化的逻辑封装在回调函数里,然后把回调函数当作参数传入一个稳定和封闭的函数中。当回调函数被执行的时候,程序就可以因为回调函数的内部逻辑不通,而产生不同的结果。 ## 设计模式中的开放-封闭原则 开放-封闭原则是编写一个好程序的目标,其它设计原则都是达到这个目标的过程。 ### 发布-订阅模式 发布-订阅模式用来降低多个对象之间的依赖关系,它可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。当有新的订阅者出现时,发布者的代码不需要进行任何修改;同样当发布者需要改变时,也不会影响到之前的订阅者。 ### 模板方法模式 模板方法是一种典型的通过封装变化来提高系统扩展性的设计模式。在一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽出来放到父类的模板方法里面;而子类的方法具体怎么实现则是可变的,于是把这部分变化的逻辑封装到子类中。通过增加新的子类,便能给系统增加系统新的功能,并不需要改动抽象父类以及其他的子类,这也是符合开发-封闭原则的。 ### 策略模式 策略模式和模板方法模式是一对竞争者。在大多数情况下,它们可以相互替换使用。模板方法模式基于继承的思想,而策略模式则偏重于组合和委托。 策略模式将各种算法都封装成单独的策略类,这些策略类可以被交换使用。策略和使用策略的客户代码可以分别独立进行修改而互不影响。我们增加一个新的策略类也非常方便,完全不用修改之前的代码。 ### 代理模式 拿预加载图片举例,我们现在已有一个给图片设置`src`的函数`myImage`,当我们想为它增加图片预加载功能时,一种做法是改动`myImage`函数内部的代码,更好的做法是提供一个代理函数`proxyMyImage`,代理函数负责图片预加载,在图片预加载完成之后,再将请求转交给原来的`myImage`函数,`myImage`函数在这个过程中不需要任何改动。 预加载图片的功能和给图片设置`src`的功能被隔离在两个函数里,它们可以单独改变而互不影响。`myImage`不知晓代理的存在,它可以继续专注于自己的职责---给图片设置`src`。 ### 职责链模式 一个例子,把一个巨大的订单函数分别拆成了500元订单、200元订单以及普通订单的3个函数。这3个函数通过职责链连接在一起,客户的请求会在这条链条里面依次传递: ```javascript var order500yuan = new Chain(function(orderType, pay, stock){...}); var order200yuan = new Chain(function(orderType, pay, stock){...}); var orderNormal = new Chain(function(orderType, pay, stock){...}); order500yuan.setNextSuccessor(order200yuan).setNextSuccessor(orderNoraml); order500yuan.passRequest(1,true,10); ``` ## 开放-封闭原则的相对性 让程序符合开放-封闭原则的代价是引入更多的抽象层次,更多的抽象有可能会增大代码的复杂度。 - 挑选出最容易发生变化的地方,然后构造抽象来封闭这些变化。 - 在不可避免发生修改的时候,尽量修改那些相对容易修改的地方。那一个开源库来说,修改它提供的配置文件,总比修改它的源代码来得简单。 ## 接受第一次愚弄 一方面,我们需要尽快知道程序在哪些地方会发生变化,这要求我们有一些“未卜先知”的能力。另一方面,留给程序员的需求排期并不是无限的,所以我们可以说服自己去接受不合理的代码带来的第一次愚弄。先假设变化永远不会发生,这有利于我们迅速完成需求。当变化发生并且对我们接下来工作造成影响的时候,可以再回头来封装这些变化的地方。 # 接口和面向接口编程 不关注对象的具体类型,而仅仅针对超类型中的“契约方法”来编写程序,可以产生可靠性高的程序,也可以极大地减少子系统实现之间的相互依赖关系。这就是面向接口编程,而不是面向实现编程。 ## JavaScript语言是否需要抽象类和interface 抽象类和`interface`的作用主要都是以下两点: - 通过向上转型来隐藏对象的真正类型,以表现对象的多态性。 - 约定类与类之间的一些契约行为。 接口在`JavaScript`中的最大作用就退化到了检查代码的规范性。 ## 用鸭子类型进行接口检查 *如果它走起来像鸭子,叫起来也是鸭子,那么它就是鸭子。* 鸭子类型是动态类型语言面向对象设计中的一个重要概念,利用鸭子类型的思想,不必借助超类型的帮助,就能在动态类型语言中轻松地实现设计原则:面向接口编程,而不是面向实现编程。 用鸭子类型来判断一个对象是否为数组 ```javascript var isArray = function(obj){ return obj && typeof obj === 'object' && typeof obj.length === 'number' && typeof obj.splice === 'function' }; ``` # 代码重构 模式和重构之间有着一种与生俱来的关系。从某种角度来看,设计模式的目的就是为了许多重构行为提供目标。 1. 提炼函数 2. 合并重复的代码片段 3. 把条件分支语句提炼成函数 4. 合理使用循环 5. 提前让函数退出代替嵌套条件分支 6. 传递对象参数代替过长的参数列表 7. 尽量减少参数数量 8. 少用三目运算符 9. 合理使用链式调用 10. 分解大型类 11. 用return退出多重循环