從面向?qū)ο蠼庾x設(shè)計思想

作者:哲思

時間:2021.8.30

郵箱:1464445232@qq.com

GitHub:zhe-si (哲思) (github.com)

前言

很早就想總結(jié)一下自己對面向?qū)ο蟮睦斫?,借這次公開課梳理了一下思路,并在之后撰寫成本文。

對于面向?qū)ο蟾拍钚缘慕榻B與理解當前網(wǎng)上已經(jīng)有很多了,但卻很少有人能講出怎樣用好面向?qū)ο螅簿褪侨绾斡妹嫦驅(qū)ο蟮乃枷朐O(shè)計出好的程序。所以本文將側(cè)重“設(shè)計”二字來講述這個問題。

當然,本文只是我參照當下所學和做的項目產(chǎn)生的認識,可能隨著見識的提升和技術(shù)的發(fā)展,推翻一些當下所寫。但是,其中對設(shè)計的思考,想必是走向更高位置的必經(jīng)之路。

注:本文舉例所用的代碼統(tǒng)一使用Kotlin,一種包含諸多高級特性、可代替Java并能夠編譯成諸多類型的產(chǎn)物、已經(jīng)成為Android官方推薦的高級語言。

1.什么是面向?qū)ο?/h2>

首先,給大家一個思考題。

小明是一個志存高遠的程序員。一天,由于業(yè)務(wù)需要,他想要在原有數(shù)據(jù)類型Number的基礎(chǔ)上拓展兩個新的子數(shù)據(jù)類型A與B,但操作時需要統(tǒng)一使用父類型Number進行操作,同時需要支持調(diào)用順序無關(guān)的相加(add)的方法(假設(shè)相加邏輯為A.numA + B.numB,相加結(jié)果始終為C類型)。

小明的設(shè)計之魂涌上心頭,打算不光要實現(xiàn),還要實現(xiàn)一個更靈活、易拓展的設(shè)計,但沒有什么好的思路,你能幫幫他嗎?

1.1.面向?qū)ο蟮暮x

從小明的問題回過頭,我們開門見山的給出面向?qū)ο缶幊痰亩x:

面向?qū)ο缶幊叹褪?strong>將事物抽象成對象,針對對象所持有的數(shù)據(jù)和與之相關(guān)的行為進行編程。

想要了解這個概念,就不得不從老生常談的編程范式的歷史講起。

當計算機世界初開的時候,世界上只有低級語言,即機器語言和匯編語言。這種語言,從計算機的角度,一步步告訴計算機它該先做什么,再做什么。而我們需要把我們實際的問題轉(zhuǎn)化為計算機的基本模型:存儲器、運算器、控制器、輸入/輸出設(shè)備,也就是把什么數(shù)據(jù)存起來,什么數(shù)據(jù)和什么數(shù)據(jù)取出來做運算。我們把這種編程方式叫做指令式編程。

后來,人們?yōu)榱俗尵幊陶Z言更符合人的理解,所以將最能描述事物本質(zhì)同時又足夠抽象的數(shù)學概念引入其中,我們可以像解數(shù)學題一樣定義變量、對變量相加減(此處的變量指用一個標識符代指一個數(shù)據(jù))、甚至定義函數(shù)來表示一個通用操作過程。這樣,我們就可以通過數(shù)學去描述現(xiàn)實事物,并將事物的發(fā)展轉(zhuǎn)化為一步步的運算過程。我們把這種編程方式叫作過程式編程,也算指令式編程的一種延伸。

在編寫程序的過程中,人們發(fā)現(xiàn)編程的本質(zhì)就是處理數(shù)據(jù),也就是數(shù)據(jù)和操作(對數(shù)據(jù)的處理)。而二者有著非常明顯的對應(yīng)關(guān)系,一組相關(guān)的數(shù)據(jù),總是對應(yīng)一組相關(guān)的操作。而這樣的組合,便滿足了我們生活中對于絕大多數(shù)事物(也就是對象)的描述。我們將現(xiàn)實中的事物對應(yīng)程序中的對象,讓程序的運行變成對象與對象間的交互。對象成為程序中的基本單元,將一類對象相關(guān)的數(shù)據(jù)和數(shù)據(jù)對應(yīng)的操作封裝到一起作為類,而對象則是該類的一個具體實例,這便是面向?qū)ο缶幊?/strong>。

編程的發(fā)展史便是不斷抽象來讓編程符合人的認知和事物的本質(zhì)。包括之后出現(xiàn)的函數(shù)式編程、響應(yīng)式編程,都是如此。但之后的編程范式都沒有完全逃脫面向?qū)ο蟮乃枷耄瑫r都是在一些具體場景下的產(chǎn)物。世界是由事物組成的,這已經(jīng)符合了我們對世界基本的認知,這也是面向?qū)ο笠恢苯?jīng)久不衰的原因。

1.2.面向?qū)ο蟮娜筇卣?/h3>

這里要首先強調(diào)一個概念:類型。面向?qū)ο髮⒁磺锌闯蓪ο?,通過類去描述對象,這里的類,在程序中,就是類型。我們將一類對象定義為一種類型,并在類型中聲明屬性和方法(這些都是該類型的特征)??梢哉f,面向?qū)ο缶幊?,從計算機角度來說,就是面向類型編程!

接下來,我們將細說面向?qū)ο蟮母拍?。而面向?qū)ο蟮娜筇卣鲃t是對其概念最好的描述:封裝、繼承、多態(tài)

三者可以說從三個層面對面向?qū)ο筮M行了描述。封裝是面向?qū)ο笞罨镜谋憩F(xiàn),繼承是面向?qū)ο笞詈诵牡男袨?,多態(tài)是面向?qū)ο笞钪匾哪芰?/strong>。

1.2.1.封裝

封裝:將不需要外部看到的數(shù)據(jù)和對應(yīng)方法放到類內(nèi),外部不可見,只暴露外部需要看到的數(shù)據(jù)和方法。

這是面向?qū)ο蟮某踔院妥罨镜谋憩F(xiàn),將相關(guān)的數(shù)據(jù)放到一起,將數(shù)據(jù)對應(yīng)的方法放到一起,實現(xiàn)了高內(nèi)聚。

同時,進行信息隱藏,將內(nèi)部數(shù)據(jù)和邏輯隱藏到類的內(nèi)部,只讓外部看到這個類的外部表現(xiàn)對應(yīng)的數(shù)據(jù)和操作,實現(xiàn)了低耦合。

舉個經(jīng)典的例子:

屬性 行為
名字、顏色、尾巴長短 吃飯、叫、尾巴長不長
class Dog(
    val name: String,
    val color: String,
    private val tailLength: Double
) {
    private val description: String
        get() = "${color}色、尾巴${tailLength}厘米的狗${name}"
    
    fun eat() {
        println(description + "正在吃飯")
    }

    fun shout() {
        println(description + "正在叫:汪汪汪!")
    }
      
    fun isTailLong(): Boolean {
        return tailLength > 15
    }
}

fun main() {
    // 一個狗的實例對象
    val dog1 = Dog("dog1", "黑白", 12.5)
    // 狗暴露出的外部信息與行為
    println(dog1.name)
    println(dog1.color)
    dog1.eat()
    dog1.shout()
    println(dog1.isTailLong())
}

在這里狗的屬性與行為都被封裝到Dog類中。

當前場景下外界不需要了解狗的尾巴具體是多長,所以將尾巴具體長度的信息隱藏,而暴露判斷尾巴長不長的方法。同時對內(nèi)部實現(xiàn)所需的狗的自我描述description也進行隱藏,只能通過對外暴露的行為間接訪問。

