代码整洁之道——5、SOLID

面向对象编程,五大原则:(这里只讲到一小部分,深入理解需要单独看设计模式)

  1. The Single Responsibility Principle(单一职责 SRP)
  2. The Open/Closed Principle(开闭原则 OCP)
  3. The Liskov Substitution Principle(里氏替换原则 LSP)
  4. The Interface Segregation Principle(接口分离原则 ISP)
  5. The Dependency Inversion Principle(依赖反转原则 DIP)

一、S 单一职责原则

正如代码整洁之道所述:“永远不要有超过一个理由去改变一个类”。给一个类很多功能,类似于你只能带一个行李箱上飞机。这样做的问题是,你的类不是高内聚,并且将会有很多理由要去改变这个类。减少改变一个类的次数是很重要的,因为一个类有多个函数,你修改了其中一部分,将很难搞清楚会影响代码库中的哪些其他地方。

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()) {
      // ...
    }
  }
}

二、开闭原则

正如Bertrand Meyer所说的,软件整体(类、模块、函数等)都应该都扩展开放,对修改关闭。这是什么意思呢?这个原则基本阐述了,在不改变现有代码的基础上你应该允许用户增加新功能

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 === 'httpNodeAdapter') {
      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
    });
  }
}

三、里氏替换原则

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但是、不能改变父类原有功能。(这句话不是翻译的原文)

最好的解释就是,你有一个父类和一个子类,那么子类和基类可以互换使用且不发生错误。这可能仍然很迷惑,所以我们看下矩形和正方形的经典例子。数学角度来讲,正方形是矩形,但是你用“is-a”的关系通过继承来实现,你很快就会遇到麻烦。

Demo

 1 Bad:
 2 class Rectangle {
 3   constructor() {
 4     this.width = 0;
 5     this.height = 0;
 6   }
 7 
 8   setColor(color) {
 9     // ...
10   }
11 
12   render(area) {
13     // ...
14   }
15 
16   setWidth(width) {
17     this.width = width;
18   }
19 
20   setHeight(height) {
21     this.height = height;
22   }
23 
24   getArea() {
25     return this.width * this.height;
26   }
27 }
28 
29 class Square extends Rectangle {
30   setWidth(width) {
31     this.width = width;
32     this.height = width;
33   }
34 
35 //正方形继承了矩形这个类,但是这里重写了父类中的方法,setWidth同理
36   setHeight(height) {
37     this.width = height;
38     this.height = height;
39   }
40 }
41 
42 function renderLargeRectangles(rectangles) {
43   rectangles.forEach((rectangle) => {
44     rectangle.setWidth(4);
45     rectangle.setHeight(5);
46     const area = rectangle.getArea(); // 应该返回20,但是返回的却是25
47     rectangle.render(area);
48   });
49 }
50 
51 const rectangles = [new Rectangle(), new Rectangle(), new Square()];
52 renderLargeRectangles(rectangles);
53 
54 Good:
55 //Rectangle和Square都继承了Shape,各自有自己的getArea方法互不影响
56 class Shape {
57   setColor(color) {
58     // ...
59   }
60 
61   render(area) {
62     // ...
63   }
64 }
65 
66 class Rectangle extends Shape {
67   constructor(width, height) {
68     super();
69     this.width = width;
70     this.height = height;
71   }
72 
73   getArea() {
74     return this.width * this.height;
75   }
76 }
77 
78 class Square extends Shape {
79   constructor(length) {
80     super();
81     this.length = length;
82   }
83 
84   getArea() {
85     return this.length * this.length;
86   }
87 }
88 
89 function renderLargeShapes(shapes) {
90   shapes.forEach((shape) => {
91     const area = shape.getArea();
92     shape.render(area);
93   });
94 }
95 
96 const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
97 renderLargeShapes(shapes);

四、接口分离原则(ISP)

JS没有接口所以,这个原则用起来不像其他原则一样严格。但是,对于js这种缺少类型的语言仍然很重要。

ISP原则指出:“客户端不应该强制依赖他们用不到的接口”。因为JS是弱类型语言,接口对它来说是模糊的。

在JS中,类需要大量的配置对象可以很好的说明这个原则。不需要客户端设置大量的选项是有好处的,因为很多时候,他们不需要所有的配置。让他们可选,有助于避免拥有一个很大的接口。

Bad:
class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.setup();
  }

  setup() {
    this.rootNode = this.settings.rootNode;
    this.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() {}
  }
});

五、依赖反转原则(DIP)

这个原则说明了两件重要的事情:

1、高级模块不应依赖于低级模块,但两者都需要依赖于抽象。

2、抽象不应该依赖于具体实现。具体实现应该依赖于抽象。

起初,这个很难理解,但是如果你用过AngularJs,你已经通过依赖注入看到过这个原则。虽然他们不是同一个概念,依赖反转原则让高级模块原理低级模块及他们的配置。耦合是一个很差的开发模式,因为它使得代码难以重构。

如上所述,JS没有接口,所以抽象依赖于隐式契约。这说明,一个对象的方法和类直接暴露给其他方法和类。在下面的例子中,隐式契约就是InventoryTracker 的任何Request模块将会有一个requestItems方法。

Bad:
class InventoryRequester {
  constructor() {
    this.REQ_METHODS = ['HTTP'];
  }

  requestItem(item) {
    // ...
  }
}

class InventoryTracker {
  constructor(items) {
    this.items = items;

//我们创建了一个具体请求实现的依赖。我们应该只有requestItem依赖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) {
    // ...
  }
}

//通过外部创建依赖并注入,我们可以轻松地用一个新的websockets,替换我们的请求模块
const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
inventoryTracker.requestItems();

 

posted on 2017-07-26 21:04  小小驰  阅读(274)  评论(0编辑  收藏  举报