什么是设计模式?
一个模式就是一个可重用的方案,可应用于在软件设计中的常见问题。另一种解释就是一个我们如何解决问题的模板 – 那些可以在许多不同的情况里使用的模板。
设计模式分类
1. 创建型模式
单例模式
单例模式 (Singleton Pattern)又称为单体模式,保证一个类只有一个实例,并提供一个访问它的全局访问点
实现方式:
创建一个类,这个类包含一个方法,这个方法在没有对象存在的情况下,将会创建一个新的实例对象。如果对象存在,这个方法只是返回这个对象的引用。
实现方案:
- 懒汉式:用到的时候才创建(场景:不一定需要用到、创建代价大、延迟加载、需要快速启动系统)
- 饿汉式:系统启动时创建(场景:必定用到、临时创建影响响应速度)
- 多例:同一类的固定个数相同实例
代码实现
let ProxyCreateSingleton = (function(){
let instance;
return function(name) {
// 代理函数仅作管控单例
if (instance) {
return instance;
}
return instance = new Singleton(name);
}
})();
// 独立的Singleton类,处理对象实例
let Singleton = function(name) {
this.name = name;
}
Singleton.prototype.getName = function() {
console.log(this.name);
}
let Winner = new PeozyCreateSingleton('Winner');
let Looser = new PeozyCreateSingleton('Looser');
console.log(Winner === Looser); // true
console.log(Winner.getName()); // 'Winner'
console.log(Looser.getName()); // 'Winner'
复制代码
适用场景
- 引用第三方库(多次引用只会使用一个库引用,如 jQuery)
- 弹窗(登录框,信息提升框)
- 购物车 (一个用户只有一个购物车)
- 全局态管理 store (Vuex / Redux)
优缺点
- 优点:适用于单一对象,只生成一个对象实例,避免频繁创建和销毁实例,减少内存占用。
- 缺点:不适用动态扩展对象,或需创建多个相似对象的场景。
工厂方法模式
根据不同的输入返回不同类的实例,一般用来创建同一类对象
代码实现
constructor(name = '', viewPage = []) {
if(new.target === User) {
throw new Error('抽象类不能实例化!');
}
this.name = name;
this.viewPage = viewPage;
}
}
class UserFactory extends User {
constructor(name, viewPage) {
super(name, viewPage)
}
create(role) {
switch (role) {
case 'superAdmin':
return new UserFactory( '超级管理员', ['首页', '通讯录', '发现页', '应用数据', '权限管理'] );
break;
case 'admin':
return new UserFactory( '普通用户', ['首页', '通讯录', '发现页'] );
break;
case 'user':
return new UserFactory( '普通用户', ['首页', '通讯录', '发现页'] );
break;
default:
throw new Error('参数错误, 可选参数:superAdmin、admin、user')
}
}
}
let userFactory = new UserFactory();
let superAdmin = userFactory.create('superAdmin');
let admin = userFactory.create('admin');
let user = userFactory.create('user');
复制代码
适用场景
- 当我们的对象或者组件设置涉及到高程度级别的复杂度时。
- 当我们需要根据我们所在的环境方便的生成不同对象的实体时。
- 当我们在许多共享同一个属性的许多小型对象或组件上工作时。
- 当带有其它仅仅需要满足一种API约定(又名鸭式类型)的对象的组合对象工作时.这对于解耦来说是有用的。
优缺点
优点:
- 降低了代码耦合度,对象的生成交给子类去完成
- 实现了开放封闭原则 – 每次添加子产品 不需要修改原有代码
缺点:
- 增加了代码量,每个具体产品都需要一个具体工厂
- 当增加抽象产品 也就是添加一个其他产品族 需要修改工厂 违背OCP
抽象工厂模式
通过对类的工厂抽象使其业务用于对产品类簇的创建,而不是负责创建某一类产品的实例
代码实现
switch (type) {
case 'wechat':
return UserOfWechat;
break;
case 'qq':
return UserOfQq;
break;
case 'weibo':
return UserOfWeibo;
break;
default:
throw new Error('参数错误, 可选参数:superAdmin、admin、user')
}
}
let WechatUserClass = getAbstractUserFactory('wechat');
let QqUserClass = getAbstractUserFactory('qq');
let WeiboUserClass = getAbstractUserFactory('weibo');
let wechatUser = new WechatUserClass('微信');
let qqUser = new QqUserClass('QQ');
let weiboUser = new WeiboUserClass('微博');
复制代码
适用场景
一组实例都有相同的结构
优缺点
优点
- 代码解耦
- 实现多个产品族(相关联产品组成的家族),而工厂方法模式的单个产品,可以满足更多的生产需求
- 很好的满足OCP开放封闭原则
- 抽象工厂模式中我们可以定义实现不止一个接口,一个工厂也可以生成不止一个产品类 对于复杂对象的生产相当灵活易扩展
缺点
- 扩展产品族相当麻烦 而且扩展产品族会违反OCP,因为要修改所有的工厂,例如我们有电脑和鼠标组成的一个产品族,我们写完代码再去添加一个键盘就会很麻烦,看完下面代码就会理解了
- 由于抽象工厂模式是工厂方法模式的扩展 总体的来说 很笨重
建造者模式
将一个复杂对象的构建层与其表现层相互分离,同样的构建过程可采用不同的表示。
代码实现
var Human = function (param) {
this.skill = param && param.skill || '保密';
this.hobby = param && param.hobby || '保密';
}
Human.prototype = {
getSkill: function () {
return this.skill;
},
getHobby: function () {
return this.hobby;
}
}
// 实例化姓名类
var Named = function (name) {
var that = this;
// 构造器
// 构造函数解析姓名的姓与名
(function (name, that) {
that.wholeName = name;
if (name.indexOf(' ') > -1) {
that.FirstName = name.slice(0, name.indexOf(' '));
that.SecondName = name.slice(name.indexOf(' '));
}
})(name, that);
}
// 实例化职位类
var Work = function (work) {
let that = this;
(function (work, that) {
switch (work) {
case 'code':
that.work = '工程师';
that.workDescript = '每天都沉醉于编程';
break;
case 'UI':
case 'UE':
that.work = '设计师';
that.workDescript = '设计更似一种艺术';
break;
case 'teach':
that.work = '教师';
that.workDescript = '分享也是一种快乐';
break;
default:
that.work = work;
that.workDescript = '对不起,我们还不清楚你选择职位的相关描述'
break;
}
})(work, that)
}
// 更换期望的职位
Work.prototype.changeWork = function (work) {
this.work = work;
}
// 添加对工作的描述
Work.prototype.changeDescript = function (setence) {
this.workDescript = setence;
}
// 创建一位应聘者
var Person = function (name, work) {
var _person = new Human();
_person.name = new Named(name);
_person.work = new Work(work);
return _person;
}
// 测试代码
var person = new Person('小 明', 'code');
console.log(person.skill); // 保密
console.log(person.name.FirstName); // 小
console.log(person.work.work); // 工程师
console.log(person.work.workDescript);// 每天都沉醉于编程
person.work.changeDescript('更改一下职位描述');
console.log(person.work.workDescript); // 更改一下职位描述
复制代码
适用场景
- 相同的方法,不同的执行顺序,产生不一样的产品时
- 产品的组成部件类似,通过组装不同的组件获得不同产品时
- 创建时有很多必填参数需要验证。
- 创建时参数求值有先后顺序、相互依赖。
- 创建有很多步骤,全部成功才能创建对象。
优缺点
优点:
- 使用建造者模式可以使产品的构建流程和产品的表现分离
- 扩展性方便
- 更好的复用性
缺点:
- 建造者模式一般适用于产品之间组成部件类似的情况,如果产品之间差异性很大、复用性不高,那么不要使用建造者模式;
- 实例的创建增加了许多额外的结构,无疑增加了许多复杂度,如果对象粒度不大,那么我们最好直接创建对象;
2. 结构型模式
适配器模式
适配器模式(Adapter Pattern)又称包装器模式,将一个类(对象)的接口(方法、属性)转化为用户需要的另一个接口,解决类(对象)之间接口不兼容的问题。
主要功能是进行转换匹配,目的是复用已有的功能
代码实现
show: function () {
console.log('开始渲染谷歌地图');
}
};
const baiduMap = {
display: function () {
console.log('开始渲染百度地图');
}
};
const baiduMapAdapter = {
show: function(){
return baiduMap.display();
}
};
renderMap(googleMap); // 输出:开始渲染谷歌地图
renderMap(baiduMapAdapter); // 输出:开始渲染百度地图
复制代码
适用场景
当你想用已有对象的功能,却想修改它的接口时
优缺点
- 已有的功能如果只是接口不兼容,使用适配器适配已有功能,可以使原有逻辑得到更好的复用,有助于避免大规模改写现有代码;
- 可扩展性良好,在实现适配器功能的时候,可以调用自己开发的功能,从而方便地扩展系统的功能;
- 灵活性好,因为适配器并没有对原有对象的功能有所影响,如果不想使用适配器了,那么直接删掉即可,不会对使用原有对象的代码有影响;
桥接模式
桥接模式(Bridge Pattern)又称桥梁模式,将抽象部分与它的实现部分分离,使它们都可以独立地变化
代码实现
constructor(name) {
this.name = name
}
}
class Shape {
constructor(name, color) {
this.name = name
this.color = color
}
draw() {
console.log(`${this.color.name}-${this.name}`)
}
}
const red = new Color('red')
const yellow = new Color('yellow')
const blue = new Color('blue')
const line = new Shape('line', red)
const rectangle = new Shape('rectangle', yellow)
const circle = new Shape('circle', blue)
line.draw()
rectangle.draw()
circle.draw()
复制代码
适用场景
- 如果产品的部件有独立的变化维度,可以考虑桥接模式;
- 不希望使用继承,或因为多层次继承导致系统类的个数急剧增加的系统;
- 产品部件的粒度越细,部件复用的必要性越大,可以考虑桥接模式;
优缺点
优点:
- 分离了抽象和实现部分,将实现层(DOM 元素事件触发并执行具体修改逻辑)和抽象层( 元素外观、尺寸部分的修改函数)解耦,有利于分层;
- 提高了可扩展性,多个维度的部件自由组合,避免了类继承带来的强耦合关系,也减少了部件类的数量;
- 使用者不用关心细节的实现,可以方便快捷地进行使用;
缺点:
- 桥接模式要求两个部件没有耦合关系,否则无法独立地变化,因此要求正确的对系统变化的维度进行识别,使用范围存在局限性;
- 桥接模式的引入增加了系统复杂度;
装饰者模式
装饰者模式 (Decorator Pattern)又称装饰器模式,在不改变原对象的基础上,通过对其添加属性或方法来进行包装拓展,使得原有对象可以动态具有更多功能。
代码实现
// 数据上报
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('打开登录浮层');
}
var log = function () {
console.log('上报标签为: ' + this.getAttribute('tag'));
}
showLogin = showLogin.after(log); // 打开登录浮层之后上报数据
document.getElementById('button').onclick = showLogin;
复制代码
适用场景
- 如果不希望系统中增加很多子类
- 需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,这时采用装饰者模式可以很好实现;
- 当对象的功能要求可以动态地添加,也可以动态地撤销,可以考虑使用装饰者模式
优缺点
优点:
- 允许用户在不引起子类数量暴增的前提下动态地修饰对象,添加功能,装饰者和被装饰者之间松耦合,可维护性好;
- 被装饰者可以使用装饰者动态地增加和撤销功能,可以在运行时选择不同的装饰器,实现不同的功能,灵活性好
- 装饰者模式把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器只实现一个功能,可以给一个对象增加多个同样的装饰器,也可以把一个装饰器用来装饰不同的对象,有利于装饰器功能的复用;
- 可以通过选择不同的装饰者的组合,创造不同行为和功能的结合体,原有对象的代码无须改变,就可以使得原有对象的功能变得更强大和更多样化,符合开闭原则
缺点:
- 使用装饰者模式时会产生很多细粒度的装饰者对象,这些装饰者对象由于接口和功能的多样化导致系统复杂度增加,功能越复杂,需要的细粒度对象越多;
- 由于更大的灵活性,也就更容易出错,特别是对于多级装饰的场景,错误定位会更加繁琐;
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END