這樣,外部可以通過Dog來訪問狗的各種外在信息與行為,同時也看不到內(nèi)部具體的實現(xiàn)。

本例中,封裝的是一個實體類,將一個實體相關(guān)的數(shù)據(jù)和方法放到一個類中。但如果只是這樣,實現(xiàn)的方法有很多種,稱不上使用了面對對象,因為現(xiàn)實事物都有一個很重要的描述方式:依據(jù)特征去分類。

1.2.2.繼承

繼承:依據(jù)相關(guān)類的共有特征進行層級分類,具體類包含抽象類(它的上一層分類)的特征,二者是一種“is-a”的關(guān)系。

這是面向?qū)ο笞詈诵牡男袨榕c標志。子類繼承父類,表示子類“is-a”父類,子類從父類得到子類共有的方法,并進行個性化實現(xiàn)與拓展,是一種父類別下的具體類別,有著父類包含的特征,也可以擁有自己獨有的特征。而父類是一組相關(guān)子類共同特征的集合,可以從抽象層面代指子類

比如以下的例子,

狗、猩猩、貓、兔子,都是(is-a)動物,“動物”是那些具體動物的上一級分類(當然,這里還可以說它們都是哺乳動物,這分類的依據(jù),需要根據(jù)需求和實際情況而定),包含了具體動物在“動物”這個抽象層面的共同特征。

動物

屬性 行為
名字、顏色 吃飯、叫
abstract class Animal(
    val name: String,
    val color: String
) {
    // description是通用的內(nèi)部特征,但會隨著不同的實例而變化,所以將會改變的子類個性化特征描述otherDescription與子類型名typeName抽象出來,讓子類實現(xiàn)
    protected abstract val otherDescription: String
    protected abstract val typeName: String
  
    protected val description: String
        get() = "${color}色${otherDescription}的${typeName}${name}"

    abstract fun eat()
    abstract fun shout()
}

于是,我們將狗的抽象特征提取到動物抽象類中,

class Dog(
    name: String,
    color: String,
    private val tailLength: Double
): Animal(name, color) {

    override val otherDescription = "、尾巴${tailLength}厘米"
    override val typeName = "狗"

    override fun eat() {
        println(description + "正在吃飯")
    }

    override fun shout() {
        println(description + "正在叫:汪汪汪!")
    }

    fun isTailLong(): Boolean {
        return tailLength > 15
    }
}

并引入新的動物類別:猩猩。它也是動物的一種,包含動物的特征。

class Orangutan(
    name: String,
    color: String,
): Animal(name, color) {

    override val otherDescription = ""
    override val typeName = "猩猩"

    override fun eat() {
        println(description + "正在吃飯")
    }

    override fun shout() {
        println(description + "正在叫:嗷嗷~!")
    }
}

我們雖然提取了狗和猩猩的抽象特征“動物”,但當我們直接需要狗或者猩猩對象時,二者的外在表現(xiàn)沒有任何區(qū)別。我們可以調(diào)用它們的抽象特征和特有特征。

    // main()中
    // 當我們需要狗的時候,直接實例化一只狗,可以調(diào)用它的抽象特征(如:name、eat等)以及特有特征(isTailLong)
    println("**************** 1 *******************")
    val dog1 = Dog("dog1", "黑白", 12.5)
    println(dog1.name)
    println(dog1.color)
    dog1.eat()
    dog1.shout()
    println(dog1.isTailLong())

    // 需要猩猩也是同理
    println("**************** 2 *******************")
    val orangutan1 = Orangutan("orangutan1", "黑")
    println(orangutan1.name)
    orangutan1.eat()
    orangutan1.shout()
// 輸出
**************** 1 *******************
dog1
黑白
黑白色、尾巴12.5厘米的狗dog1正在吃飯
黑白色、尾巴12.5厘米的狗dog1正在叫:汪汪汪!
false
**************** 2 *******************
orangutan1
黑色的猩猩orangutan1正在吃飯
黑色的猩猩orangutan1正在叫:嗷嗷~!

但是當我們只需要關(guān)注動物的抽象特征、不關(guān)心具體動物的特有特征時,可以用“動物”這個抽象類別去統(tǒng)一代指和對待。從抽象層面,狗、猩猩都是動物。

    // 當我們只需要所有的動物,不需要區(qū)分是狗還是猩猩,則可以用父類去統(tǒng)一代指具體類,并調(diào)用其抽象的共有特征(但這些抽象特征的具體表現(xiàn)不同)
    println("**************** 3 *******************")
    val animals = listOf(dog1, orangutan1, Dog("dog2", "白", 15.2), Orangutan("orangutan2", "棕"))
    for (animal in animals) {
        println(animal.name)
        println(animal.color)
        animal.eat()
        animal.shout()
        println()
    }
// 輸出
**************** 3 *******************
dog1
黑白
黑白色、尾巴12.5厘米的狗dog1正在吃飯
黑白色、尾巴12.5厘米的狗dog1正在叫:汪汪汪!

orangutan1
黑
黑色的猩猩orangutan1正在吃飯
黑色的猩猩orangutan1正在叫:嗷嗷~!

dog2
白
白色、尾巴15.2厘米的狗dog2正在吃飯
白色、尾巴15.2厘米的狗dog2正在叫:汪汪汪!

orangutan2
棕
棕色的猩猩orangutan2正在吃飯
棕色的猩猩orangutan2正在叫:嗷嗷~!

1.2.3.多態(tài)

多態(tài):相同的特征,在不同情況下有不同的表現(xiàn)。

這是面向?qū)ο笞钪匾哪芰?,也是它靈活、易拓展和復(fù)用的原因。多態(tài)本身的內(nèi)涵非常寬泛,有重載多態(tài)、子類型多態(tài)、參數(shù)多態(tài)、結(jié)構(gòu)多態(tài)、行多態(tài)等。從面向?qū)ο蠼嵌?,最常用的是子類型多態(tài)。但不管是那種多態(tài),都符合以上的定義,都可以在調(diào)用相同的特征后產(chǎn)生不同的表現(xiàn)。

比如,重載多態(tài),通過重載函數(shù)(函數(shù)本身即可理解為一種能力或特征,放到類中,即該類型的特征),調(diào)用時使用不同的參數(shù)(類別、個數(shù))進而得到不同的表現(xiàn)。

class Number(val num: Int) {
 fun add(number: Number): Int {
        return num + number.num
    }
    
    fun add(number: Int): Int {
        return number + num
    }
}

fun main() {
    val n1 = Number(5)
    println(n1.add(6))
    println(n1.add(Number(6)))
}

而子類型多態(tài),在繼承的例子中已有表現(xiàn),Animal父類指代不同的子類型(狗、猩猩)時,雖然一視同仁的調(diào)用了共有的特征animal.eat()animal.shout(),但卻產(chǎn)生了不同的表現(xiàn),如狗的“汪汪”叫和猩猩的“嗷嗷”叫。

這(子類型多態(tài))是通過定義具體子類型,并調(diào)用抽象父類型的共有特征實現(xiàn)的多態(tài)。父類型聲明了一組類型的共有特征,但不一定直接實現(xiàn),可以延遲到子類型去實現(xiàn),進而基于子類型不同的實現(xiàn)方式產(chǎn)生不同的表現(xiàn)(多態(tài))。而這種多態(tài)的實現(xiàn)方式,是基于繼承實現(xiàn)的。

由于在講面向?qū)ο?,所以以下我們所說的多態(tài)都特指子類型多態(tài),如果描述其他類型多態(tài),會具體說明。

