整潔代碼:JavaScript 當中的面向對象設計原則(S.O.L.I.D)
GitHub 總之有很多 Code Example 案例的倉庫來教你如何正確寫出好代碼,諸多 Markdown 寫手以 BAD/GOOD 兩種代碼作為示范,輔以一些敘述和注釋作為說明,清晰易懂。我就準備來翻譯最近看到的 JavaScript 整潔代碼中 Classes 設計原則這一段,進一步加深對面向對象設計原則的理解,而不只是 Java 世界。
JavaScript Classes
Single Responsibility Principle (SRP) | 單一職責原則
As stated in Clean Code, “There should never be more than one reason for a class to change”. It’s tempting to jam-pack a class with a lot of functionality, like when you can only take one suitcase on your flight. The issue with this is that your class won’t be conceptually cohesive and it will give it many reasons to change. Minimizing the amount of times you need to change a class is important. It’s important because if too much functionality is in one class and you modify a piece of it, it can be difficult to understand how that will affect other dependent modules in your codebase.
如《整潔代碼》中所述,「不應該有一個以上的理由去修改某個類」。我們往往會傾向于往一個類里面塞入過多的功能,就像你只能往航班上攜帶唯一一個行李箱的時候。這里的問題在于,這個類從概念上來說不再是內聚的,從而導致了將來可能有很多理由會去修改它。盡可能少地去修改某個類非常重要,因為如果在一個類里面包含了過多的功能,那么當你修改其中的某一部分,就會很難理解你所做的修改將如何影響代碼庫當中的其他依賴模塊。
Bad:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials(user)) {
// ...
}
}
verifyCredentials(user) {
// ...
}
}
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()) {
// ...
}
}
}
Open/Closed Principle (OCP) | 開放封閉原則
As stated by Bertrand Meyer, “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” What does that mean though? This principle basically states that you should allow users to extend the functionality of your module without having to open up the .js source code file and manually manipulate it.
正如 Bertrand Meyer 所言,「軟件實體(類、模塊、函數等等)應該對于擴展是開放的,而對于修改是關閉的」。換句話說,該原則的基本意思就是你應該讓用戶在擴展你的模塊功能時,沒有必要去打開 .js 源文件并且手動修改源代碼。
Bad:
class AjaxRequester {
constructor() {
// What if we wanted another HTTP Method, like DELETE? We would have to
// open this file up and modify this and put it in manually.
this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
}
get(url) {
// ...
}
}
Good:
class AjaxRequester {
constructor() {
this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
}
get(url) {
// ...
}
addHTTPMethod(method) {
this.HTTP_METHODS.push(method);
}
}
Liskov Substitution Principle (LSP) | 里氏替換原則
This is a scary term for a very simple concept. It’s formally defined as “If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).” That’s an even scarier definition.
這個原則聽起來有點兒拗口但是概念非常簡單。正式的定義就是「如果 S 為 T 的子類型,那么 T 類型的對象可以被 S 類型的對象所替換(也就是說類型 S 的對象可以替換類型 T 的對象),而不會改變該程序的任何期望的屬性(正確性,執行的任務等)」。這是一個更拗口的定義。
The best explanation for this is if you have a parent class and a child class, then the base class and child class can be used interchangeably without getting incorrect results. This might still be confusing, so let’s take a look at the classic Square-Rectangle example. Mathematically, a square is a rectangle, but if you model it using the “is-a” relationship via inheritance, you quickly get into trouble.
形象而言,我們創建的父類與其子類應當可交換地使用而不會引起異常。這可能依然會使人困惑, 所以讓我們來看看這個經典的 Square-Rectangle 這個例子。從數學上來說,一個 Square 也是 一個 Rectangle,但是如果你通過繼承使用 “is-a” 的關系對其進行建模,你很快就會遇到問題。
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 {
constructor() {
super();
}
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);
let area = rectangle.getArea(); // BAD: Will return 25 for Square. Should be 20.
rectangle.render(area);
})
}
let rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
Good:
class Shape {
constructor() {}
setColor(color) {
// ...
}
render(area) {
// ...
}
}
class Rectangle extends Shape {
constructor() {
super();
this.width = 0;
this.height = 0;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width \* this.height;
}
}
class Square extends Shape {
constructor() {
super();
this.length = 0;
}
setLength(length) {
this.length = length;
}
getArea() {
return this.length \* this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach((shape) => {
switch (shape.constructor.name) {
case 'Square':
shape.setLength(5);
case 'Rectangle':
shape.setWidth(4);
shape.setHeight(5);
}
let area = shape.getArea();
shape.render(area);
})
}
let shapes = [new Rectangle(), new Rectangle(), new Square()];
renderLargeShapes(shapes);
Interface Segregation Principle (ISP) | 接口隔離原則
JavaScript doesn’t have interfaces so this principle doesn’t apply as strictly as others. However, it’s important and relevant even with JavaScript’s lack of type system.
JavaScript 語言本身并不包含對于接口語法的支持,因此也無法像其他語言那樣達到嚴格限制的程度。不過鑒于 JavaScript 本身類型系統的缺失,遵循接口隔離原則還是非常重要的。
ISP states that “Clients should not be forced to depend upon interfaces that they do not use.” Interfaces are implicit contracts in JavaScript because of duck typing.
ISP 的表述是「不應該強制客戶端去依賴于他們不需要的接口」,由于 JavaScript 的「鴨子類型」,JavaScript 當中的接口也只是一種隱性的契約。
A good example to look at that demonstrates this principle in JavaScript is for classes that require large settings objects. Not requiring clients to setup huge amounts of options is beneficial, because most of the time they won’t need all of the settings. Making them optional helps prevent having a “fat interface”.
這一點在 JavaScript 中較為典型的例子就是那些需要大量配置信息的類。其實使用者并不需要去關心每一個配置項,不強制他們設置大量的選項能夠節省大量的時間,保持設置選項可選能夠有助于防止「胖接口」。
Bad:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}
traverse() {
// ...
}
}
let $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
animationModule: function() {} // 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() {
// ...
}
}
let $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
options: {
animationModule: function() {}
}
});
Dependency Inversion Principle (DIP) | 依賴反轉原則
This principle states two essential things:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend upon details. Details should depend on abstractions.
這個原則主要闡述了兩件重要的事情:
- 上層模塊不需要依賴下層模塊,兩者依賴于抽象。
- 抽象不應該依賴于細節。細節應當依賴于抽象。
This can be hard to understand at first, but if you’ve worked with Angular.js, you’ve seen an implementation of this principle in the form of Dependency Injection (DI). While they are not identical concepts, DIP keeps high-level modules from knowing the details of its low-level modules and setting them up. It can accomplish this through DI. A huge benefit of this is that it reduces the coupling between modules. Coupling is a very bad development pattern because it makes your code hard to refactor.
最開始可能很難理解,但是如果你曾經用過 Angular.js,你就已經見過這個原則的一種實現了,依賴注入(DI)就是其中一種形式。但是他們不是完全相同的概念,DIP 可以避免上層模塊知道你的下層模塊的實現細節和具體設置,這可以通過 DI 來達成目的。一個顯著的好處就是減少了模塊之間的耦合,耦合是非常壞的一種開發模式,因為它會使得你的代碼難以重構。
As stated previously, JavaScript doesn’t have interfaces so the abstractions that are depended upon are implicit contracts. That is to say, the methods and properties that an object/class exposes to another object/class. In the example below, the implicit contract is that any Request module for an InventoryTracker will have a requestItems method.
就像之前所提到的,JavaScript 語言本身沒有接口,從而抽象只能依賴于隱性的契約。也就是說,一個對象/類暴露會暴露方法和屬性給另一個對象/類。下面這個例子當中的隱性契約就是, InventoryTracker 所依賴的任意一個 Request 模塊都要有一個 requestItems 方法。
Bad:
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);
});
}
}
class InventoryRequester {
constructor() {
this.REQ_METHODS = ['HTTP'];
}
requestItem(item) {
// ...
}
}
let 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.
let inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
inventoryTracker.requestItems();
來自:http://blog.jimmylv.info/2017-01-08-clean-code-javascript-classes-design-principles/