设计模式-6大原则
内容来自参考《大话设计模式》与自己的话术结合
- 1. 单一职责(SRP Single Responsibility Principle):
概念
就一个类而言,应该仅有一个引起它变化的原因。
如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会消弱或者抑制这个类完成其他职责的能力。
举例:
一个手机的功能:相机,mp3,mp4,电话,游戏机.. 包含的功能很多,但是不能在某个领域做的很好。单反就是单一职责,做好自己拍照的事。游戏机(xbox, ps5)专注游戏。
代码片段
class MyMp3 {
constructor(mp3) {
this.mp3 = mp3
}
play() {} // 播放音乐
pause() {} // 暂停音乐
next() {} // 下一首音乐
prev() {} // 上一首音乐
}
每个函数体单纯得完成一个大类的功能点,对于里面更为复杂的逻辑可以拆分为更细的函数
一个函数做单一的事,但也不能单个函数内存在大量逻辑代码
1) 类行数不过300行、函数不过50行、函数和属性的个数不多于10个。
2 )类依赖的其他类过多,不符合高内聚低耦合的思想,需要拆分。
3 )私有方法过多,可以考虑放到其他类里弄成public公用,提高复用性。
- 2. 开放-封闭原则(OCP Open Closed Principle):
概念
软件实体(类、模块、函数等)应该可以扩展,但是不可修改。
无论模块是多么的‘封闭’,都会存在一些无法对之封闭的变化。既然不可能完全封闭,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。
举例:
作为程序员,进行软件开发时,不要指望需求不会变更,而是要考虑需求改变时如何不让代码推倒重来,代码编写初期,尽量抽象化代码以隔绝变化,代码编写中期,尽量不改变已有代码,而是增加代码面对变化,代码编写的越多,越难以抽象化代码,除非推倒重来。
符合面向对象的,具体可维护,可扩展,可复用,灵活性好等优点。我们对程序中呈现的频繁的变化的部分进行抽象,然而,对于应用程序中的每个部分都刻意的进行抽象同样不是一个好主意,拒绝不成熟的抽象和抽象本身一样重要。
当一个需求变化导致程序中多个依赖模块都发生了级联的改动,那么这个程序就展现出了我们所说的 "坏设计(bad design)" 的特质。应用程序也相应地变得脆弱、僵化、无法预期和无法重用。开放封闭原则(Open Closed Principle)即为解决这些问题而产生,它强调的是你设计的模块应该从不改变。当需求变化时,你可以通过添加新的代码来扩展这个模块的行为,而不去更改那些已经存在的可以工作的代码。
class MyPhone {
constructor () {
}
playMusic() // 播放音乐
pauseMusic() // 暂停音乐
nextMusic() // 下一首音乐
prevMusic() // 上一首音乐
sendMassage() // 发送qq消息
readText() // 读取文字
showText() // 展示文字
}
// 上面的函数为一个大类功能点,但是如果作为手机,想在播放音乐的同时,发送qq消息,两种类型或者多种类型同时进行,那么如果采用这样的函数写法
class MyPhone {
constructor () {
}
playMusicAndSendMassage() // 播放音乐并发送消息
pauseMusicAndReadText() // 暂停音乐并读取文字
}
// 这样写的话,函数命名大量,难以维护,耦合性太高
又或者这样
class MyPhone {
constructor (action) {
this.action = action
}
operate() { // 操作类
if(action === 'playMusic' && action === 'sendMassage'){
// 播放音乐并发送消息
} else if(action=== 'pauseMusic' && action === 'readText'){
//暂停音乐并读取文字
}
}
}
// 对于维护都是困难的,而且可读性不高,那么我们要采用什么写法会好一点呢
class MyPhone {
constructor (action) {
this.action = action
}
//添加操作
getOperate (){
//清空当前的动作集合
this._currentstate = {};
//遍历添加动作
Object.keys(arguments).forEach((i) => this._currentstate[arguments[i]] = true)
return this;
}
//执行操作
userOperate(){
//当前动作集合中的动作依次执行
const actions = operates()
Object.keys(this._currentstate).forEach(
(k) => actions [k] && actions[k].apply(this)
)
return this;
}
operates() {
return {
playMusic: () => {}
pauseMusic: () => {}
readText: () => {}
showText: () => {}
}
}
}
const aPhone = new MyPhone()
aPhone.getOperate('playMusic', 'readText').userOperate()
如果一个复杂得容器需要同时完成多件事情,又不能大量的判断,就得使用 状态模式,单一得原则只是限制,使用正常得模式才能让项目更为完整
1. 有什么缺点
开发过程中,因为变化、升级和维护等原因需要对原有逻辑进行修改时,很有可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有功能新测试。
2, 怎么解决
我们应该尽量通过扩展实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
3. 具体一点呢
类、模块和函数应该对扩展开放,对修改关闭。模块应该尽量在不修改原代码的情况下进行扩展。
4. 核心:用抽象构建框架,用实现扩展细节。
- 3. 依赖倒转原则(DIP Dependence Inversion Principle):
概念
- 高层模块不应该依赖低层模块。两个都应该依赖抽象
- 抽象不应该依赖细节。细节应该依赖抽象。
举例:
先对以上三个原则再一次加深理解,以电脑主机为例:
如果电脑坏掉了,要排查问题,找到引起电脑不能开机的原因,根据键盘灯和显示器的反应,可以得出关于内存,电源是否出现问题。如果能进入bois界面,可以检查硬盘,显卡的问题。电脑硬件,就如单一职责原理,每个硬件做好自己的事,找到某个硬件出现问题,只需要换掉该硬件。
如果内存不够,导致游戏蓝屏卡顿,电脑主板支持多根内存条的插入。硬盘大小不够,也可以通过增加硬盘解决问题,这也是开发-封闭原则,可以扩展,但是不可修改。
上述电脑的元件基本都是插入在电脑主板上的。主板就可以看做是一个高层,他依赖与cpu,内存,硬盘,电源。
- 4. 里氏代换原则(LSP Liskov Substitution Principle):
概念
子类型必须能够替换掉它们的父类型
该原则的核心思想就是在程序当中,如果将一个父类对象替换成它的子类对象后,该程序不会发生异常。这也是该原则希望达到的一种理想状态。
注意事项
1. 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法
2. 我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,通过调用子类实例我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。
- 5. 迪米特法则(Law of Demeter)
概念
最少知识原则(LKP,Least Knowledge Principle),就是说一个对象应当对其它对象有尽可能少的了解,类与类之间的了解的越多,关系越密切,耦合度越大,当一个类发生改变时。还有一个类也可能发生变化。
含义
- 1. 类内部应该高内聚,设置对应的权限。有选择的暴露方法,这就是封装的奥秘。
- 2. 类的依赖关系尽量降低,保持简单和独立。降低耦合。
有些东西,能够适当的知道,知道的太多对你不好。
关系越复杂,人越不敢接近你。
要达到非常高的内修养。才干有非常好的表现。
举例
// 方式 一
class myRestaurant {
constructor(customer) {
this.customer = customer
}
getMenu() {}
chooseFoot() {}
getOrder() {}
giveMoney() {}
sendFoot() {}
}
const customer = new myRestaurant()
customer.getMenu()
customer.chooseFoot()
customer.getOrder()
customer.giveMoney()
// 方式 二
class myRestaurant {
constructor(customer) {
this.customer = customer
this.eat()
}
eat() {
this.getMenu() {}
this.chooseFoot() {}
this.getOrder() {}
this.giveMoney() {}
this.sendFoot() {}
}
}
const customer = new myRestaurant()
customer.eat()
// 方式 三
class myRestaurant {
constructor(customer) {
this.customer = customer
this.eat()
}
behavior() {
// 处理serve 函数... 可链式调用,每次函数体返回this 实例
}
serve() {
return {
getMenu: () =>{}
chooseFoot: () =>{}
getOrder: () =>{}
giveMoney: () =>{}
sendFoot: () =>{}
}
}
}
const customer = new myRestaurant()
customer.behavior()
我开了一家餐厅,从顾客开始获取菜单,选择食物,下单,付钱,得到食物,如果我们按照第一种方式,那么我们对外需要顾客做的事太多,对外暴露的细节太多,不好
所以我们采用方式二,顾客进入餐厅就是吃饭,就如套餐一样,不需要他知道太多,就只管吃,获取菜单,选择食物,下单,付钱,得到食物这一类的操作事餐厅内部的操作。
对于方式二,内部的耦合性还是太高,导致每个步骤的关联性太强,不拿到菜单就不能付钱,如果一个流程卡住,就会导致整个生产线停止,,所以针对第二种方式,我们拆解步骤,使用状态模式,这里还可以用其他的模式。
- 6. 接口隔离原则(ISP Interface Segregation Principle)
概念
A、客户端不应该依赖它不需要的接口。
B、类间的依赖关系应该建立在最小的接口上。
含义
这是接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface),但是“小”是有限度的,首先就是不能违反单一职责原则。高内聚就是提高接口、类、模块的处理能力,减少对外的交互。具体到接口隔离原则就是,要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本。
一个系统或系统内的模块之间必然会有耦合,有耦合就要有相互访问的接口(并不一定就是Java中定义的Interface,也可能是一个类或单纯的数据交换),我们设计时就需要为各个访问者(即客户端)定制服务,什么是定制服务?定制服务就是单独为一个个体提供优良的服务。我们在做系统设计时也需要考虑对系统之间或模块之间的接口采用定制服务。采用定制服务就必然有一个要求:只提供访问者需要的方法
接口的设计粒度越小,系统越灵活,这是不争的事实。但是,灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低所以接口设计一定要注意适度
class Func{
funcAll() {}
}
class B {
funcB() {
}
}
//A类通过接口B类,但只是会用到funcB方法
class A{
funcA() {
}
}