1.3.面向?qū)ο蟮乃枷?/h3>

上面已經(jīng)說過,三大特征是從三個層面去描述面向?qū)ο?。封裝從代碼手段層面將相關(guān)的數(shù)據(jù)和對應(yīng)的操作集中放到一起,讓程序聚合成類和對象的基本單元;繼承從核心行為層面,給予了類聚合相關(guān)特征、靈活分類的能力;多態(tài)從表現(xiàn)和結(jié)果層面,描述了基于這種分類所帶來的好處,即可拓展性和可復(fù)用性。


好了,讀到這里,想必大家對面對對象的基本概念和想法有了初步的理解,這些知識是當前網(wǎng)上比較“流行”的內(nèi)容,也足夠大家去面試或回答本科課堂的問題(甚至比較自信的說,算是比較透徹的了 ??)。

但我想問,你真的理解了面向?qū)ο罅藛??或者說,你能用好面向?qū)ο蟛懗龊玫脑O(shè)計嗎?再或者說,你已經(jīng)有了足夠好的想法,去解決本節(jié)一開始提出的思考題嗎?

我想你也看到了,這不是結(jié)束,只是這篇文章的開始。面向?qū)ο蟮乃枷氩┐缶睿膊粩嗯c時俱進,需要豐富的經(jīng)驗與知識儲備,更需要我們不斷的探索。

我們直接給出思考題比較基本和經(jīng)典的解法,也希望大家能在繼續(xù)閱讀的過程中,發(fā)現(xiàn)當前解法在不同場景(或拓展場景)的問題,并產(chǎn)生自己新的想法:

// 解法: 二次分派
typealias C = Int

abstract class Number {
    abstract fun add(number: Number): C
    abstract fun add(a: A): C
    abstract fun add(b: B): C
}

class A(private val numA: C): Number() {
    override fun add(number: Number): C {
        return number.add(this)
    }

    override fun add(a: A): C {
        return a.numA + numA
    }

    override fun add(b: B): C {
        return b.numB + numA
    }
}

class B(val numB: C): Number() {
    override fun add(number: Number): C {
        return number.add(this)
    }

    override fun add(a: A): C {
        return a.add(this)
    }

    override fun add(b: B): C {
        return b.numB + numB
    }
}

fun main() {
    val n1: Number = A(3)
    val n2: Number = B(4)
    println(n1.add(n2))
    println(n2.add(n1))
}

// 輸出
// 7
// 7

那么,面向?qū)ο蟮乃枷氲降资鞘裁矗?/p>

面向?qū)ο笫且环N語言無關(guān)、技術(shù)無關(guān)的一種編程范式,強調(diào)編程時思考問題的角度與方法。

我們回過頭再去看面向?qū)ο蟮亩x:面向?qū)ο缶幊叹褪?strong>將事物抽象成對象,針對對象所持有的數(shù)據(jù)和與之相關(guān)的行為進行編程。

簡單來說,我們要分析目標數(shù)據(jù)和操作,將世界合理的抽象為有層級分類的對象結(jié)構(gòu),每個對象結(jié)構(gòu)是一組數(shù)據(jù)和與之相關(guān)的操作的集合,通過對象的創(chuàng)建、運行、交互和銷毀實現(xiàn)一個程序。

這里的對象,不光包括以上例子中的實體類對象(如:Animal、Dog等),還包括一些抽象的對象(如:行為、消息、算法、狀態(tài)等)。

這里的關(guān)鍵點就是對世界的抽象與分類,難點則有很多,如:對象的交互方式、銷毀的時機等。由此也可以看出來,對目標事物本質(zhì)的思考與理解才是最重要的,有了不同的抽象與分類形式,進而才會有其他問題(如:交互方式)的產(chǎn)生。

對世界進行抽象與分類的過程,也是認知事物相同與不同的過程?!暗郎唬簧?,二生三,三生萬物?!笨梢哉f,正因為不同,因為變化,才產(chǎn)生了世界上的萬物!所以想要描述好世界上的事物,就必須描述好事物的變化規(guī)律。

2.面向“變化”編程

在我看來,良好的設(shè)計最重要、也是最主要的就是面向“變化”編程。

處理好了變化,不光擁有了良好的拓展性,更提升了復(fù)用性、可讀性、穩(wěn)定性、可測試性、兼容性等諸多指標,甚至性能也會有所提升。

2.1.什么是變化?

那么,什么是“變化”?

變化就是一組可以獨立改變的事物。

具體來講,從抽象類衍生出各種有著特有特征的具體類就是變化;產(chǎn)生了(或可能產(chǎn)生)新的事物進而需要拓展新的類就是變化;在不同的情況下事物需要有不同的表現(xiàn)就是變化。這三種變化,總結(jié)起來,就是:衍生細節(jié)、拓展種類、封裝差異。

2.1.1.衍生細節(jié)

小明開了一家快餐店,由于小明隨便賣了點吃的(方便面),所以大家來了都招呼小明:老板,來份吃的。

后來,小明拓展業(yè)務(wù),也做起了手抓餅。由于有的顧客想吃方便面,有的想吃手抓餅,而“吃的”是二者的統(tǒng)稱,無法區(qū)分二者,所以之后顧客進門就說:老板,來份方便面/手抓餅?;蛘哒f:老板,吃的中來份方便面/手抓餅。當然,也有一些不拘小節(jié)的顧客,進門就說:老板,隨便來份吃的。

在這個例子中:

初期,由于沒有或不需要那么多細節(jié),大家不關(guān)系與區(qū)分具體種類,所以用統(tǒng)稱去描述。

后期,發(fā)生了變化,由于業(yè)務(wù)細化,內(nèi)容變得豐富起來,細節(jié)變多,這個統(tǒng)稱無法去描述與區(qū)分那些不同的細節(jié),需要用更具體的類別去描述所需,而具體的類別自然也被包括在籠統(tǒng)的類別中。當然,也存在一些不關(guān)心這些細節(jié)的場景與需要,而直接用統(tǒng)稱去描述它們。

這就是對細節(jié)的衍生。從一開始簡單的業(yè)務(wù)需要,到后來逐漸細化業(yè)務(wù),區(qū)分細節(jié)。對應(yīng)的,就是一開始的抽象類(也許一開始不認為它是抽象類),到后來從抽象類衍生的、可以區(qū)分細節(jié)的具體類。

而這樣從抽象類衍生具體類,非常自然的應(yīng)對了這種細節(jié)衍生的變化。不但解決了新的要區(qū)分細節(jié)的需要,還不破壞原本對抽象事物的使用。

2.1.2.拓展種類

小紅也開了一個快餐店,她的拿手絕活是牛肉包子,吸引許多人慕名來吃,每個進來都招呼小紅:老板,來個牛肉包子。

后來,小紅也拓展業(yè)務(wù),想到自己牛肉包子好吃,自己做別的包子估計也不錯,于是出了新的豬肉包子。每個進來賣豬肉包子的顧客可以仿照賣牛肉包子招呼小紅:老板,來個豬肉包子。

在這個例子中:

初期,業(yè)務(wù)場景單一,只有一個種類。

后期,發(fā)生了變化,由于業(yè)務(wù)拓展,新的類似業(yè)務(wù)產(chǎn)生,有了新的種類。同時,由于二者的相似性,拓展時如果完全重新拓展,成本較高。由此,將這類業(yè)務(wù)的共同特征提取,作為公共的抽象業(yè)務(wù)(抽象類),基于此抽象業(yè)務(wù)去拓展新的業(yè)務(wù)。

