如何寫出優(yōu)雅的 JS 代碼?使用 SOLID 原則
本文轉(zhuǎn)載自微信公眾號(hào)「大遷世界」,轉(zhuǎn)載本文請(qǐng)聯(lián)系大遷世界公眾號(hào)。
設(shè)計(jì)模式的六大原則有:
- Single Responsibility Principle:?jiǎn)我宦氊?zé)原則
- Open Closed Principle:開閉原則
- Liskov Substitution Principle:里氏替換原則
- Law of Demeter:迪米特法則
- Interface Segregation Principle:接口隔離原則
- Dependence Inversion Principle:依賴倒置原則
把這六個(gè)原則的首字母聯(lián)合起來(lái)(兩個(gè) L 算做一個(gè))就是 SOLID (solid,穩(wěn)定的),其代表的含義就是這六個(gè)原則結(jié)合使用的好處:建立穩(wěn)定、靈活、健壯的設(shè)計(jì)。下面我們來(lái)分別看一下這六大設(shè)計(jì)原則。
單一職責(zé)原則(SRP)
單一功能原則 :?jiǎn)我还δ茉瓌t 認(rèn)為對(duì)象應(yīng)該僅具有一種單一功能的概念。
換句話說(shuō)就是讓一個(gè)類只做一種類型責(zé)任,當(dāng)這個(gè)類需要承擔(dān)其他類型的責(zé)任的時(shí)候,就需要分解這個(gè)類。在所有的SOLID原則中,這是大多數(shù)開發(fā)人員感到最能完全理解的一條。嚴(yán)格來(lái)說(shuō),這也可能是違反最頻繁的一條原則了。單一責(zé)任原則可以看作是低耦合、高內(nèi)聚在面向?qū)ο笤瓌t上的引申,將責(zé)任定義為引起變化的原因,以提高內(nèi)聚性來(lái)減少引起變化的原因。責(zé)任過(guò)多,可能引起它變化的原因就越多,這將導(dǎo)致責(zé)任依賴,相互之間就產(chǎn)生影響,從而極大的損傷其內(nèi)聚性和耦合度。單一責(zé)任,通常意味著單一的功能,因此不要為一個(gè)模塊實(shí) 現(xiàn)過(guò)多的功能點(diǎn),以保證實(shí)體只有一個(gè)引起它變化的原因。
「不好的寫法」
- class UserSettings {
- constructor(user) {
- this.user = user;
- }
- changeSettings(settings) {
- if (this.verifyCredentials()) {
- // ...
- }
- }
- verifyCredentials() {
- // ...
- }
- }
「好的寫法」
- 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()) {
- // ...
- }
- }
- }
開放閉合原則 (OCP)
軟件實(shí)體應(yīng)該是可擴(kuò)展,而不可修改的。也就是說(shuō),對(duì)擴(kuò)展是開放的,而對(duì)修改是封閉的。這個(gè)原則是諸多面向?qū)ο缶幊淘瓌t中最抽象、最難理解的一個(gè)。
- 通過(guò)增加代碼來(lái)擴(kuò)展功能,而不是修改已經(jīng)存在的代碼。
- 若客戶模塊和服務(wù)模塊遵循同一個(gè)接口來(lái)設(shè)計(jì),則客戶模塊可以不關(guān)心服務(wù)模塊的類型,服務(wù)模塊可以方便擴(kuò)展服務(wù)(代碼)。
- OCP支持替換的服務(wù),而不用修改客戶模塊。
說(shuō)大白話就是:你不是要變化嗎?,那么我就讓你繼承實(shí)現(xiàn)一個(gè)對(duì)象,用一個(gè)接口來(lái)抽象你的職責(zé),你變化越多,繼承實(shí)現(xiàn)的子類就越多。
「不好的寫法」
- 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
- }
「好的寫法」
- 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
- });
- }
- }
里氏替換原則(LSP)
里氏替換原則 :里氏替換原則 認(rèn)為“程序中的對(duì)象應(yīng)該是可以在不改變程序正確性的前提下被它的子類所替換的”的概念。
LSP則給了我們一個(gè)判斷和設(shè)計(jì)類之間關(guān)系的基準(zhǔn):需不需 要繼承,以及怎樣設(shè)計(jì)繼承關(guān)系。
當(dāng)一個(gè)子類的實(shí)例應(yīng)該能夠替換任何其超類的實(shí)例時(shí),它們之間才具有is-A關(guān)系。繼承對(duì)于「OCP」,就相當(dāng)于多態(tài)性對(duì)于里氏替換原則。子類可以代替基類,客戶使用基類,他們不需要知道派生類所做的事情。這是一個(gè)針對(duì)行為職責(zé)可替代的原則,如果S是T的子類型,那么S對(duì)象就應(yīng)該在不改變?nèi)魏纬橄髮傩郧闆r下替換所有T對(duì)象。
客戶模塊不應(yīng)關(guān)心服務(wù)模塊的是如何工作的;同樣的接口模塊之間,可以在不知道服務(wù)模塊代碼的情況下,進(jìn)行替換。即接口或父類出現(xiàn)的地方,實(shí)現(xiàn)接口的類或子類可以代入。
「不好的寫法」
- 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);
「好的寫法」
- 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);
接口隔離原則(ISP)
接口隔離原則 :接口隔離原則 認(rèn)為“多個(gè)特定客戶端接口要好于一個(gè)寬泛用途的接口”的概念。
不能強(qiáng)迫用戶去依賴那些他們不使用的接口。換句話說(shuō),使用多個(gè)專門的接口比使用單一的總接口總要好。
這個(gè)原則起源于施樂(lè)公司,他們需要建立了一個(gè)新的打印機(jī)系統(tǒng),可以執(zhí)行諸如裝訂的印刷品一套,傳真多種任務(wù)。此系統(tǒng)軟件創(chuàng)建從底層開始編制,并實(shí)現(xiàn)了這些 任務(wù)功能,但是不斷增長(zhǎng)的軟件功能卻使軟件本身越來(lái)越難適應(yīng)變化和維護(hù)。每一次改變,即使是最小的變化,有人可能需要近一個(gè)小時(shí)的重新編譯和重新部署。這 是幾乎不可能再繼續(xù)發(fā)展,所以他們聘請(qǐng)羅伯特Robert幫助他們。他們首先設(shè)計(jì)了一個(gè)主要類Job,幾乎能夠用于實(shí)現(xiàn)所有任務(wù)功能。只要調(diào)用Job類的 一個(gè)方法就可以實(shí)現(xiàn)一個(gè)功能,Job類就變動(dòng)非常大,是一個(gè)胖模型啊,對(duì)于客戶端如果只需要一個(gè)打印功能,但是其他無(wú)關(guān)打印的方法功能也和其耦合,ISP 原則建議在客戶端和Job類之間增加一個(gè)接口層,對(duì)于不同功能有不同接口,比如打印功能就是Print接口,然后將大的Job類切分為繼承不同接口的子 類,這樣有一個(gè)Print Job類,等等。
「不好的寫法」
- class DOMTraverser {
- constructor(settings) {
- this.settings = settings;
- this.setup();
- }
- setup() {
- thisthis.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.
- // ...
- });
「好的寫法」
- class DOMTraverser {
- constructor(settings) {
- this.settings = settings;
- this.options = settings.options;
- this.setup();
- }
- setup() {
- thisthis.rootNode = this.settings.rootNode;
- this.setupOptions();
- }
- setupOptions() {
- if (this.options.animationModule) {
- // ...
- }
- }
- traverse() {
- // ...
- }
- }
- const $ = new DOMTraverser({
- rootNode: document.getElementsByTagName("body"),
- options: {
- animationModule() {}
- }
- });
依賴倒置原則(DIP)
依賴倒置原則:依賴倒置原則 認(rèn)為一個(gè)方法應(yīng)該遵從“依賴于抽象而不是一個(gè)實(shí)例” 的概念。依賴注入是該原則的一種實(shí)現(xiàn)方式。
依賴倒置原則(Dependency Inversion Principle,DIP)規(guī)定:代碼應(yīng)當(dāng)取決于抽象概念,而不是具體實(shí)現(xiàn)。
- 高層模塊不要依賴低層模塊
- 高層和低層模塊都要依賴于抽象;
- 抽象不要依賴于具體實(shí)現(xiàn)
- 具體實(shí)現(xiàn)要依賴于抽象
- 抽象和接口使模塊之間的依賴分離
類可能依賴于其他類來(lái)執(zhí)行其工作。但是,它們不應(yīng)當(dāng)依賴于該類的特定具體實(shí)現(xiàn),而應(yīng)當(dāng)是它的抽象。這個(gè)原則實(shí)在是太重要了,社會(huì)的分工化,標(biāo)準(zhǔn)化都 是這個(gè)設(shè)計(jì)原則的體現(xiàn)。顯然,這一概念會(huì)大大提高系統(tǒng)的靈活性。如果類只關(guān)心它們用于支持特定契約而不是特定類型的組件,就可以快速而輕松地修改這些低級(jí) 服務(wù)的功能,同時(shí)最大限度地降低對(duì)系統(tǒng)其余部分的影響。
「不好的寫法」
- 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();
「好的寫法」
- 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) {
- // ...
- }
- }
- const inventoryTracker = new InventoryTracker(
- ["apples", "bananas"],
- new InventoryRequesterV2()
- );
- inventoryTracker.requestItems();