Character Specifications for A Word in Golang

序言

筆者有幸參加了一次Code Retreat活動,整個過程很有收獲,本文通過Golang語言來回放一下。

需求一:判斷某個單詞是否包含數(shù)字

這個需求比較簡單,代碼實(shí)現(xiàn)如下:

func HasDigit(word string) bool {
    for _, c := range word {
        if unicode.IsDigit(c) {
            return true
        }
    }
    return false
}

需求二:判斷某個單詞是否包含大寫字母

有了需求一的基礎(chǔ)后,可以通過copy-paste快速實(shí)現(xiàn)需求二:

func HasDigit(word string) bool {
    for _, c := range word {
        if unicode.IsDigit(c) {
            return true
        }
    }
    return false
}

func HasUpper(word string) bool {
    for _, c := range str {
        if unicode.IsUpper(c) {
            return true
        }
    }
    return false
}

很明顯,HasDigit函數(shù)和HasUpper函數(shù)除過if的條件判斷外,其余代碼都一樣,所以我們使用抽象這個強(qiáng)大的屠龍刀來消除重復(fù):

  1. 定義一個接口CharSpec,作為所有字符謂詞的抽象,方法Satisfy用來判斷謂詞是否為真
  2. 針對需求一定義具有原子語義的謂詞IsDigit
  3. 針對需求二定義具有原子語義的謂詞IsUpper

謂詞相關(guān)代碼實(shí)現(xiàn)如下:

type CharSpec interface {
    Satisfy(c rune) bool
}

type IsDigit struct {

}

func (i IsDigit) Satisfy(c rune) bool {
    return unicode.IsDigit(c)
}

type IsUpper struct {

}

func (i IsUpper) Satisfy(c rune) bool {
    return unicode.IsUpper(c)
}

要完成需求,還必須將謂詞注入給單詞的Has語義函數(shù),而Exists具有Has語義,同時表達(dá)力很強(qiáng):

func Exists(word string, spec CharSpec) bool {
    for _, c := range word {
        if spec.Satisfy(c) {
            return true
        }
    }
    return false
}

通過Exists判斷某個單詞word是否包含數(shù)字:

isDigit := IsDigit{}
ok := Exists(word, isDigit)
...

通過Exists判斷某個單詞word是否包含大寫字母:

isUpper := IsUpper{}
ok := Exists(word, isUpper)
...

其實(shí)需求二的故事還沒講完:)

對于普通的程序員來說,能完成上面的代碼已經(jīng)很好了,而對于經(jīng)驗(yàn)豐富的程序員來說,在需求一剛完成后可能就發(fā)現(xiàn)了新的變化方向,即單詞的Has語義和字符的Is語義是兩個不同的變化方向,所以在需求二開始前就通過重構(gòu)分離了變化方向:

type IsDigit struct {

}

func (i IsDigit) Satisfy(c rune) bool {
    return unicode.IsDigit(c)
}

func Exists(word string, spec IsDigit) bool {
    for _, c := range word {
        if spec.Satisfy(c) {
            return true
        }
    }
    return false
}

通過Exists判斷某個單詞word是否包含數(shù)字:

isDigit := IsDigit{}
ok := Exists(word, isDigit)
...

在需求二出來后,謂詞被第一顆子彈擊中,我們根據(jù)Uncle Bob的建議,應(yīng)用開放封閉原則,于是也就寫出了普通程序員在需求二中消除重復(fù)后的代碼。

殊途同歸,這并不是巧合,而是有理論依據(jù)。

我們一起回顧一下 袁英杰先生 提出的正交設(shè)計(jì)四原則

  1. 一個變化導(dǎo)致多處修改:消除重復(fù)
  2. 多個變化導(dǎo)致一處修改:分離不同的變化方向
  3. 不依賴不必要的依賴:縮小依賴范圍
  4. 不依賴不穩(wěn)定的依賴:向著穩(wěn)定的方向依賴

這四個原則的提出是針對簡單設(shè)計(jì)四原則中的第二條“消除重復(fù)”,使得目標(biāo)的達(dá)成有章可循。我們應(yīng)用正交設(shè)計(jì)四原則,可以將系統(tǒng)分解成很多單一職責(zé)的小類(也有一些小函數(shù)),然后再將它們根據(jù)需要而靈活的組合起來。

細(xì)細(xì)品味正交設(shè)計(jì)四原則,你就會發(fā)現(xiàn):第一條是被動策略,而后三條是主動策略。這就是說,第一條是一種事后補(bǔ)救的策略,而后三條是一種事前預(yù)防的策略,目標(biāo)都是為了消除重復(fù)。

從上面的分析可以看出,普通的程序員習(xí)慣使用被動策略,而經(jīng)驗(yàn)豐富的程序員更喜歡使用主動策略。Anyway,他們殊途同歸,都消除了重復(fù)。

需求三:判斷某個單詞是否包含_

不管是包含下劃線還是中劃線,都有原子語義Equals,我們將代碼快速實(shí)現(xiàn):

type Equals struct {
    c rune
}

func (e Equals) Satisfy(c rune) bool {
    return c == e.c
}

通過Exists判斷某個單詞word是否包含_:

isUnderline := Equals{'_'}
ok := Exists(word, isUnderline)
...

需求四:判斷某個單詞是否不包含_