這就是對種類的拓展。從一開始單一的業(yè)務(wù)需要,到后來逐漸拓展業(yè)務(wù),添加業(yè)務(wù)種類。對應(yīng)的,就是一開始只有一個具體類別,到后來需要高效拓展,提取公共業(yè)務(wù)封裝到抽象類中,并基于抽象類去拓展新的具體類別。

這樣提取抽象類并拓展其他具體類,非常統(tǒng)一、高效的應(yīng)對了這種種類拓展的變化。不但解決了創(chuàng)建新類別的要求,還支持原有公共邏輯與接口的復(fù)用。


這里可能會有同學困惑,兩個例子都是現(xiàn)有一個具體類,后來需要產(chǎn)生新類,為啥一個是衍生細節(jié)、一個是拓展種類呢?

首先,請注意衍生細節(jié)在初期的抽象類指沒有或不需要那么多細節(jié),本例子講方便面雖是一個具體類,但在當前不區(qū)分細節(jié)的場景下,是當作一個抽象類——“吃的”去處理的。當然,有一些場景,可能一開始就是只需要抽象類(如:一開始我認為長得像貓和老鼠中的杰瑞的都是老鼠,后來才知道老鼠還分倉鼠、田鼠等具體種類,甚至其下還有更具體的種類)。

而拓展種類,需要抽象類是因為需要提取公共邏輯與接口,實現(xiàn)復(fù)用。

所以雖然看上去都是實現(xiàn)抽象類和具體類,但是二者出發(fā)點不同,抽象類和具體類的概念出現(xiàn)的順序也不同。


2.1.3.封裝差異

小明的快餐店不景氣,快要交不起店鋪租用費了。他想起老朋友小紅的快餐店的包子十分火爆,想去投靠小紅。

小紅十分善良,同意了小明的請求。兩人一合計,打算將兩個快餐店合成一個,同時不分你我,一起經(jīng)營,并合并了菜單。

菜單:

吃的:方便面、手抓餅、包子(牛肉包子、豬肉包子)

在這個例子中:

當前,存在兩個相似業(yè)務(wù)(兩人的快餐店),雖然存在許多實現(xiàn)上的不同(一個是小明的,一個是小紅的,菜單也不同),但大體業(yè)務(wù)形式是相似的(都支持顧客點餐)。

既然是相似的業(yè)務(wù),外部就希望可以統(tǒng)一的看待它們,不希望自己處理這些不同。而這些不同,就是差異,就是變化。

為了統(tǒng)一對待相似業(yè)務(wù),可以提取它們的宏觀公共業(yè)務(wù)邏輯與表現(xiàn)接口,封裝為總體業(yè)務(wù)的類。而實現(xiàn)上的不同,則交由實現(xiàn)接口(抽象類)的具體類去實現(xiàn)。

這就是對差異的封裝。從一開始有多個相似的業(yè)務(wù),到后來需要統(tǒng)一對待,隱藏差異。對應(yīng)的,就是一開始有多個業(yè)務(wù)邏輯類,到后來提取相同的宏觀公共業(yè)務(wù)邏輯與接口作為總體業(yè)務(wù)類,而變化的具體實現(xiàn)則延遲到實現(xiàn)對應(yīng)接口的具體類中去實現(xiàn)。

這樣提取提取相似的業(yè)務(wù)邏輯與表現(xiàn)、封裝變化的具體實現(xiàn)的方式,對外透明且清晰的應(yīng)對了這種存在差異而導(dǎo)致的變化。不但讓差異對外透明,而且精簡了重復(fù)的邏輯,同時支持具體實現(xiàn)的復(fù)用。

2.2.怎么面向變化編程

面向變化編程的過程,也是封裝變化、調(diào)整結(jié)構(gòu)支持變化的過程。

什么是變化的章節(jié),我們講到了三種常見變化,在實際問題中,我們不一定每次都要分清這是什么變化,因為變化的表現(xiàn)千變?nèi)f化,一個變化可能所屬多個變化的種類,難以明確分類,更難以一一歸納。但不要害怕,以上三種變化從大的方面基本涵蓋了變化的情形,其他大多是它們的變種。其次,上面的三種變化是給出了三種典型情況,希望以此為例,說明變化是什么以及解決變化的通用方法。

2.2.1.封裝變化

面向變化編程的第一步就是封裝變化。

封裝變化有三個重點:1. 讓變化對外界透明;2. 將變化縮小為它本質(zhì)的樣子;3. 將變化的影響范圍縮到最小。

  • 將變化封裝的結(jié)果就是變化對外界透明。

    對變化的細節(jié)感興趣的,我們允許它們感知到變化;對于更多對變化對細節(jié)不感興趣的,我們要讓變化可以像一類事物去對待,而將變化的實現(xiàn)延遲到具體類中實現(xiàn),減少外界對變化的了解。這不但減輕了外界處理變化、了解變化細節(jié)的復(fù)雜度與工作量,降低了代碼復(fù)雜度,同時也將變化的影響控制在了內(nèi)部范圍,便于查找與修改。

  • 將變化封裝的難點就是將變化縮小到它本質(zhì)的樣子。

    換句話說,就是找出變化的本質(zhì),并將其封裝,不要將沒有變化的部分也封裝其內(nèi)。找出變化的本質(zhì)可是一個技術(shù)活,需要你切實了解到底是什么引發(fā)了變化,撥開應(yīng)用場景對變化的層層封裝,將真正的變化抽象出來。當我們將本質(zhì)的變化封裝起來,也就最大限度的復(fù)用了代碼,減少了每次變化的代碼體積,同時邏輯也將更加清晰,好處自然不言而喻。

  • 將變化的影響范圍縮到最小,是封裝變化的重要目的。

    除了將變化的部分透明的封裝到內(nèi)部,同時要減少對外暴露的表現(xiàn)形式,讓行為與職責更加單一明確。而在變化的類內(nèi)部,變化的部分影響的范圍也應(yīng)盡可能的縮小。最好,變化的類就是變化本身。減少變化的影響范圍,本身也反映了對變化的理解程度以及對變化的抽象能力。當我們將變化的影響范圍縮到最小,我們也就盡可能的掌控了變化。

2.2.2.支持變化

面向變化編程的核心一步,就是調(diào)整變化的封裝結(jié)構(gòu),支持更多的變化。

變化雖帶來復(fù)雜性,但更是發(fā)展與進步的標志。項目只應(yīng)該因為業(yè)務(wù)而終止,而不應(yīng)該是代碼無法繼續(xù)維護和拓展而結(jié)束。面對變化,不但不應(yīng)該逃避,我們反而應(yīng)該去擁抱變化。

當我們已經(jīng)通過封裝變化將變化盡可能集中,接下來,我們就可以讓變化在此處支持拓展,并在不同的情形下選擇不同的具體表現(xiàn)。而支持變化的方法,無外乎還是提取公共特征與相同的抽象業(yè)務(wù)形式(接口),并將不同的實現(xiàn)延遲到具體類。但困難的是如何對這些特征及接口進行抽象,讓其足夠清晰且靈活。

同時,在實現(xiàn)的過程中,大家也在盡可能的不讓新的變化影響原本的邏輯,這樣做可以讓變化更靈活、易拓展與復(fù)用。而方法還是將變化盡可能的封裝,將變化的邏輯更集中的放到具體的實現(xiàn)類中,讓變化可以自描述。當然,外界如果想使用一個新的類別,就必須使用新類別的聲明。想要優(yōu)化這個問題,除了將新類別的引用封裝到單個地方來隱藏其他地方的顯示引用外,還可以通過代碼生成、依賴注入等方式隱式引用。

2.3.在面向?qū)ο笾忻鎸ψ兓?/h3>

藏了這么長時間,終于講到了如何用面向?qū)ο笕ソ鉀Q變化問題 ??。在什么是變化章節(jié)中,我們講了變化就是在原本的業(yè)務(wù)基礎(chǔ)上產(chǎn)生了新的業(yè)務(wù)需求,主要有三種情況:衍生細節(jié)、拓展種類、封裝差異;在怎么面向變化編程章節(jié)中,我們講了要對變化進行抽象、封裝和拓展。但大家有沒有一種莫名的熟悉感?這不就是我們在面向?qū)ο笾袑W到的東西嗎?

2.3.1.用面向?qū)ο笱苌毠?jié)

什么是變化:衍生細節(jié)的例子中,從抽象類中衍生細節(jié),產(chǎn)生具體類,不就是繼承嗎?這種變化體現(xiàn)了繼承允許一種類別可以在其基礎(chǔ)上去完成個性化的實現(xiàn)與拓展的能力。而由于指向不同的具體類,在使用抽象類的統(tǒng)一調(diào)用中,產(chǎn)生不同的表現(xiàn)(表現(xiàn)不同的細節(jié)),就是多態(tài)呀。

將該例子實現(xiàn)為代碼,并針對怎么面向變化編程所講進行具體說明,如下:

原本小明的快餐店,賣吃的,不需要那么多細節(jié),

class Food() {
    fun sellFood() {
        println("老板,來份吃的")
    }
}

class FastFoodShopXM {
    val food = Food()
    
    fun orderFood() {
        food.sellFood()
    }
}

后來,細化了業(yè)務(wù),具體區(qū)分了“吃的”的種類,

open class Food {
    open fun sellFood() {
        // 此處省略隨便點了點吃的之后一系列過程
        println("老板,隨便來電吃的")  // 這里代表對某個具體類的封裝,也可以將該邏輯是現(xiàn)在外部,不提供本默認實現(xiàn),同時給orderFood隨機傳入一個具體類別
    }
}

class InstantNoodles: Food() {
    override fun sellFood() {
        // 此處省略點了方便面之后一系列過程
        println("老板,來份方便面")
    }
}

class HandCake: Food() {
    override fun sellFood() {
        // 此處省略點了手抓餅之后一系列過程
        println("老板,來份手抓餅")
    }
}

// 對原FastFoodShopXM進行調(diào)整,使支持新的場景
class FastFoodShopXM {
    // 點餐
    fun orderFood(food: Food) {
        food.sellFood()
    }
}

fun main() {
    val fastFoodShopXM = FastFoodShopXM()
    // 顧客點不同的食物
    fastFoodShopXM.orderFood(InstantNoodles())
    fastFoodShopXM.orderFood(HandCake())
    fastFoodShopXM.orderFood(Food())
}

// 老板,來份方便面
// 老板,來份手抓餅
// 老板,隨便來電吃的

此例子中,變化是從一個抽象種類衍生包含更多細節(jié)的具體種類,對應(yīng)原來的Food,到后來更具體的InstantNoodles和HandCake。

  • 封裝變化:對外部透明

    我們將這些變化的細節(jié)通過繼承封裝到具體類中,而抽象類Food中的sellFood作為公共特征與接口,讓外部可見,而變化的具體實現(xiàn)(點具體食物及之后的所有操作)封裝到具體類中,對外部透明。

  • 封裝變化:將變化縮小為它本質(zhì)的樣子

    這里我們可以進行如下實現(xiàn):

    open class FastFoodShopXMTest {
        open fun orderFood() {
            // 這里還包括一些點餐的其他邏輯
            println("老板,隨便來電吃的")
        }
    }
    
    class FastFoodShopXMTestInstantNoodles: FastFoodShopXMTest() {
        override fun orderFood() {
            // 這里還包括一些點餐的其他邏輯
            println("老板,來份方便面")
        }
    }
    
    class FastFoodShopXMTestHandCake: FastFoodShopXMTest() {
        override fun orderFood() {
            // 這里還包括一些點餐的其他邏輯
            println("老板,來份手抓餅")
        }
    }
    
    fun main() {
        FastFoodShopXMTest().orderFood()
        FastFoodShopXMTestInstantNoodles().orderFood()
        FastFoodShopXMTestHandCake().orderFood()
    }
    
    // 老板,隨便來電吃的
    // 老板,來份方便面
    // 老板,來份手抓餅
    

    這里,我們將變化的類擴大到了快餐店,同樣實現(xiàn)了需求。但先不說有多個快餐店分別對應(yīng)某種食物很不符合正常邏輯(這只是一個明顯的例子),在orderFood中也并不是只包括與食物相關(guān)的邏輯(如點餐收銀),而這部分邏輯是相似且可復(fù)用的??梢韵胂?,這部分邏輯通過復(fù)制粘貼的方式重復(fù)出現(xiàn)在各個實現(xiàn)類中,極大增加了代碼復(fù)雜度(這里還要單獨覆蓋測試等,因為你不能跟測試說這幾段代碼是一樣的,所以測試覆蓋率可以直接翻倍)。甚至一些不熟悉代碼的同學還會自己重復(fù)實現(xiàn)一遍,而讓這段邏輯的正確性更難以保證。

    這里,當然也有解決方案,那就是將其他邏輯提取封裝到抽象類中,并在子類中復(fù)用(但誰能保證子類一定調(diào)用已經(jīng)按照正確順序調(diào)用這些邏輯方法?)。甚至使用模版方法模式,將流程中與食物相關(guān)的邏輯封裝為抽象方法,交由具體類去實現(xiàn)(這里,不感覺邏輯已經(jīng)更復(fù)雜了嗎?而且與食物相關(guān) (某個單獨流程) 的代碼也無法單獨復(fù)用。當流程中變化的東西開始增加,你要寫多少類才能覆蓋所有情況?)。解決問題的辦法肯定不止一條,但如果要引入大量代碼來解決問題,為什么不一開始就不讓問題發(fā)生?

    對變化本身小而精巧的描述,是其復(fù)用性提升的關(guān)鍵,而我們可以靈活復(fù)用的單位,自然是對象或類。

    通過傳遞參數(shù)讓復(fù)用單位從類下降到對象也是一個實用的技巧。(但本文暫不具體討論)

  • 封裝變化:將變化的影響范圍縮到最小

    本例中,我們將變化本身(從抽象食物衍生出具體食物)封裝為一組類,同時在Food中只對外暴露一個sellFood接口,外部只用也只可以調(diào)用它,相比于暴露出sellFood內(nèi)部的多個具體步驟,雖然少了一些使用的靈活性(請問:這些靈活性是該場景下有必要的嗎?),但卻讓外面了解了變化最少的細節(jié),減少了變化對外的影響,也讓變化的拓展更靈活豐富。

  • 支持變化

    本例中,我們提取公共特征與相同的抽象業(yè)務(wù)形式(sellFood)到抽象類Food,并將不同的實現(xiàn)延遲到具體類,同時每個類都自描述了與其變化的具體實現(xiàn)相關(guān)的邏輯,無需外界為其補充描述,拓展時對外部影響?。ɑ緹o需修改外部代碼),對繼續(xù)變化支持較好。

2.3.2.用面向?qū)ο笸卣狗N類

什么是變化:拓展種類的例子中,為了拓展新的種類,提取包含公共特征和接口的抽象類,作為拓展的基準,來拓展新類。面向?qū)ο笸ㄟ^繼承實現(xiàn)從抽象類拓展新類,通過繼承表現(xiàn)出來的多態(tài)作為拓展的子類變化的部分的不同表現(xiàn)。

