什么是设计模式
设计模式是一种应对多场景或者复杂的业务场景的一种工具,有利于代码的复用/维护,系统模块的组织和设计的沟通
设计模式五大原则
开闭原则
开闭原则(OCP: open closed principle) - 对拓展开放、对修改关闭
开闭原则的设计思路是:在我们系统已有的场景下,对于需要拓展的功能进行开放,拒绝直接的功能修改
exm:
假如现在有PUBG和LOL两个游戏,我们需要在LOL弹出一个充值折扣的功能,PUBG需要一个高亮的功能(功能随意,只是为了demo的理解而写)
首先看一个一下就就能想到的万能if写法
if(game === 'PUBG') {
// 高亮
} else {
// ……
}
// event
if(game === 'LOL') {
// 弹出折扣框
} else {
// 付款
}
这种写法虽然很好理解,但对于代码维护和可复用性来说无疑是非常差的,那么针对于开闭原则,可以做以下改造
class Game {
//基础类 进行基础方法定义
constructor (name) {
this.name = name;
}
setColor(){
console.log('color');
}
openDialog(){
console.log('付款');
}
}
class LOL extends Game {
openDialog(){
//我需要基础的方法的部分功能,但有一些又不能满足我的需求,于是我给他重写了
console.log('折扣');
}
}
class PUBG extends Game {
openDialog(){
console.log('付款');
}
setColor(){
console.log('高亮');
}
}
class DNF extends Game {
/**
* 假如以后我又有一款游戏,这个游戏只需要基础的模式即可,那么我只需要创造出来
* 就自带了基础的功能,如果我想要添加新的功能,直接在这个游戏类里面进行添加/修改
*/
}
这个例子就说明了OCP的设计思想,对于一个系统/模块,在很多情况下存在同样功能的时候,统一管理核心功能模块并进行维护即可,对于个别派生出来的模块需要有别的功能,添加/修改即可,不会影响到核心
单一职责原则
单一职责原则(SRP) - 通过解耦让每一个职责更加的独立,职责单一,互不重叠
exm: 要在游戏里面有一个弹框的功能,以下是基础类
class PUBGManager {
openDialog() {
// 弹框
// 计算金额
setPrice();
}
}
const game = new PUBGManager();
game.openDialog(); // 弹框 < = > 计算金额 两个模块耦合
很明显,这样的写法达到功能需求是可以的,但是如果到了code review的时候,不难发现,弹框和计算金额两个模块耦合了,那么如何进行重构/优化呢?
// game库 (前置功能,拿到游戏的管理器)
function gameManager(game) {
return `${
game}Manager`;
}
// gameManager.js - 游戏管理器
class PUBGManager {
//通过命令修改价格
constructor(command){
//command可以理解为后续拿到的金额设置模块的一个实例
//也是一种设计模式(命令模式),我的操作对象不是元素/模块本身,而是命令
this.command = command;
}
openDialog(price){
this.command.setPrice(price);
}
}
// optManager.js - 底层库
class PriceManager {
setPrice(price){
//配置金额
}
}
// main.js
const exe = new PriceManager();
const game = new PUBGManager(exe);
game.openDialog(15);
// game.setPrice(10); // 若需求需要直接调用可增加该模块
这样做看起来就没有那么耦合了,因为我把功能抽离了来,游戏也成了统一的管理,每一个文件管理一个大类,而每一个模块就做专一的一个小事,从而达到了解耦的目的
依赖倒置原则
依赖倒置原则(DIP):上层不应依赖底层实现,也就是我们的设计应该依赖于接口的抽象,而不依赖于具体的实现
exm 需要一个分享功能,老规矩,先来一个反面教材
// 分享功能
class Store {
constructor() {
this.share = new Share();
}
}
class Share {
shareTo() {
// 分享到不同平台
}
}
const store = new Store();
store.share.shareTo('wx');
//这个时候需要增加一个功能
// 评分功能
class Store {
constructor() {
this.share = new Share();
this.rate = new Rate();//问题就在这里
}
}
class Share {
shareTo() {
// 分享到不同平台
}
}
class Rate {
star(stars) {
// 评分
}
}
const store = new Store();
store.rate.stars('5');
这样固然是实现了功能,那么假如我现在有一个大的版本迭代呢,需求要加很多功能模块,那我就需要在constructor里面去新增挂载,显然,这样的设计是不好的,那么正面教材又来了
// 目标: 暴露挂载 => 动态挂载
class Rate {
init(store) {
store.rate = this;
}
store(stars) {
// 评分
}
}
class Store {
// 维护模块名单
static modules = new Map();
constructor() {
// 遍历名单做初始化挂载
for (let module of Store.modules.values()) {
module.init(this);
}
}
// 注入功能模块
static inject(module) {
Store.modules.set(module.constructor.name, module);
}
}
class Share {
init(store) {
store.share = this;
}
shareTo(platform) {
// 分享到不同平台
}
}
// 依次注册完所有模块
const rate = new Rate();
Store.inject(rate);
// 初始化商城
const store = new Store();
store.rate.star(4);
这样就实现了一个动态挂载的功能,实例在新建的时候就已经有了所需的功能,有了功能其实只需要调用游戏库里面注入的功能的方法,就可以把模块挂载到游戏库里面了
接口隔离原则
假设一个场景,有一个接口有我们需要的某个或者某几个功能,但是我们每次run都会跑接口所有的功能模块,这样的接口一般称为胖接口,那么接口隔离原则即是对接口进行分组管理,拆分模块
接口隔离原则(ISP):多个专业的接口比单个胖接口好用
exm: 现在我已经可以开发游戏了,但是需要实现游戏中台 - 快速生产游戏
class Game {
constructor(name) {
this.name = name;
}
run() {
// 跑
}
shot() {
// 开枪
}
mega() {
// 开大
}
}
class PUGB extends Game {
constructor() {
// pubg constructor
}
}
class LOL extends Game {
constructor() {
// lol constructor
}
}
pubg = new PUBG('pubg');
pubg.run();
pubg.shot();
pubg.mega();
那么这个例子一看就看出毛病了:谁家的PUBG还能开大?那么接下来就是改造环节了
// 用多个接口替代他,每个接口服务于一个子模块
// 瘦身
class Game {
constructor(name) {
this.name = name;
}
run() {
// 跑
}
}
class FPS {
}
class MOBA {
}
class PUGB extends Game {
constructor() {
// pubg constructor
}
shot() {
}
}
class LOL extends Game {
constructor() {
// lol constructor
}
mega() {
}
}
那么这样的话就实现了每个接口/功能模块专注于做自己的事情
里氏替换原则
里氏替换原则(LSP:the Lxxxx substitution principle):子类能够覆盖父类,父类能够出现的地方子类就能出现,子类可以扩展,不能改变
exm: 现在就是要把游戏分为pc端和mobile端来做
class Game {
start() {
// 开机逻辑
}
shutdown() {
// 关机
}
play() {
// 游戏
}
}
const game = new Game();
game.play();
// sprint 2
class MobileGame extends Game {
tombStone() {
// tombStone
}
play() {
// 移动端游戏
//这里的play覆盖掉了父类的play
}
}
const mobile = new MobileGame();
mobile.play();
问题来了MobileGame 的play实际上覆盖掉了父类的play,这样直接去覆盖属性其实是不好的,那么下面就是基于里氏替换原则的改进
class Game {
start() {
// 开机逻辑
console.log('start');
}
shutdown() {
// 关机
console.log('shutdown');
}
}
class MobileGame extends Game {
tombStone() {
console.log('tombStone');
}
play() {
console.log('playMobileGame');
}
}
class PC extends Game {
speed() {
console.log('speed');
}
play() {
console.log('playPCGAME');
}
}
不难发现,父类已经不再具有play的方法了,我把play下发到子类去进行自我实现,这样也就体现出了“子类能够覆盖父类,父类能够出现的地方子类就能出现”的功能,里氏替换原则更多的关注的是代码的可维护性,可长期迭代性
最后
以上就是设计模式的五大原则,下机