这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
Structural Design Patterns 结构型设计模式
关于结构型设计模式
结构型设计模式主要关注的是 对象组合
,也就是实例之间如何互相引用
结构型设计模式的分类
- Adapter 适配器模式
- Bridge 桥接模式
- Composite 组合模式
- Decorator 装饰者模式
- Facade 外观模式
- Flyweight 享元模式
- Proxy 代理模式
适配器模式
真实的案例
想象一下,我们在家中经常会用到插头,比如说手机充电器、电视机插头、电风扇插头、电饭锅插头等,举个例子,我们的电磁炉插头,一般都是三孔插头,某天,我们兴致勃勃,突发奇想想要吃火锅,巴拉巴拉一通搞完所有食材,然后将电磁炉放在桌子的正中央,结果发现我们的插座竟然不支持三孔,这个时候我们就很容易想要去找带有三孔插座的拖线板,然后拖线板的插头是两孔的,这样就能过满足我们的干饭需求了,这个时候,我们找到了拖线板A,A其实是一个整体,暴露出去一个接口(三孔插头),而拖线板A这里其实充当的是适配器的作用,也就是中间做了些包装,使得插座可以调用A暴露出来的接口
简而言之
适配器模式允许您将不兼容的对象包装在适配器中以使其与另一个类兼容
维基百科
在软件工程中,适配器模式是一种软件设计模式,它允许将现有类的接口用作另一个接口。 它通常用于使现有类与其他类一起工作,而无需修改其源代码
编程案例
我们来玩一个游戏,游戏中有一个猎人,然后他猎杀狮子
首先我们有一个狮子的接口,所有类型的狮子都会执行一个方法,比如说吼叫
class AfricanLion {
roar() {}
}
class AsianLion {
roar() {}
}
然后猎人有狩猎的方法,狩猎的过程中狮子可能会吼叫,而对于不同的狮子它们暴露出来的方法是一样的,都是吼叫,具体应该怎么吼,可以在每只狮子内部的 roar
方法中实现
class Hunter {
hunt(lion) {
// ... some code before
lion.roar();
// ... some code after
}
}
现在我们想在游戏中加入一只野狗,然后猎人也可以狩猎野狗,但是我们不能直接狩猎,因为野狗没有吼叫这个接口,野狗只会吠叫,所以为了使猎人狩猎的接口兼容,我们可以创建一个适配器
class WildDog {
bark() {
}
}
class WildDogAdapter {
constructor(dog) {
this.dog = dog;
}
roar() {
this.dog.bark();
}
}
现在我们可以将野狗加入我们的狩猎游戏中,通过调用 WildDogAdapter
const wildDog = new WildDog();
const wildDogAdapter = new WildDogAdapter(wildDog);
const hunter = new Hunter();
hunter.hunt(wildDogAdapter);
什么时候使用
当我们需要去适配第三方接口或者是封装一些旧接口的时候,可以让两个毫不相关的类一起运行,提高了类的复用,但是随着额外对象的创建,存在一定的开销,如果没有必要采用适配器模式,可以进行重构,否则需要完善好文档
桥接模式
真实的案例
假设你有一个包含很多个页面的网站,而且你还得允许用户更改网站的主题,你会怎么处理呢?为每个主题创建每个页面的多个副本吗?还是只是单独创建主题,根据用户的偏好来加载不同的页面呢?这个时候就用到了桥接模式
简而言之
桥接模式组合优于继承,实现的细节是从一个层次结构推送到另一个具有单独层次结构的对象中
维基百科
桥接模式是软件工程中使用的一种设计模式,旨在“将抽象与其实现分离,以便两者可以独立变化”
编程案例
从上面的例子中来实现我们的网页示例,这里我们有网页层次结构
class About {
constructor(theme) {
this.theme = theme;
}
getContent() {
return 'About page in ' + this.theme.getColor();
}
}
class Careers {
constructor(theme) {
this.theme = theme;
}
getContent() {
return 'Careers page in ' + this.theme.getColor();
}
}
然后我们有单独的主题结构
class DarkTheme {
getColor() {
return 'Dark Black';
}
}
class LightTheme {
getColor() {
return 'Off white';
}
}
class AquaTheme {
getColor() {
return 'Light blue';
}
}
现在我们可以将两者层次的组合在一起
const darkTheme = new DarkTheme();
const about = new About(darkTheme);
const careers = new Careers(darkTheme);
console.log(about.getContent()); // "About page in Dark Black"
console.log(careers.getContent()); // "Careers page in Dark Black"
什么时候使用
当你需要独立管理各组成成分的时候,把抽象化和实现化进行解耦,提高可扩充性,但是大量的类的添加将会导致开发成本的增加和性能的下降
组合模式
真实的案例
每个公司都是由员工组成的。 每个员工都有相同的特征,即有薪水,有一些责任,可能会或可能不会向某人报告,可能有也可能没有下属等。
简而言之
组合模式允许你以统一的方式处理单个对象
维基百科
在软件工程中,复合模式是一种分区设计模式。 组合模式描述了一组对象的处理方式与对象的单个实例相同。 复合的目的是将对象“组合”成树结构以表示部分-整体层次结构。 实现复合模式可以让你统一处理单个对象和组合
编程案例
以我们的员工为例。 这里我们有不同的员工类型
class Developer {
constructor(name, salary) {
this.name = name;
this.salary = salary;
}
getName() {
return this.name;
}
setSalary(salary) {
this.salary = salary;
}
getSalary() {
return this.salary;
}
getRoles() {
return this.roles;
}
develop() {
/* */
}
}
class Designer {
constructor(name, salary) {
this.name = name;
this.salary = salary;
}
getName() {
return this.name;
}
setSalary(salary) {
this.salary = salary;
}
getSalary() {
return this.salary;
}
getRoles() {
return this.roles;
}
design() {
/* */
}
}
然后我们有一个由几种不同类型的员工组成的组织
class Organization {
constructor() {
this.employees = [];
}
addEmployee(employee) {
this.employees.push(employee);
}
getNetSalaries() {
let netSalary = 0;
this.employees.forEach(employee => {
netSalary += employee.getSalary();
});
return netSalary;
}
}
然后我们可以这样使用
const john = new Developer('John Doe', 12000);
const jane = new Designer('Jane', 10000);
const organization = new Organization();
organization.addEmployee(john);
organization.addEmployee(jane);
console.log('总薪水: ', organization.getNetSalaries()); // 总薪水: 22000
什么时候使用
当你需要表示对象-整体层次结构,希望用户忽略组合对象和单个对象的不同,统一的使用组合结构中的所有对象方法,反之,我们如果创建太多对象,系统的性能反而会下降
装饰器模式
真实的案例
想象一下,您经营一家提供多种服务的汽车服务店。 现在你如何计算要收取的账单? 您选择一项服务并动态地不断添加所提供服务的价格,直到获得最终成本。 在这里,每种类型的服务都是一个装饰器
简而言之
装饰器模式允许您通过将对象包装在装饰器类的对象中,在运行时动态更改对象的行为
维基百科
在面向对象的编程中,装饰器模式是一种设计模式,它允许将行为静态或动态地添加到单个对象中,而不会影响来自同一类的其他对象的行为。 装饰器模式对于遵守单一职责原则通常很有用,因为它允许在具有独特关注领域的类之间划分功能
编程案例
让我们以咖啡为例。 首先我们有一个简单的coffee
来实现 coffee
接口
class SimpleCoffee {
getCost() {
return 10;
}
getDescription() {
return 'Simple coffee';
}
}
我们希望使代码可扩展,以便在需要时允许选项对其进行修改。 让我们做一些附加功能(装饰器)
class MilkCoffee {
// 牛奶咖啡
constructor(coffee) {
this.coffee = coffee;
}
getCost() {
return this.coffee.getCost() + 2;
}
getDescription() {
return this.coffee.getDescription() + ', milk';
}
}
class WhipCoffee {
// 手磨咖啡
constructor(coffee) {
this.coffee = coffee;
}
getCost() {
return this.coffee.getCost() + 5;
}
getDescription() {
return this.coffee.getDescription() + ', whip';
}
}
class VanillaCoffee {
// 香草咖啡
constructor(coffee) {
this.coffee = coffee;
}
getCost() {
return this.coffee.getCost() + 3;
}
getDescription() {
return this.coffee.getDescription() + ', vanilla';
}
}
现在让我们泡杯咖啡
let someCoffee;
someCoffee = new SimpleCoffee();
console.log(someCoffee.getCost()); // 10
console.log(someCoffee.getDescription()); // Simple Coffee
someCoffee = new MilkCoffee(someCoffee);
console.log(someCoffee.getCost()); // 12
console.log(someCoffee.getDescription()); // Simple Coffee, milk
someCoffee = new WhipCoffee(someCoffee);
console.log(someCoffee.getCost()); // 17
console.log(someCoffee.getDescription()); // Simple Coffee, milk, whip
someCoffee = new VanillaCoffee(someCoffee);
console.log(someCoffee.getCost()); // 20
console.log(someCoffee.getDescription()); // Simple Coffee, milk, whip, vanilla
什么时候使用
当你需要动态地给某个对象添加一些额外的职责时,装饰器模式是一种实现继承的替代方案,在不改变原对象的基础上,通过对其进行包装扩展,使原有对象可以满足用户的更复杂需求,而不会影响从这个类中派生的其他对象,但是多层的装饰会比较复杂
外观模式
真实的案例
你是怎么打开电脑的? “按下电源按钮” 你说! 这就是你所深信不疑的,因为您使用的是计算机在外部提供的开机接口,在内部它必须做很多事情才能实现。 这个复杂子系统的简单接口是一个外观模式
简而言之
外观模式为复杂的子系统提供了一个简化的接口
维基百科
外观是一个对象,它为更大的代码结构体(例如类库)提供简化的接口
编程案例
以上面的计算机为例, 这里有一个 Computer
类
class Computer {
getElectricShock() {
console.log('Ouch!');
}
makeSound() {
console.log('Beep beep!');
}
showLoadingScreen() {
console.log('Loading..');
}
bam() {
console.log('Ready to be used!');
}
closeEverything() {
console.log('Bup bup bup buzzzz!');
}
sooth() {
console.log('Zzzzz');
}
pullCurrent() {
console.log('Haaah!');
}
}
然后我们来定义 Computer
的外观
class ComputerFacade {
constructor(computer) {
this.computer = computer;
}
turnOn() {
this.computer.getElectricShock();
this.computer.makeSound();
this.computer.showLoadingScreen();
this.computer.bam();
}
turnOff() {
this.computer.closeEverything();
this.computer.pullCurrent();
this.computer.sooth();
}
}
现在让我们使用外观提供的接口
const computer = new ComputerFacade(new Computer());
computer.turnOn();
// Ouch!
// Beep beep!
// Loading..
// Ready to be used!
computer.turnOff();
// Bup bup bup buzzz!
// Haaah!
// Zzzzz
什么时候使用
经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观Facade,增加外观Facade可以提供一个简单的接口,减少子系统之间的依赖,提高了灵活性和安全性,但是不符合开闭原则,继承和重写都比较复杂
享元模式
真实的案例
你有没有从某个摊位喝过早茶? 他们通常会制作很多份的早茶,所以一般都会一锅煮,给你来一杯茶后,并将其余的留给任何其他客户,以节省资源,例如 空气等。享元模式就是关于共享的
简而言之
它用于通过尽可能多地与相似对象共享来最小化内存使用或计算开销
维基百科
在计算机编程中,享元是一种软件设计模式。 享元是一种通过与其他类似对象共享尽可能多的数据来最小化内存使用的对象,当简单的重复表示会使用不可接受的内存量时,它是一种使用大量对象的方法
编程案例
以上面我们喝茶为例子, 首先我们有 Tea
类和 TeaMaker
制茶人
class KarakTea {
}
class TeaMaker {
constructor() {
this.availableTea = {};
}
make(preference) {
this.availableTea[preference] = this.availableTea[preference] || (new KarakTea());
return this.availableTea[preference];
}
}
然后我们有创建订单以及为客户服务的 TeaShop
class TeaShop {
constructor(teaMaker) {
this.teaMaker = teaMaker;
this.orders = [];
}
takeOrder(teaType, table) {
this.orders[table] = this.teaMaker.make(teaType);
}
serve() {
this.orders.forEach((order, index) => {
console.log('Serving tea to table#' + index);
});
}
}
现在我们可以这样使用,此时如果你的 teaType
没有改变的话,使用的就是现有的创建的 KarakTea
实例
const teaMaker = new TeaMaker();
const shop = new TeaShop(teaMaker);
shop.takeOrder('less sugar', 1);
shop.takeOrder('more milk', 2);
shop.takeOrder('without sugar', 5);
shop.serve();
// Serving tea to table# 1
// Serving tea to table# 2
// Serving tea to table# 5
什么时候使用
运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用,如果一个应用程序使用了大量的对象,而这些大量的对象造成了很大的存储开销时就应该考虑使用享元模式,提高效率,但是需要注意需要分离出内部和外部的状态,且外部状态具有固有化的性质
代理模式
真实的案例
您是否曾经使用门禁卡通过门? 打开那扇门有多种选择,即可以使用门禁卡或密码或指纹或钥匙打开它。 这扇门的主要功能是打开,但在它上面添加了一个代理来添加一些功能。 让我使用下面的代码示例更好地解释它
简而言之
使用代理模式,一个类代表另一个类的功能
维基百科
代理,就其最一般的形式而言,是一个充当其他事物接口的类。 代理是一个包装器或代理对象,你调用它来访问幕后的真实服务对象。 代理的使用可以简单地转发到真实对象,或者可以提供额外的逻辑。 在代理中可以提供额外的功能,例如当真实对象上的操作是资源密集型时缓存,或者在调用真实对象上的操作之前检查先决条件
编程案例
以上面开始我们开门为例, 首先,我们有 Door
类 和 门上的一些方法
class LabDoor {
open() {
console.log('Opening lab door');
}
close() {
console.log('Closing the lab door');
}
}
然后我们有一个代理来保护我们想要打开的门
class Security {
constructor(door) {
this.door = door;
}
open(password) {
if (this.authenticate(password)) {
this.door.open();
} else {
console.log('Oh no! password failed!');
}
}
authenticate(password) {
return password === 'qwerdf';
}
close() {
this.door.close();
}
}
现在我们可以这样使用
const door = new Security(new LabDoor());
door.open('invalid'); // Oh no! password failed!
door.open('qwerdf'); // Opening lab door
door.close(); // Closing lab door
什么时候使用
当我们需要在到达目标对象之前,做一些额外的逻辑,可以考虑使用代理模式,代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用,代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则,但是代理模式由于不是直接访问,所以在时间上存在一定的开销,最直接的例子就是前端在请求后端接口时中间会有一层转发的动作,主要目的是为了保护后端接口不被暴露
参考资料
- Javascript 设计模式
- 《Javascript设计模式——张容铭》
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!