將該例子實現(xiàn)為代碼,并針對怎么面向變化編程所講進行簡要說明(詳細說明請見用面向?qū)ο笱苌毠?jié),與此類似),如下:

原本小紅的快餐店只有牛肉包子,

class BeefBun {
    fun sellBun() {
        println("老板,來份牛肉包子")
    }
}

class FastFoodShopXH {
    fun orderFood(beefBun: BeefBun) {
        beefBun.sellBun()
    }
}

后來小紅拓展了包子業(yè)務(wù),提取包子的抽象類,并新增了豬肉包子,

abstract class Bun() {
    abstract fun sellBun()
}

class BeefBun: Bun() {
    override fun sellBun() {
        println("老板,來份牛肉包子")
    }
}

class PorkBun: Bun() {
    override fun sellBun() {
        println("老板,來份豬肉包子")
    }
}

class FastFoodShopXH {
    fun orderFood(bun: Bun) {
        bun.sellBun()
    }
}

fun main() {
    val fastFoodShopXH = FastFoodShopXH()
    fastFoodShopXH.orderFood(BeefBun())
    fastFoodShopXH.orderFood(PorkBun())
}

// 老板,來份牛肉包子
// 老板,來份豬肉包子

本例中,通過提取公共特征與接口到抽象類Bun,我們將拓展種類的變化單獨封裝到每個具體子類中,變化對外部透明(外部統(tǒng)一使用Bun操作具體的包子類),同時讓變化(不同的包子種類)自描述,減少對不同包子種類對外部對影響,對繼續(xù)變化(增加新的包子種類)成本較小。

2.3.3.用面向?qū)ο蠓庋b差異

什么是變化:封裝差異的例子中,需要對外部隱藏差異,而在不同情況下有不同的表現(xiàn)的要求,不就是多態(tài)嗎?而在面對對象中想要實現(xiàn)多態(tài),就要使用繼承機制,繼承是面對對象實現(xiàn)多態(tài)的方法。

將該例子實現(xiàn)為代碼,并針對怎么面向變化編程所講進行簡要說明(詳細說明請見用面向?qū)ο笱苌毠?jié),與此類似),如下:

小明和小紅的快餐店在以上代碼的基礎(chǔ)上,合并為一個快餐店,并封裝菜單新的層級類別以及小明和小紅分別對幾種食物的制作過程,

open class Food {
    open fun sellFood() {
        println("老板,隨便來電吃的")
    }
}

class InstantNoodles: Food() {
    override fun sellFood() {
        println("老板,來份方便面。(小明制作)")
    }
}

class HandCake: Food() {
    override fun sellFood() {
        println("老板,來份手抓餅。(小明制作)")
    }
}

abstract class Bun: Food() {
    final override fun sellFood() {
        sellBun()
    }

    abstract fun sellBun()
}

class BeefBun: Bun() {
    override fun sellBun() {
        println("老板,來份牛肉包子。(小紅制作)")
    }
}

class PorkBun: Bun() {
    override fun sellBun() {
        println("老板,來份豬肉包子。(小紅制作)")
    }
}

class FastFoodShop {
    fun orderFood(food: Food) {
        food.sellFood()
    }
}

fun main() {
    val fastFoodShop = FastFoodShop()
    fastFoodShop.orderFood(BeefBun())
    fastFoodShop.orderFood(InstantNoodles())
    fastFoodShop.orderFood(PorkBun())
    fastFoodShop.orderFood(HandCake())
    fastFoodShop.orderFood(Food())
}

// 老板,來份牛肉包子。(小紅制作)
// 老板,來份方便面。(小明制作)
// 老板,來份豬肉包子。(小紅制作)
// 老板,來份手抓餅。(小明制作)
// 老板,隨便來電吃的

本例中,我們通過繼承實現(xiàn)了一組不斷變化的食物類別族,允許我們通過抽象的類別統(tǒng)一對待它的子類別。它們在不同層級上提取并復(fù)用了公共的特征與接口,并表現(xiàn)了具體食物種類、制作過程的多態(tài)。由此,我們通過面向?qū)ο蠓庋b了差異。

通過以上三個例子,我們可以很清楚的發(fā)現(xiàn),我們就在使用封裝和繼承,其表現(xiàn)是透明和多態(tài),通過這種抽象與實現(xiàn)去封裝變化。而變化,自然是代碼中最不穩(wěn)定也是最需要復(fù)用的部分。我們準確封裝好變化,每個模塊都是穩(wěn)定的部分,那不就是高內(nèi)聚嗎?模塊中的變化準確封裝,可以獨立靈活的變化,且對外暴露的變化接口最小,那其他模塊自然沒有耦合之說,自然實現(xiàn)了低耦合。

當然,面向?qū)ο蟛皇欠庋b變化唯一的途徑,甚至不能說最好(因為在不同場景下有不同情況),但絕對是適用性最廣且相性極好的方式。但是,在實際情況下,我們也要根據(jù)情況靈活選擇合適的方式去面向變化編程。注意,核心是面向變化編程,面向?qū)ο笾皇且环N手段。

3.解讀面向?qū)ο蟮牧笤瓌t

首先,直接給出面向?qū)ο蟮牧笤瓌t:

  • 里氏替換原則(Liskov Substitution Principle)
  • 單一職責原則(Single Responsibility Principle)
  • 開閉原則(Open Closed Principle)
  • 迪米特法則(Law of Demeter),又叫“最少知道法則”
  • 接口隔離原則(Interface Segregation Principle)
  • 依賴倒置原則(Dependence Inversion Principle)

可以說,以上六大原則,都是面對對象為了更好的服務(wù)于“面向變化編程”的。

3.1.里氏替換原則

里氏替換原則:如果對每一個類型為S的對象o1,都有類型為T的對象o2,使得以T定義的所有程序P在所有的對象o1都代替o2時,程序P的行為沒有發(fā)生變化,那么類型S是類型T的子類型。

簡單來說:子類永遠可以替換父類,且不會造成錯誤、異常

這是一個用來規(guī)范繼承的原則,該原則包含四層含義:1. 子類必須完全實現(xiàn)父類的方法;2. 子類可以有自己的實現(xiàn);3. 覆蓋或?qū)崿F(xiàn)父類的方法時,輸入?yún)?shù)可以放大;4. 覆蓋或?qū)崿F(xiàn)父類的方法時,返回值可以縮小。

通過該原則,繼承標準的實現(xiàn)了一個“is-a”語義,讓父類是子類的公共特征與接口的集合,可以從抽象層面代指子類。也是通過這樣一個標準的抽象與實現(xiàn)的語義,才可以讓我們將變化的抽象公共特征提取,并交由子類去實現(xiàn)變化的細節(jié)。

3.2.單一職責原則

單一職責原則:一個類應(yīng)該有且僅有一個原因引起類的變更

單一職責原則要求一個接口或一個類只能有一個原因引起變化,也就是一個接口或者類只能有一個職責,它就負責一件事情。

從定義上,我們就可以看出,這是在控制變化。在面向變化編程中我們講到,我們要封裝變化,讓變化本身最小,同時讓變化對外暴露的接口最小??梢哉f,每一個變化,就對應(yīng)一個類,二者是一對一的關(guān)系,變化本身是獨立的。而一個抽象類則對應(yīng)了一組相關(guān)的變化。

讓變化本身最小,就是在告訴我們每個對變化的封裝只包含一個變化,且不包含與變化無關(guān)的邏輯部分。

讓變化對外暴露的接口最小,則是在說抽象變化的宏觀表現(xiàn),將每個完整而獨立的功能當成一個整體。簡單來說,就是多個相同層面的功能不能放到一起,盡可能暴露更抽象層面的表現(xiàn)接口。

