序言
筆者有幸參加了一次Code Retreat活動(dòng),整個(gè)過(guò)程很有收獲庐冯,本文通過(guò)Golang語(yǔ)言來(lái)回放一下孽亲。
需求一:判斷某個(gè)單詞是否包含數(shù)字
這個(gè)需求比較簡(jiǎn)單,代碼實(shí)現(xiàn)如下:
func HasDigit(word string) bool {
for _, c := range word {
if unicode.IsDigit(c) {
return true
}
}
return false
}
需求二:判斷某個(gè)單詞是否包含大寫(xiě)字母
有了需求一的基礎(chǔ)后展父,可以通過(guò)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ù)除過(guò)if的條件判斷外,其余代碼都一樣栖茉,所以我們使用抽象這個(gè)強(qiáng)大的屠龍刀來(lái)消除重復(fù):
- 定義一個(gè)接口CharSpec篮绿,作為所有字符謂詞的抽象,方法Satisfy用來(lái)判斷謂詞是否為真
- 針對(duì)需求一定義具有原子語(yǔ)義的謂詞IsDigit
- 針對(duì)需求二定義具有原子語(yǔ)義的謂詞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語(yǔ)義函數(shù)亲配,而Exists具有Has語(yǔ)義,同時(shí)表達(dá)力很強(qiáng):
func Exists(word string, spec CharSpec) bool {
for _, c := range word {
if spec.Satisfy(c) {
return true
}
}
return false
}
通過(guò)Exists判斷某個(gè)單詞word是否包含數(shù)字:
isDigit := IsDigit{}
ok := Exists(word, isDigit)
...
通過(guò)Exists判斷某個(gè)單詞word是否包含大寫(xiě)字母:
isUpper := IsUpper{}
ok := Exists(word, isUpper)
...
其實(shí)需求二的故事還沒(méi)講完:)
對(duì)于普通的程序員來(lái)說(shuō)惶凝,能完成上面的代碼已經(jīng)很好了吼虎,而對(duì)于經(jīng)驗(yàn)豐富的程序員來(lái)說(shuō),在需求一剛完成后可能就發(fā)現(xiàn)了新的變化方向苍鲜,即單詞的Has語(yǔ)義和字符的Is語(yǔ)義是兩個(gè)不同的變化方向思灰,所以在需求二開(kāi)始前就通過(guò)重構(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
}
通過(guò)Exists判斷某個(gè)單詞word是否包含數(shù)字:
isDigit := IsDigit{}
ok := Exists(word, isDigit)
...
在需求二出來(lái)后,謂詞被第一顆子彈擊中混滔,我們根據(jù)Uncle Bob的建議洒疚,應(yīng)用開(kāi)放封閉原則,于是也就寫(xiě)出了普通程序員在需求二中消除重復(fù)后的代碼坯屿。
殊途同歸油湖,這并不是巧合,而是有理論依據(jù)领跛。
我們一起回顧一下 袁英杰先生 提出的正交設(shè)計(jì)四原則:
- 一個(gè)變化導(dǎo)致多處修改:消除重復(fù)
- 多個(gè)變化導(dǎo)致一處修改:分離不同的變化方向
- 不依賴(lài)不必要的依賴(lài):縮小依賴(lài)范圍
- 不依賴(lài)不穩(wěn)定的依賴(lài):向著穩(wěn)定的方向依賴(lài)
這四個(gè)原則的提出是針對(duì)簡(jiǎn)單設(shè)計(jì)四原則中的第二條“消除重復(fù)”乏德,使得目標(biāo)的達(dá)成有章可循。我們應(yīng)用正交設(shè)計(jì)四原則吠昭,可以將系統(tǒng)分解成很多單一職責(zé)的小類(lèi)(也有一些小函數(shù))喊括,然后再將它們根據(jù)需要而靈活的組合起來(lái)。
細(xì)細(xì)品味正交設(shè)計(jì)四原則怎诫,你就會(huì)發(fā)現(xiàn):第一條是被動(dòng)策略,而后三條是主動(dòng)策略贷痪。這就是說(shuō)幻妓,第一條是一種事后補(bǔ)救的策略,而后三條是一種事前預(yù)防的策略,目標(biāo)都是為了消除重復(fù)肉津。
從上面的分析可以看出强胰,普通的程序員習(xí)慣使用被動(dòng)策略,而經(jīng)驗(yàn)豐富的程序員更喜歡使用主動(dòng)策略妹沙。Anyway偶洋,他們殊途同歸,都消除了重復(fù)距糖。
需求三:判斷某個(gè)單詞是否包含_
不管是包含下劃線(xiàn)還是中劃線(xiàn)玄窝,都有原子語(yǔ)義Equals,我們將代碼快速實(shí)現(xiàn):
type Equals struct {
c rune
}
func (e Equals) Satisfy(c rune) bool {
return c == e.c
}
通過(guò)Exists判斷某個(gè)單詞word是否包含_:
isUnderline := Equals{'_'}
ok := Exists(word, isUnderline)
...
需求四:判斷某個(gè)單詞是否不包含_
字母是下劃線(xiàn)的謂詞是Equals悍引,那么字母不是下劃線(xiàn)的謂詞就是給Equals前增加一個(gè)修飾語(yǔ)義Not恩脂,Not修飾謂詞后是一個(gè)新的謂詞,代碼實(shí)現(xiàn)如下:
type Not struct {
spec CharSpec
}
fun (n Not) Satisfy(c rune) bool {
return !n.spec.Satisfy(c)
}
單詞不包含下劃線(xiàn)趣斤,就不是Exists語(yǔ)義了俩块,而是ForAll語(yǔ)義,代碼實(shí)現(xiàn)如下:
func ForAll(word string, spec CharSpec) bool {
for _, c := range str {
if !spec.Satisfy(c) {
return false
}
}
return true
}
通過(guò)ForAll判斷某個(gè)單詞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)
}
需求五:判斷某個(gè)單詞是否包含_或者*
字母是x或y的謂詞具有組合語(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
}
通過(guò)Exists判斷某個(gè)單詞word是否包含_或*:
isUnderlineOrStar := AnyOf{[]CharSpec{Equals{'_'}, Equals{'*'}}}
ok := Exists(word, isUnderlineOrStar)
...
需求六:判斷某個(gè)單詞是否包含空白符,但除去空格
空白符包括空格撑蒜、制表符和換行符等歹啼,具體見(jiàn)下面代碼:
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)
}
字母是空白符的謂詞還沒(méi)有實(shí)現(xiàn),我們定義具有原子語(yǔ)義的謂詞IsSpace:
type IsSpace struct {
}
func (i IsSpace) Satisfy(c rune) bool {
return unicode.IsSpace(c)
}
字母是x和y的謂詞具有組合語(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
}
通過(guò)Exists判斷某個(gè)單詞word是否包含空白符浴滴,但除去空格:
isSpaceAndNotBlank := AllOf{[]CharSpec{IsSpace{}, Not{Equals{' '}}}}
ok := Exists(word, isSpaceAndNotBlank)
...
需求七:判斷某個(gè)單詞是否包含字母x拓萌,且不區(qū)分大小寫(xiě)
不區(qū)分大小寫(xiě)是一種具有修飾語(yǔ)義的謂詞IgnoreCase,代碼實(shí)現(xiàn)如下:
type IgnoreCase struct {
spec CharSpec
}
func (i IgnoreCase) Satisfy(c rune) bool {
return i.spec.Satisfy(unicode.ToLower(c))
}
通過(guò)Exists判斷某個(gè)單詞word是否包含字母x升略,且不區(qū)分大小寫(xiě):
isXIgnoreCase := IgnoreCase{Equals{'x'}}
ok := Exists(word, isXIgnoreCase)
...
小結(jié)
在需求的實(shí)現(xiàn)過(guò)程中微王,我們不斷應(yīng)用正交設(shè)計(jì)四原則,最終得到了很多小類(lèi)(也有一些小函數(shù))品嚣,再通過(guò)組合來(lái)完成一個(gè)個(gè)既定需求炕倘,不但領(lǐng)域表達(dá)力強(qiáng),而且非常靈活翰撑,容易應(yīng)對(duì)變化罩旋。
字符的謂詞是本文的重點(diǎn),有以下三類(lèi)
分類(lèi) | 謂詞 |
---|---|
原子語(yǔ)義 | IsDigit IsUpper Equals IsSpace |
修飾語(yǔ)義 | Not IgnoreCase |
組合語(yǔ)義 | AnyOf AllOf |
它的領(lǐng)域模型如下所示: