1. 程式人生 > 其它 >Practical Training Demo1-登入介面(11.1)

Practical Training Demo1-登入介面(11.1)

介紹

裝飾者提供比繼承更有彈性的替代方案。 裝飾者用用於包裝同介面的物件,不僅允許你向方法新增行為,而且還可以將方法設定成原始物件呼叫(例如裝飾者的建構函式)。
裝飾者用於通過過載方法的形式新增新功能,該模式可以在被裝飾者前面或者後面加上自己的行為以達到特定的目的
當然這定義一般很難理解,我們從現實生活中去理解一下什麼是裝飾者模式。

生活中的裝飾器

去年有個手機殼在同事裡非常流行,我也隨大流買了一個,它長這樣:

這個手機殼的安裝方式和普通手機殼一樣,就是卡在手機背面。不同的是它卡上去後會變成一塊水墨屏,這樣一來我們手機就有了兩個螢幕。平時辦公或者玩遊戲的時候,用正面的普通螢幕;閱讀的時候怕傷眼睛,就可以翻過來用背面的水墨屏了。

這個水墨屏手機殼安裝後,不會對手機原有的功能產生任何影響,僅僅是使手機具備了一種新的能力(多了塊螢幕),因此它在此處就是一個標準的裝飾器。

一般的函式例子

//需要裝飾的類(函式)
function Macbook() {
    this.cost = function () {
        return 1000;
    };
}

function Memory(macbook) {
    this.cost = function () {
        return macbook.cost() + 75;
    };
}

function BlurayDrive(macbook) {
    this.cost = function () {
        return macbook.cost() + 300;
    };
}


function Insurance(macbook) {
    this.cost = function () {
        return macbook.cost() + 250;
    };
}


// 用法
var myMacbook = new Insurance(new BlurayDrive(new Memory(new Macbook())));
console.log(myMacbook.cost());

裝飾器的應用場景

按鈕是我們平時寫業務時常見的頁面元素。假設我們的初始需求是:每個業務中的按鈕在點選後都彈出「您還未登入哦」的彈框。

那我們可以很輕易地寫出這個需求的程式碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>按鈕點選需求1.0</title>
</head>
<style>
    #modal {
        height: 200px;
        width: 200px;
        line-height: 200px;
        position: fixed;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        border: 1px solid black;
        text-align: center;
    }
</style>
<body>
	<button id='open'>點選開啟</button>
	<button id='close'>關閉彈框</button>
</body>
<script>
    // 彈框建立邏輯,這裡我們複用了單例模式面試題的例子
    const Modal = (function() {
    	let modal = null
    	return function() {
            if(!modal) {
            	modal = document.createElement('div')
            	modal.innerHTML = '您還未登入哦~'
            	modal.id = 'modal'
            	modal.style.display = 'none'
            	document.body.appendChild(modal)
            }
            return modal
    	}
    })()
    
    // 點選開啟按鈕展示模態框
    document.getElementById('open').addEventListener('click', function() {
        // 未點選則不建立modal例項,避免不必要的記憶體佔用
    	const modal = new Modal()
    	modal.style.display = 'block'
    })
    
    // 點選關閉按鈕隱藏模態框
    document.getElementById('close').addEventListener('click', function() {
    	const modal = document.getElementById('modal')
    	if(modal) {
    	    modal.style.display = 'none'
    	}
    })
</script>
</html>

按鈕釋出上線後,過了幾天太平日子。忽然有一天,產品經理找到你,說這個彈框提示還不夠明顯,我們應該在彈框被關閉後把按鈕的文案改為“快去登入”,同時把按鈕置灰。

聽到這個訊息,你立刻馬不停蹄地翻出之前的程式碼,找到了按鈕的 click 監聽函式,手動往裡面添加了文案修改&按鈕置灰邏輯。但這還沒完,因為你司的幾乎每個業務裡都用到了這類按鈕:除了“點選開啟”按鈕,還有“點我開始”、“點選購買”按鈕等各種五花八門的按鈕,這意味著你不得不深入到每一個業務的深處去給不同的按鈕新增這部分邏輯。

有的業務不在你這兒,但作為這個新功能迭代的 owner,你還需要把需求細節再通知到每一個相關同事(要麼你就自己上,去改別人的程式碼,更恐怖),怎麼想怎麼麻煩。一個文案修改&按鈕置灰尚且如此麻煩,更不要說我們日常開發中遇到的更復雜的需求變更了。

不僅麻煩,直接去修改已有的函式體,這種做法違背了我們的“開放封閉原則”;往一個函式體裡塞這麼多邏輯,違背了我們的“單一職責原則”。所以說這個事兒,越想越不能這麼幹。

裝飾器模式初相見

為了不被已有的業務邏輯干擾,當務之急就是將舊邏輯與新邏輯分離,把舊邏輯抽出去

// 將展示Modal的邏輯單獨封裝
function openModal() {
    const modal = new Modal()
    modal.style.display = 'block'
}

編寫新邏輯:

/ 按鈕文案修改邏輯
function changeButtonText() {
    const btn = document.getElementById('open')
    btn.innerText = '快去登入'
}

// 按鈕置灰邏輯
function disableButton() {
    const btn =  document.getElementById('open')
    btn.setAttribute("disabled", true)
}

// 新版本功能邏輯整合
function changeButtonStatus() {
    changeButtonText()
    disableButton()
}

然後把三個操作逐個新增open按鈕的監聽函式裡:

document.getElementById('open').addEventListener('click', function() {
    openModal()
    changeButtonStatus()
})

如此一來,我們就實現了“只新增,不修改”的裝飾器模式,使用changeButtonStatus的邏輯裝飾了舊的按鈕點選邏輯。以上是ES5中的實現,ES6中,我們可以以一種更加面向物件化的方式去寫:

/ 定義開啟按鈕
class OpenButton {
    // 點選後展示彈框(舊邏輯)
    onClick() {
        const modal = new Modal()
    	modal.style.display = 'block'
    }
}

// 定義按鈕對應的裝飾器
class Decorator {
    // 將按鈕例項傳入
    constructor(open_button) {
        this.open_button = open_button
    }
    
    onClick() {
        this.open_button.onClick()
        // “包裝”了一層新邏輯
        this.changeButtonStatus()
    }
    
    changeButtonStatus() {
        this.changeButtonText()
        this.disableButton()
    }
    
    disableButton() {
        const btn =  document.getElementById('open')
        btn.setAttribute("disabled", true)
    }
    
    changeButtonText() {
        const btn = document.getElementById('open')
        btn.innerText = '快去登入'
    }
}

const openButton = new OpenButton()
const decorator = new Decorator(openButton)

document.getElementById('open').addEventListener('click', function() {
    // openButton.onClick()
    // 此處可以分別嘗試兩個例項的onClick方法,驗證裝飾器是否生效
    decorator.onClick()
})

大家這裡需要特別關注一下 ES6 這個版本的實現,這裡我們把按鈕例項傳給了 Decorator,以便於後續 Decorator 可以對它為所欲為進行邏輯的拓展。在 ES7 中,Decorator 作為一種語法被直接支援了,它的書寫會變得更加簡單,但背後的原理其實與此大同小異。在下一節,我們將一起去探究一下 ES7 中 Decorator 背後的故事。