3.3.開閉原則

開閉原則:類應(yīng)該對拓展開放,對修改關(guān)閉

對拓展開放,對修改關(guān)閉,不正是面向變化的編程方法——支持變化的表述?

我們通過提取抽象的公共特征與接口作為對外表現(xiàn)接口來屏蔽外部對內(nèi)部變化的感知,通過繼承實現(xiàn)包含不同細節(jié)的具體類進行拓展,通過盡可能的自描述與其他優(yōu)化方法降低外部對具體類的引用。

3.4.最少知道原則

最少知道法則:一個對象應(yīng)該對其他對象有最少的了解

最少知道原則讓我們盡可能少的允許直接引用某個對象,同時盡可能少的給外部(包括可以直接引用的“朋友”)暴露自己的細節(jié)。

在面向變化編程中,要求封裝變化,每個變化都對應(yīng)一個類,每個變化都只在有此變化的類中被引用。同時,針對變化良好的封裝要求我們控制變化對外部的影響,只提供更抽象的表現(xiàn)接口。由此,我們要暴露變化本身最宏觀的表現(xiàn),讓外部了解變化最少的細節(jié)。

3.5.依賴倒置原則

依賴倒置原則:要依賴抽象,不要依賴具體的類

類之間的依賴關(guān)系也是變化之間的依賴關(guān)系。該原則要求我們面向接口編程,而不是面向?qū)崿F(xiàn)。換句話說,是要求我們依賴變化的抽象類(包含了變化的公共特征與抽象表現(xiàn)接口),而不是包含變化細節(jié)的具體類。

本原則,正是我們對變化的處理方法。面向變化編程要求我們在外部關(guān)心的是變化的抽象表現(xiàn),而變化的細節(jié)全部封裝到內(nèi)部。這不但是封裝變化的要求,同時也是支持變化的基礎(chǔ)。

3.6.接口隔離原則

接口隔離原則:不應(yīng)該依賴它不需要的接口,類間的依賴關(guān)系應(yīng)該建立在最小的接口上

該原則要求我們明確真正獨立改變的變化到底是什么。這就對應(yīng)我們封裝變化的關(guān)鍵之一:將變化縮小為它本質(zhì)的樣子。

通過切實了解到底是什么引發(fā)了變化,進而確定每組變化的公共特征與抽象外在表現(xiàn),才能保證變化的獨立性和靈活性,才能保證項目的可拓展和代碼的可復(fù)用性。

4.解讀設(shè)計模式

在這里,先澄清,將設(shè)計模式放到這里,并不是說設(shè)計模式屬于面對對象的一部分。二者是并行的,但設(shè)計模式在標準實現(xiàn)中用了面向?qū)ο?,同時符合良好的面向?qū)ο笤O(shè)計的思想(也就是符合面向變化編程的思想。但注意,面向變化編程也是面向?qū)ο蟛⑿械?,可?a href="#2.%E9%9D%A2%E5%90%91%E2%80%9C%E5%8F%98%E5%8C%96%E2%80%9D%E7%BC%96%E7%A8%8B" target="_blank">面向變化編程末尾總結(jié)部分),所以經(jīng)常將設(shè)計模式當成面對對象的進階知識。

4.1.解讀“多用組合,少用繼承”

在具體解讀設(shè)計模式之前,先問大家一個問題:你們知道為啥要“多用組合,少用繼承”嗎?

這個問題,直接關(guān)系到大家最這句話本身內(nèi)涵的理解。

從事物的關(guān)系層面理解,組合是“has-a”關(guān)系,而繼承是“is-a”關(guān)系。在我看來,事物與事物有以下五種關(guān)系:無關(guān)系、“use-a”、“has-a”、“implement-a”、“is-a”,嚴格意義上,“implement-a”是“is-a”在純接口情況下的一種特殊情形。而事物與事物的關(guān)系,就直接回答了我要用哪種選擇。

如:策略模式,將一組算法封裝(算法就是其變化的細節(jié)),而算法/策略相對于當前使用它的主體,就是一種“has-a”的關(guān)系,使用的是組合。而裝飾者模式,不管是裝飾器還是被裝飾的基本實現(xiàn)類,都是抽象類,雖然裝飾類可以不斷組合實現(xiàn)這個抽象類的具體類,但不管套了多少層,相對于抽象類,都是“is-a”關(guān)系,使用的是繼承;而裝飾類相對于被裝飾對象,則是一種“has-a”關(guān)系,使用的是組合。

從封裝變化的方法角度講,二者則沒有區(qū)別,只是看問題的粒度不同。支持用組合,是因為可以它可以細粒度的動態(tài)改變,表現(xiàn)出多態(tài)。但這種變化,不也是繼承變化的抽象類實現(xiàn)的?話又說回來了,面向?qū)ο笫峭ㄟ^繼承實現(xiàn)的多態(tài)。而用繼承,在使用它的層面,不也是組合嗎?所以,區(qū)分二者,其實是在強調(diào)封裝變化的粒度問題,也就是封裝變化講到的:將變化縮小到它本質(zhì)的樣子,而具體的例子,也在用面向?qū)ο笱苌毠?jié)封裝變化:將變化縮小為它本質(zhì)的樣子中講到。

4.2.解讀“工廠”

工廠有三種:簡單工廠(不算一個設(shè)計模式,而是一個設(shè)計習慣)、工廠方法和抽象工廠。

簡單工廠,可以根據(jù)一些條件去有選擇的生產(chǎn)(實例化病返回)一個產(chǎn)品(具體類)。這就是在封裝變化,更具體來說,就是封裝對變化的具體引用,并將工廠作為了解具體變化的唯一位置。由此,可以更好的支持變化。

工廠方法,可以在不同的實現(xiàn)類中決定實際生產(chǎn)(實例化并返回)的產(chǎn)品(具體類),將具體類的引用與選擇延遲到子類中。這也是一種變相的對變化的具體引用的封裝,向外部透明具體的變化。

抽象工廠,可以定義一組生產(chǎn)相關(guān)產(chǎn)品的方法,基于此產(chǎn)生不同“風格”的工廠,每個工廠可以生產(chǎn)該“風格”的一組相關(guān)產(chǎn)品。這可以說是對工廠方法的拓展,工廠方法只能生產(chǎn)一種抽象種類的產(chǎn)品,抽象工廠可以生產(chǎn)一組抽象種類的產(chǎn)品。其目的,也是對變化(成組統(tǒng)一的變化)的封裝。

4.3.解讀“觀察者模式”

觀察者模式是最常用的設(shè)計模式之一,通過訂閱和發(fā)布的形式實現(xiàn)一個消息的通知與同步,是一種一對多的依賴關(guān)系。

那觀察者模式是在處理哪類問題?或者說,是在處理什么變化?

顧名思義,這是在處理觀察者的變化。對某個發(fā)布的主題訂閱的觀察者是不斷在變化的,數(shù)量不定,具體類型不定。但只要實現(xiàn)了抽象觀察者類,向被觀察者注冊,就可以收到對應(yīng)的消息。抽象的觀察者類,就是觀察者的變化的公共特征和抽象對外接口的集合,并基于此產(chǎn)生了與“觀察”相關(guān)的一組具體變化。

4.4.解讀設(shè)計模式

