需求
leetcode 上有一道關(guān)于強(qiáng)密碼校驗(yàn)器的練習(xí)題,如下所述:
一個(gè)強(qiáng)密碼應(yīng)滿足以下所有條件:
- 由至少6個(gè)气笙,至多20個(gè)字符組成祸轮。
- 至少包含一個(gè)小寫字母黔姜,一個(gè)大寫字母,和一個(gè)數(shù)字晦款。
- 同一字符不能連續(xù)出現(xiàn)三次 (比如 "...aaa..." 是不允許的, 但是 "...aa...a..." 是可以的)贷洲。
編寫函數(shù) strongPasswordChecker(s)筒饰,s 代表輸入字符串,如果 s 已經(jīng)符合強(qiáng)密碼條件,則返回0骂际;否則返回要將 s 修改為滿足強(qiáng)密碼條件的字符串所需要進(jìn)行修改的最小步數(shù)(插入疗琉、刪除、替換任一字符都算作一次修改)歉铝。
需求分析
需求中有兩個(gè)要點(diǎn):
- 強(qiáng)密碼考慮了三個(gè)維度:長度盈简、連續(xù)性、字符種類
- 允許使用三種修改方法:插入犯戏、刪除送火、替換
我們下面使用表格來分析一下這幾種修改方法對(duì)這幾個(gè)緯度的影響:
修改方法 | 長度 | 字符種類 | 連續(xù)性 |
---|---|---|---|
插入 | +1 | +1 (if < 3) | 連續(xù)性長度-2 |
刪除 | -1 | 不影響 | 連續(xù)性長度-1 (連續(xù)性長度%3的值越小優(yōu)先級(jí)越高) |
替換 | 不影響 | +1 (if < 3) | 連續(xù)性長度-3 |
說明如下:
- 修改長度:對(duì)長度的修改是確定的(長了就刪除,短了就插入)先匪,但是要注意插入和刪除也會(huì)修改連續(xù)性和字符種類种吸,所以要先讓密碼滿足長度的要求,然后使用替換來改進(jìn)連續(xù)性和字符種類
- 修改字符種類:在修改長度和修改連續(xù)性后呀非,如果字符種類還不是3坚俗,則應(yīng)該通過替換增加字符種類
- 修改連續(xù)性:對(duì)于插入來說,密碼長度肯定小于6岸裙,那么只可能有一個(gè)連續(xù)性字符子串(3<=子串長度<=5)猖败,而插入一個(gè)字符會(huì)使得該子串長度減2,這時(shí)要么已沒有連續(xù)性字符子串降允,要么還有一個(gè)長度為3的連續(xù)性字符子串(再做一次替換即可)恩闻;對(duì)于刪除來說,密碼長度肯定大于20剧董,那么連續(xù)性字符子串可能有多個(gè)幢尚,這時(shí)應(yīng)該選擇子串長度模3的值最小的子串,然后將該子串長度減1
軟件設(shè)計(jì)
組合式設(shè)計(jì)概述
七巧板翅楼,是大家熟知的一種源自中國的古老智力游戲:
由這么七塊簡單的小素材尉剩,可以拼出變化無窮的圖案,受限的只是你的想象力:
這個(gè)簡單的游戲毅臊,蘊(yùn)含這一種極具價(jià)值的設(shè)計(jì)思想:組合理茎。
基本語義規(guī)則都是小素材,基本語義規(guī)則通過組合形成新的對(duì)用戶有價(jià)值的語義規(guī)則就是圖案管嬉。對(duì)于強(qiáng)密碼校驗(yàn)器這個(gè)需求來說皂林,要實(shí)踐組合式設(shè)計(jì),首先要拆出小素材宠蚂,然后再將小素材層層組合成強(qiáng)密碼校驗(yàn)器圖案式撼。其實(shí),軟件設(shè)計(jì)就是一門研究系統(tǒng)“如何分”和“怎么合”的學(xué)問求厕,目的是為了在讓軟件在長期范圍內(nèi)容易應(yīng)對(duì)變化著隆,而組合式設(shè)計(jì)就是將系統(tǒng)正交拆分且低成本組合的最佳軟件設(shè)計(jì)實(shí)踐扰楼。
于是,問題來了美浦,系統(tǒng)究竟該如何分弦赖?在回答這個(gè)問題之前,我們先看看什么是好的軟件設(shè)計(jì)浦辨。
我們?cè)谲浖_發(fā)中蹬竖,經(jīng)常隨口就說簡單設(shè)計(jì),但簡單設(shè)計(jì)不是一句語義模糊的無法評(píng)判的空洞口號(hào)流酬,而是具有明確的衡量標(biāo)尺币厕。
關(guān)于這個(gè)標(biāo)尺的定義,Kent Beck 給出了清晰的答案:
- 通過所有測(cè)試(Passes all tests)
- 盡可能消除重復(fù) (Minimizes duplication)
- 盡可能清晰表達(dá) (Maximizes clarity)
- 更少代碼元素 (Has fewer elements)
以上四個(gè)原則的重要程度依次降低芽腾。
這就是簡單設(shè)計(jì)四原則旦装,只有第四條強(qiáng)調(diào)簡單,其他三條分別從需求摊滔、易修改性和可理解性方面對(duì)簡單進(jìn)行了約束:
- 你不能為了簡單而不去實(shí)現(xiàn)客戶需求
- 你不能為了簡單而不去消除重復(fù)
- 你不能為了簡單而不注重清晰表達(dá)
固然前面三條的價(jià)值比簡單這個(gè)價(jià)值更重要阴绢,但是簡單也同樣會(huì)約束前面三條:
- 為了簡單,不要實(shí)現(xiàn)客戶暫時(shí)還用不到的需求
- 為了簡單艰躺,不要在變化方向未出現(xiàn)之前就對(duì)類進(jìn)行額外的抽象
- 為了簡單呻袭,不要增加額外的代碼元素,除非能增強(qiáng)表達(dá)力
我們?cè)诠ぷ髦袘?yīng)按照需求腺兴、易修改性左电、可理解性和復(fù)雜度這四個(gè)緯度的重要程度由高到底對(duì)任務(wù)進(jìn)行排序。比如页响,我們重構(gòu)時(shí)券腔,先消除重復(fù),然后再增強(qiáng)代碼的表達(dá)力拘泞。
如果一個(gè)軟件設(shè)計(jì)滿足簡單設(shè)計(jì)四原則的要求,我們就說這個(gè)軟件設(shè)計(jì)是好的軟件設(shè)計(jì)枕扫。
為了讓簡單設(shè)計(jì)四原則中的第二條(盡可能消除重復(fù))的達(dá)成有章可循陪腌,袁英杰大師提出了正交設(shè)計(jì)四原則:
- 消除重復(fù)
- 分離不同的變化方向
- 縮小依賴范圍
- 向著穩(wěn)定的方向依賴
第一條是重復(fù)出現(xiàn)之后被動(dòng)的消除重復(fù),而后面三條是重復(fù)出現(xiàn)之前的主動(dòng)預(yù)防策略烟瞧。同時(shí)诗鸭,前面兩條是為了驅(qū)動(dòng)模塊內(nèi)向高內(nèi)聚方向演進(jìn),后面兩條是為了驅(qū)動(dòng)模塊間向低耦合方向演進(jìn)参滴。
于是强岸,我們給出組合式設(shè)計(jì)的定義:應(yīng)用正交設(shè)計(jì)四原則,將系統(tǒng)分解成很多單一職責(zé)的小類或函數(shù)(函數(shù)式)砾赔,然后再將它們根據(jù)需要而靈活的組合起來蝌箍。
對(duì)于組合手段來說青灼,最常見且通用的手段是依賴注入,其他手段與編程語言相關(guān)妓盲,比如 C++ 可以使用多重繼承來實(shí)現(xiàn)組合杂拨。
語義模型
我們對(duì)強(qiáng)密碼校驗(yàn)器這個(gè)需求的通用語言進(jìn)行一下梳理:
- 強(qiáng)密碼考慮了三個(gè)緯度:長度記作 len,連續(xù)性記作 cont悯衬,字符種類記作 types弹沽;
- 允許使用三種修改方法:插入記作 insert,刪除記作 delete筋粗,替換記作 replace策橘;
- 原子操作記作 atom,需求中有四個(gè)原子操作娜亿,分別為密碼長度小于 6 的操作 len_less_than_atom丽已, 密碼長度大于 20 的操作,len_more_than_atom暇唾, 字符種類小于 3 的操作 types_atom促脉,同一個(gè)字符連續(xù)出現(xiàn) 3 次的操作 cont_atom
- 每個(gè)原子操作 atom 包含兩部分,即匹配器和執(zhí)行器二元組策州,記作(matcher, action)瘸味,那么針對(duì)四個(gè)原子操作,就有(len_less_than_matcher, insert_action)够挂、(len_more_than_matcher, delete_action)旁仿、 (types_matcher, replace_action) 和 (cont_matcher, replace_action)
- 需求中有多個(gè)規(guī)則 rule,atom 是最基本的 rule孽糖,repeat(多次執(zhí)行) 是修飾語義的 rule枯冈,allof (“與”的關(guān)系)和 anyof(“或”的關(guān)系)是組合語義的 rule,rule 通過組合可以產(chǎn)生新 rule
我們使用統(tǒng)一語言形式化表達(dá)一下需求:
password = newPassword(s)
len_less_than_atom_rule = atom(len_less_than_matcher, insert_action)
len_more_than_atom_rule = atom(len_more_than_matcher, delete_action)
types_atom_rule = atom(types_matcher, replace_action)
cont_atom_rule = atom(cont_matcher, replace_action)
len_rule = anyof(repeat(len_less_than_atom_rule), repeat(len_more_than_atom_rule))
types_rule = repeat(types_atom_rule)
cont_rule = repeat(cont_atom_rule)
rule = allof(len_rule, types_rule, cont_rule)
steps = rule(password)
從上面的形式化描述办悟,可以很容易的得到強(qiáng)密碼校驗(yàn)器的語義模型:
rule: Password -> int
matcher: Password -> bool
action: Password -> int
rule 的幾種形態(tài):
rule: atom | repeat | allof | anyof
我們用類圖表達(dá)一下強(qiáng)密碼校驗(yàn)器的組合式設(shè)計(jì):
我們?cè)儆帽砀駚肀磉_(dá)一下 Rule 的分類:
分類 | Rule |
---|---|
原子語義 | LenLessThanAtom LenMoreThanAtom TypesAtom ContAtom |
修飾語義 | Repeat |
組合語義 | AnyOf AllOf |
軟件實(shí)現(xiàn)
在《聊聊編程范式》一文中尘奏,我們梳闡述了常見的三種編程范式(結(jié)構(gòu)化編程、面向?qū)ο缶幊毯秃瘮?shù)式)的基本設(shè)計(jì)和架構(gòu)風(fēng)格病蛉,梳理了編程范式之間的關(guān)系炫加。
從結(jié)構(gòu)化編程到面向?qū)ο缶幊蹋俚胶瘮?shù)式編程铺然,離圖靈機(jī)模型越來越遠(yuǎn)俗孝,但抽象程度越來越高,與領(lǐng)域問題的距離越來越近魄健。
對(duì)于組合式設(shè)計(jì)的實(shí)現(xiàn)赋铝,我們既可以選擇面向?qū)ο缶幊蹋部梢赃x擇函數(shù)式編程沽瘦。函數(shù)式編程更抽象一些革骨,但代碼更簡潔农尖;面向?qū)ο缶幊滩糠执a稍顯繁雜,但相對(duì)來說易于理解苛蒲。本文選擇以函數(shù)式編程為例來實(shí)踐組合式設(shè)計(jì)卤橄,讀者可以自行使用面向?qū)ο缶幊虂韲L試一下,大同小異臂外。
API 實(shí)現(xiàn)
需求中給出了 API 的定義:
strongPasswordChecker(s)
Golang 代碼的實(shí)現(xiàn)如下:與統(tǒng)一語言的形式化表達(dá)基本一樣
func StrongPasswordChecker(s string) int {
password := newPassword(s)
len_less_than_atom_rule := atom(len_less_than_matcher, insert_action)
len_more_than_atom_rule := atom(len_more_than_matcher, delete_action)
types_atom_rule := atom(types_matcher, replace_action)
cont_atom_rule := atom(cont_matcher, replace_action)
len_rule := anyof(repeat(len_less_than_atom_rule), repeat(len_more_than_atom_rule))
types_rule := repeat(types_atom_rule)
cont_rule := repeat(cont_atom_rule)
rule := allof(len_rule, types_rule, cont_rule)
steps := rule(password)
return steps
}
函數(shù)式設(shè)計(jì)的基本方法為:借助閉包的單一接口的標(biāo)準(zhǔn)化和高階函數(shù)的可組合性窟扑,通過規(guī)則串聯(lián)設(shè)計(jì),完成數(shù)據(jù)從源到結(jié)果的映射描述漏健。這里的映射是通過多個(gè)高階函數(shù)的形式化組合完成嚎货,描述就像寫數(shù)學(xué)公式一樣放在那,等源數(shù)據(jù)從一頭傳入蔫浆,然后經(jīng)過層層函數(shù)公式的處理殖属,最后變成你想要的結(jié)果。數(shù)據(jù)在形式化轉(zhuǎn)移的過程中瓦盛,不僅僅包括數(shù)據(jù)本身洗显,還包括規(guī)則的創(chuàng)建、返回和傳遞原环。
數(shù)據(jù)(對(duì)象)實(shí)現(xiàn)
我們下面看看數(shù)據(jù)本身 Password 的設(shè)計(jì)挠唆,該數(shù)據(jù)的表現(xiàn)形式是一個(gè)對(duì)象。
Password 數(shù)據(jù)結(jié)構(gòu)
type Password struct {
initialStr string
Len int
Steps int
TypesNum int
hasDigital bool
hasLowerCase bool
hasUpperCase bool
contNumbers []*ContNumber
}
type ContNumber struct {
initialChar byte
initialIndex int
times int
}
Password 結(jié)構(gòu)中包括了密碼字符串的原始信息嘱吗、預(yù)處理信息和改造過程中的信息玄组。密碼中可能包括多個(gè)連續(xù)性字符組成的數(shù),我們通過 []*ContNumber 來表示谒麦。
Password 對(duì)象的構(gòu)造
func newPassword(s string) *Password {
pwd := &Password{initialStr: s, Len: len(s), Steps: 0, TypesNum: 0,
contNumbers: make([]*ContNumber, 0)}
typesInit(pwd)
contInit(pwd)
return pwd
}
說明:共有三步
- 構(gòu)造 Password 數(shù)據(jù)俄讹,記錄密碼字符串 s 的初始值和長度
- 遍歷密碼字符串,初始化字符種類
- 遍歷密碼字符串绕德,初始化連續(xù)性數(shù)的數(shù)組
Password 對(duì)象的方法
下面這些方法都是在 TDD 的實(shí)踐過程中患膛,為了達(dá)成正交設(shè)計(jì)四原則的目標(biāo),通過 extract 重構(gòu)手法逐步提煉出來的:
func (p *Password) increaseLen() {
p.Len++
}
func (p *Password) decreaseLen() {
p.Len--
}
func (p *Password) increaseSteps() {
p.Steps++
}
func (p *Password) increaseTypesNum() {
if p.TypesNum < maxTypesNum {
p.TypesNum++
}
}
func (p *Password) consumeContNumber(quota int) {
...
}
func (p *Password) consumeContNumberByPrio() {
...
}
rule 的實(shí)現(xiàn)
這一部分是組合式設(shè)計(jì)的核心耻蛇。
數(shù)據(jù)在形式化轉(zhuǎn)移過程中剩瓶,源數(shù)據(jù)是 Password,層層函數(shù)的輸入輸出數(shù)據(jù)都是 Password 城丧,同時(shí)目標(biāo)數(shù)據(jù)也是 Password 的 Steps 字段,所以我們使用 Password 指針類型來表達(dá)數(shù)據(jù)類型豌鹤。
rule 的抽象表達(dá)
我們知道亡哄,rule 包含兩部分,即匹配器和執(zhí)行器二元組:
type Matcher func(pwd *Password) bool
type Action func(pwd *Password) int
type Rule func(mather Matcher, action Action) func(pwd *Password) int
原子語義的實(shí)現(xiàn)
atom 的實(shí)現(xiàn)很簡單布疙,就是返回一個(gè)閉包:當(dāng)匹配器滿足時(shí)蚊惯,就返回執(zhí)行器的結(jié)果(steps)
func atom(matcher Matcher, action Action) func(pwd *Password) int {
return func(pwd *Password) int {
if matcher(pwd) {
return action(pwd)
}
return 0
}
}
原子語義的 rule 有四個(gè)愿卸,分別為 LenLessThanAtom、LenMoreThanAtom截型、TypesAtom 和 ContAtom趴荸,核心是設(shè)計(jì)它們的匹配器(4個(gè))和執(zhí)行器(3個(gè))。
匹配器實(shí)現(xiàn):
func len_less_than_matcher(pwd *Password) bool {
if pwd.Len < minLen {
return true
}
return false
}
func len_more_than_matcher(pwd *Password) bool {
if pwd.Len > maxLen {
return true
}
return false
}
func types_matcher(pwd *Password) bool {
if pwd.TypesNum < 3 {
return true
}
return false
}
func cont_matcher(pwd *Password) bool {
if len(pwd.contNumbers) > 0 {
return true
}
return false
}
執(zhí)行器實(shí)現(xiàn):
func insert_action(pwd *Password) int {
pwd.increaseLen()
pwd.increaseTypesNum()
pwd.consumeContNumber(2)
pwd.increaseSteps()
return pwd.Steps
}
func delete_action(pwd *Password) int {
pwd.decreaseLen()
pwd.consumeContNumberByPrio()
pwd.increaseSteps()
return pwd.Steps
}
func replace_action(pwd *Password) int {
pwd.increaseTypesNum()
pwd.consumeContNumber(3)
pwd.increaseSteps()
return pwd.Steps
}
修飾語義的實(shí)現(xiàn)
修飾語義的 rule (新)只有一個(gè)宦焦,即 repeat发钝,含義是重復(fù)執(zhí)行被修飾的 rule(舊),直到符合預(yù)期:
func repeat(rule func(pwd *Password) int) func(pwd *Password) int {
return func(pwd *Password) int {
for ;; {
ret := rule(pwd)
if ret == 0 {
return pwd.Steps
}
}
}
}
組合語義的實(shí)現(xiàn)
組合語義的 rule 有兩個(gè)波闹,即 allof 和 anyof酝豪,分別表達(dá)“與”的關(guān)系和“或”的關(guān)系:
func allof(rules... func(pwd *Password) int) func(pwd *Password) int {
return func(pwd *Password) int {
steps := 0
for _, rule := range rules {
pwd.Steps = 0
steps += rule(pwd)
}
return steps
}
}
func anyof(rules... func(pwd *Password) int) func(pwd *Password) int {
return func(pwd *Password) int {
steps := 0
for _, rule := range rules {
steps += rule(pwd)
if steps > 0 {
return steps
}
}
return 0
}
}
小結(jié)
本文概述了組合式設(shè)計(jì)的要點(diǎn),并通過實(shí)戰(zhàn)強(qiáng)密碼校驗(yàn)器的案例精堕,向讀者展示了組合式設(shè)計(jì)落地的主要過程和步驟孵淘,希望對(duì)讀者有一定的收益。
如果說正交設(shè)計(jì)是軟件設(shè)計(jì)的皇冠歹篓,那么組合式設(shè)計(jì)就是皇冠上的明珠瘫证。應(yīng)用組合式設(shè)計(jì)產(chǎn)出的代碼擴(kuò)展性強(qiáng)且易于理解,你是否深有感觸庄撮?如果你也曾寫過或讀過強(qiáng)密碼校驗(yàn)器的實(shí)現(xiàn)代碼背捌,你都發(fā)現(xiàn)過哪些壞味道?歡迎大家留言探討重窟!
本文實(shí)戰(zhàn)過程中的源碼及 TDD 用例载萌,作者都放到 github 了:https://github.com/agiledragon/strong-password,感興趣的同學(xué)可以查閱巡扇。