字母是下劃線的謂詞是Equals,那么字母不是下劃線的謂詞就是給Equals前增加一個修飾語義Not,Not修飾謂詞后是一個新的謂詞,代碼實(shí)現(xiàn)如下:

type Not struct {
    spec CharSpec
}

fun (n Not) Satisfy(c rune) bool {
    return !n.spec.Satisfy(c)
}

單詞不包含下劃線,就不是Exists語義了,而是ForAll語義,代碼實(shí)現(xiàn)如下:

func ForAll(word string, spec CharSpec) bool {
    for _, c := range str {
        if !spec.Satisfy(c) {
            return false
        }
    }
    return true
}

通過ForAll判斷某個單詞word是否不包含_:

isNotUnderline := Not{Equals{'_'}}
ok := ForAll(word, isNotUnderline)
...

功能雖然實(shí)現(xiàn)了,但是我們發(fā)現(xiàn)Exists函數(shù)和ForAll函數(shù)有很多代碼是重復(fù)的,使用重構(gòu)基本手法Extract Method:

func expect(word string, spec CharSpec, ok bool) bool {
    for _, c := range word {
        if spec.Satisfy(c) == ok {
            return ok
        }
    }
    return !ok
}

func Exists(word string, spec CharSpec) bool {
    return expect(word, spec, true)
}

func ForAll(word string, spec CharSpec) bool {
    return expect(word, spec, false)
}

需求五:判斷某個單詞是否包含_或者*

字母是x或y的謂詞具有組合語義AnyOf,其中x為Equals{'_'},y為Equals{'*'},代碼實(shí)現(xiàn)如下:

type AnyOf struct {
    specs []CharSpec
}

func (a AnyOf) Satisfy(c rune) bool {
    for _, spec := range a.specs {
        if spec.Satisfy(c) {
            return true
        }
    }
    return false
}

通過Exists判斷某個單詞word是否包含_或*:

isUnderlineOrStar := AnyOf{[]CharSpec{Equals{'_'}, Equals{'*'}}}
ok := Exists(word, isUnderlineOrStar)
...

需求六:判斷某個單詞是否包含空白符,但除去空格

空白符包括空格、制表符和換行符等,具體見下面代碼:

func IsSpace(r rune) bool {
    // This property isn't the same as Z; special-case it.
    if uint32(r) <= MaxLatin1 {
        switch r {
        case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0:
            return true
        }
        return false
    }
    return isExcludingLatin(White_Space, r)
}

字母是空白符的謂詞還沒有實(shí)現(xiàn),我們定義具有原子語義的謂詞IsSpace:

type IsSpace struct {

}

func (i IsSpace) Satisfy(c rune) bool {
    return unicode.IsSpace(c)
}

字母是x和y的謂詞具有組合語義AllOf,其中x為IsSpace,y為Not{Equals{' '}},代碼實(shí)現(xiàn)如下:

type AllOf struct {
    specs []CharSpec
}

func (a AllOf) Satisfy(c rune) bool {
    for _, spec := range a.specs {
        if !spec.Satisfy(c) {
            return false
        }
    }
    return true
}

通過Exists判斷某個單詞word是否包含空白符,但除去空格:

isSpaceAndNotBlank := AllOf{[]CharSpec{IsSpace{}, Not{Equals{' '}}}}
ok := Exists(word, isSpaceAndNotBlank)
...

需求七:判斷某個單詞是否包含字母x,且不區(qū)分大小寫

不區(qū)分大小寫是一種具有修飾語義的謂詞IgnoreCase,代碼實(shí)現(xiàn)如下:

type IgnoreCase struct {
    spec CharSpec
}

func (i IgnoreCase) Satisfy(c rune) bool {
    return i.spec.Satisfy(unicode.ToLower(c))
}

通過Exists判斷某個單詞word是否包含字母x,且不區(qū)分大小寫:

isXIgnoreCase := IgnoreCase{Equals{'x'}}
ok := Exists(word, isXIgnoreCase)
...

小結(jié)

在需求的實(shí)現(xiàn)過程中,我們不斷應(yīng)用正交設(shè)計(jì)四原則,最終得到了很多小類(也有一些小函數(shù)),再通過組合來完成一個個既定需求,不但領(lǐng)域表達(dá)力強(qiáng),而且非常靈活,容易應(yīng)對變化。

字符的謂詞是本文的重點(diǎn),有以下三類

分類 謂詞
原子語義 IsDigit
IsUpper
Equals
IsSpace
修飾語義 Not
IgnoreCase
組合語義 AnyOf
AllOf

它的領(lǐng)域模型如下所示:

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

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

  • //Clojure入門教程: Clojure – Functional Programming for the J...
    葡萄喃喃囈語閱讀 4,050評論 0 7
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,728評論 18 399
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,062評論 25 709
  • 從地鐵站出來,外面的風(fēng)還有點(diǎn)冷。拿著手中的地圖開始了這段旅途。 在路上的開始,似乎沒有那么順利。市區(qū)的路線很復(fù)雜,...
    東東方閱讀 913評論 0 4
  • 在廣州吃吃喝喝的旅行即將結(jié)束。按原計(jì)劃,今天要來珠海,準(zhǔn)備明天去澳門。所以早上就沒作其他安排,在酒店休息。 中午退...
    茗心M閱讀 304評論 0 2

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