設(shè)計模式還有很多,以上只是舉了幾個例子。設(shè)計模式的準則就是:1)中意于組合而不是繼承,2)依賴于接口而不是實現(xiàn),3)高內(nèi)聚,低耦合。而這些也正是面向變化編程的描述。每一個設(shè)計模式都有對應(yīng)的使用場景,每一種場景描述的正是一種常見變化(單例模式亦可看作一種特殊的變化:限制變化,只可產(chǎn)生一個實例)。而設(shè)計模式應(yīng)對這些常見變化的方法,亦是處理這些變化的經(jīng)典方法,是好的面向變化編程的范例。

5.從需求中來,到需求中去

從一開始講面對對象的基本概念與思想,到后來提出面向“變化”編程、詳細講解如何面向“變化”編程、如何通過面對對象實現(xiàn)面向“變化”編程,再到后來通過面向“變化”的視角解讀面向?qū)ο蟮牧笤瓌t與設(shè)計模式,來說明與論證面向“變化”的地位,我們已經(jīng)從淺入深的講解與論證了基于面向“變化”的良好面向?qū)ο笤O(shè)計方法的定義、方法與作用,但有一點沒有說明——那就是怎么發(fā)現(xiàn)“變化”。

如何發(fā)現(xiàn)“變化”可謂是面向變化編程的重中之重。如何找到“變化”,如何找準“變化”的本質(zhì),如何確定變化最抽象的外在表現(xiàn),直接關(guān)系到我們能否成功封裝“變化”,能否良好的封裝“變化”,能否讓項目可以繼續(xù)靈活變化。

5.1.從哪里發(fā)現(xiàn)“變化”?

要問一切的變化從哪里來,那自然是從需求中來

要問一切的變化到哪里去,那一定是到需求中去!

可以說,需求,即是變化、是項目本身的目的與意義!

那從哪里發(fā)現(xiàn)“變化”?那當然是從需求中發(fā)現(xiàn)變化。

面向?qū)ο缶幊叹褪侨绱?,它并非出自本能或技術(shù)本身,而是需要我們先理解需求,再確定有哪些類與對象、對象與對象之間有哪些交互,最后進行實現(xiàn)。

而面向變化編程則進一步要求我們,不光要分析需求中的基本元素及交互,同時要找到需求中的變化。

代碼是對應(yīng)需求的,代碼中對變化的封裝即是對需求中的變化進行封裝,代碼中支持新的變化即是支持新的需求中的變化。我們找到變化、封裝變化、支持變化,都是為了更好的應(yīng)對需求。

5.2.如何發(fā)現(xiàn)“變化”?

那么,如何在需求中發(fā)現(xiàn)“變化”呢?

首先,是去熟悉、理解需求,知道需求有什么、什么是重點、怎么變化的(需求的發(fā)展與變更歷史)。

接下來,關(guān)注需求中頻繁修改、新增、刪除的部分,確定它們的范圍、修改的原因和目的。

最后,依據(jù)變化的基本類別,對需求中的變化進行定性分析,并進一步迭代其范圍,最終確定需求中的變化。

5.3.哪里才是真正的變化?

讀完了如何發(fā)現(xiàn)“變化”,我想絕大多數(shù)同學都還是一頭霧水。

沒錯,如果光了解方法,我也不清楚哪里才是真正的變化。發(fā)現(xiàn)“變化”沒有什么捷徑或竅門(至少我沒找到),只有繼續(xù)去熟悉業(yè)務(wù)、熟悉需求。想來也是這個道理,一個業(yè)務(wù)不熟的程序猿連完成一個普通業(yè)務(wù)需求都很陌生緩慢,更不要說去設(shè)計項目了。一個好的架構(gòu)師想必不但有著豐富的架構(gòu)知識與經(jīng)驗,也一定對項目有著無與倫比的熟悉度與見解。

但誰都是慢慢才了解業(yè)務(wù)的,需要一個熟悉的過程,并在這個過程中不斷去理解業(yè)務(wù),尋找真正的變化。在這里,溫馨提示,不要著急,不要看到需求中比較直觀的改變就認為它是真正的變化。請冷靜分析、沉著思考是不是背后還有更本質(zhì)的變化形式。當然,如果有把握,就放心大膽的封裝你找到的變化吧。因為除了神,沒有人能找到變化的最本質(zhì)形式。或者最本質(zhì),那可能就是“一”,已經(jīng)沒有實際的參考價值了,而對變化相對最合適的解讀,需要你在抽象與實際間去平衡。反復(fù)迭代對變化的封裝,也是封裝變化重要的方法與必經(jīng)的步驟。而許多小的重構(gòu)與優(yōu)化,也往往可以促使發(fā)現(xiàn)更大的重構(gòu)機會。

6.面向?qū)ο蟮哪嗾?/h2>

6.1.面對對象的過度崇拜

面向?qū)ο笠呀?jīng)火了很多個年頭,并作為編程教學內(nèi)容、面試與工作的必考知識而廣為流傳。但作為一名程序猿,總發(fā)現(xiàn)身邊或網(wǎng)上存在對面對對象的過度崇拜、對設(shè)計模式的過度崇拜,一言不合,就各種方法、模式往上堆,甚至過度設(shè)計而不知。

面對對象固然是一個解決現(xiàn)實中問題的一個普適性良好的編程范式與思想,但絕非任何情形都是好的選擇,而連編程范式或思想都不算、只適用于一些特定場合的設(shè)計模式就更是這樣了。

面向“變化”編程章節(jié)所講的快餐店的例子,變化的部分是食物的種類與制作,也可以說是菜單。那么,不說更復(fù)雜的情況,完全可以通過一個配置文件去實現(xiàn)菜單的描述,通過一個菜單類去封裝對配置文件的讀取與解析,拓展性與靈活性遠高于面向?qū)ο笕シ庋b的同時,代碼也更加簡潔清晰。(當然,這里我不是在自黑上面所講的面向?qū)ο蟮睦?,就像例子中的注釋所寫,其中省略了很多涉及多個對象的復(fù)雜操作步驟。隨著復(fù)雜度的增加,就去權(quán)衡這種面向數(shù)據(jù)的編程方式和面向?qū)ο蟮睦琢耍?/p>

當然,有人“夸”面對對象,自然有人黑面向?qū)ο?,把它說的好像一無是處。我的觀點,是不信謠,不傳謠。同時,我對面向?qū)ο蟮乃枷胧终J可,認為這是對實際生活非常好的描述方式。

6.2.面向?qū)ο笾皇呛玫慕鉀Q方案的一部分

在快餐店的“面向數(shù)據(jù)”實現(xiàn)方式中可以看到,此處主要用了一個文件表格去封裝食物變化的差異。但眼尖的同學可能已經(jīng)看到,這里是用類去封裝配置文件的讀取與解析的。

在前面講過,一個類,就對應(yīng)一個變化。那這里封裝的是什么變化呢?是對不同配置文件的解析方式。由此看出,面對對象可以廣泛的出現(xiàn)在編程的各種場合,而在很多時候,面對對象是好的解決方案的一部分,但也只是一部分。

6.3.學會設(shè)計本身

想要設(shè)計出好的解決方案,就要學會設(shè)計本身,而不能局限于任何一個編程范式中,什么面向過程、面向?qū)ο?、函?shù)式編程、響應(yīng)式編程等等,都平等對待,按需使用。而好的設(shè)計最核心、也是最主要的,就是面向“變化”編程。而想要找到“變化”,就要回到具體的需求中去尋找答案。

面向?qū)ο蟊揪褪浅橄蟮乃枷耄O(shè)計的路更是玄而又玄,學習的路還有很長,需要足夠的知識與經(jīng)驗積累,也需要有好的項目機遇。希望每一個小伙伴都能在編寫更好的代碼的道路上不斷砥礪前行!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容