面向对象编程,五大原则:(这里只讲到一小部分,深入理解需要单独看设计模式)
- The Single Responsibility Principle(单一职责 SRP)
- The Open/Closed Principle(开闭原则 OCP)
- The Liskov Substitution Principle(里氏替换原则 LSP)
- The Interface Segregation Principle(接口分离原则 ISP)
- 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();