前言
设计模式的六大原则,就像武侠小说中的内功。只有真正深厚的内功,使用出的招式才能威力无比。
而 23 种设计模式,就是武功招式。要练到出神入化,形成肌肉记忆,后续就可不拘泥于具体形式,达到无招胜有招的地步。
当看过几遍设计模式之后,就发现很多 UML 类图甚至是完全一致的,区分他们的关键就在于意图。只要你的意图不一致,哪怕代码完全一样,也是不同的设计模式。
谨记:所有的设计模式都会增加系统复杂度,所以可以不用,但是不能不会。
单一职责模式
最重要但是也最难以说清的模式。最粗暴的理解就是要短小,无论是方法、类。
- 如何理解业务,应该将其拆分和聚合到什么程度。
- 如何判断变化的维度,如果是在
A维度变化,你在B维度增加了扩展性,就是过度设计。
以上两点需要结合过往的经验以及当前的业务,这些持之以恒的积累才是进阶真正的不二法门。
BAD:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
GOOD:
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
上面的代码很简单明了的说明了单一模式应该怎么做。
但有一个问题就是我的这个类真的只有上面这几行代码,那么我将其拆出是不是徒增复杂度呢?
开闭原则
23 种设计模式绝大部分都是为了这个原则服务。问题的关键还是找到变化。
- 分层:将我们真正想完成的事情的流程抽象出来,将其中可能发生的变化,交给外部控制,这样就可以避免改动底层代码
BAD:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === "ajaxAdapter") {
return makeAjaxCall(url).then(response => {
// transform response and return
});
} else if (this.adapter.name === "nodeAdapter") {
return makeHttpCall(url).then(response => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}
GOOD:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
request(url) {
// request and return promise
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
request(url) {
// request and return promise
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then(response => {
// transform response and return
});
}
}
上述终极目的是通过网络获取数据。而不同的平台可能会有不同的请求方式,他就是变化的点,我们就需要将其抽象出来。
上述代码有一个问题就在于每个调用 HttpRequester 的地方都得 new Adapter(),我们可以通过工厂模式来解决这个问题。
里氏替换原则
这条规则是我们在使用继承时应当遵守的。
子类永远不能修改父类的行为,只可以增强。也就是将所有的子类,替换为父类并不会对程序的结果造成影响。
BAD:
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles) {
rectangles.forEach(rectangle => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
GOOD:
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
getArea() {
return this.length * this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach(shape => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
接口隔离原则
可以理解为接口的单一职责原则。
Bad:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.settings.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
animationModule() {} // Most of the time, we won't need to animate when traversing.
// ...
});
GOOD:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}
setupOptions() {
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
options: {
animationModule() {}
}
});
依赖倒置原则
简单来说,A 类依赖实现了 IB 接口的类 B,那么这个时候应该由外部传入,而不应该直接在内部直接初始化一个。而如果初始化一个默认值,则是允许的。
class InventoryRequester {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
// BAD: We have created a dependency on a specific request implementation.
// We should just have requestItems depend on a request method: `request`
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();
GOOD:
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ["WS"];
}
requestItem(item) {
// ...
}
}
// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
const inventoryTracker = new InventoryTracker(
["apples", "bananas"],
new InventoryRequesterV2()
);
inventoryTracker.requestItems();
迪米特法则/最小知道原则
就是要把内部的复杂度隐藏起来,对外部只提供需要的接口。火车残骸代码就是典型的错误。设计模式中的门面模式就是他的最佳实践。