通過本系列文章,我試圖通過一個簡單的UC來構(gòu)建我的架構(gòu)世界觀和方法論,其中核心的線索是SOC,使用的語言是
Scala
,編程范式為FP
第一篇 編譯器的歸編譯器抒蚜,運行時的歸運行時
無論是2C 還是 2B,用戶都是公司的命根子柜裸,當(dāng)用戶被千方百計吸引過來時炼吴,我們一定不希望用戶被一個不穩(wěn)定的用戶注冊服務(wù)拒之門外。
那如何構(gòu)造一個穩(wěn)定的用戶注冊服務(wù)呢?
這還不簡單么
從業(yè)務(wù)角度扁眯,這確實沒什么難的壮莹,我們可以簡單的提煉出一個Case:
UC 1. 用戶注冊/Register
流程:
1. 校驗用戶提交的注冊要素(手機號碼,驗證碼姻檀,登陸名稱命满,密碼)
2. 保存用戶注冊信息,并返回成功注冊的用戶信息
2.1 如果用戶注冊要素校驗不通過绣版,則返回注冊要素不合規(guī)范的錯誤信息
2.2 如果保存失敗胶台,則返回服務(wù)端暫時不可用的錯誤信息
前置條件:
1. 通知服務(wù)已經(jīng)發(fā)送該手機號碼的驗證碼,并提供驗證碼驗證服務(wù)杂抽。
后置條件:
1. 注冊成功后诈唬,用戶可通過登錄名和密碼登錄登錄以獲取服務(wù)。
基于業(yè)務(wù)缩麸,我們很容易建模:
/** 用戶信息
* @param mobile: 手機號碼
* @param otp: 驗證碼
* @param loginName: 登錄名
* @param password: 密碼
*/
case class User(mobile: String, otp: String, loginName: String, password: Vector[Char])
/** 用戶服務(wù)
*
*/
trait UserService {
/** 注冊用戶铸磅,完成 UC1 的業(yè)務(wù)規(guī)則
*/
def register(user: User): Unit
}
我們可以把這些交給Coding小伙伴來實現(xiàn)了吧:
class UserServiceImpl extends UserService {
/** 注冊用戶,完成 UC1 的業(yè)務(wù)規(guī)則
*/
def register(user: User): Unit = {
// 校驗手機號碼格式
if (! validateMobile(user.mobile)) throw new Exception("手機號碼格式錯誤")
// 調(diào)用驗證碼驗證Restful服務(wù)
if (! validateOtp(user.otp)) throw new Exception("驗證碼錯誤")
// 校驗用戶登錄名格式
if (! validateLoginName(user.loginName)) throw new Exception("用戶名格式錯誤")
// 校驗用戶登錄名是否沖突
if (! hasExisted(user.loginName)) throw new Exception("用戶名沖突")
// 校驗密碼強度
if (! validatePassword(user.password)) throw new Exception("密碼強度不符合要求")
// 持久化用戶信息
persistUser(user)
}
private def validateMobile(mobile: String): Boolean = ???
private def validateOtp(otp: String): Boolean = ???
private def validateLoginName(loginName: String): Boolean = ???
private def hasExisted(loginName: String): Boolean = ???
private def validatePassword(password: Vector[Char]): Boolean = ???
private def persistUser(user: User): Unit = ???
}
So far, so good!
減少編碼錯誤
上線后杭朱,這段代碼正常運行了一段時間阅仔,沒有出現(xiàn)啥問題,Good弧械!
But八酒,But,在一次上線后梦谜,突然發(fā)現(xiàn)丘跌,所有的用戶無法注冊了袭景,What !1帐鳌耸棒!
在比較代碼變更時發(fā)現(xiàn),構(gòu)建User對象時报辱,一個小伙伴無意中將otp參數(shù)賦給了loginName与殃!編譯、發(fā)布碍现,一切正常幅疼,但在運行時,校驗不通過昼接,所以捅了大簍子爽篷!
當(dāng)然,我們可以通過代碼之外的手段來減少這種錯誤慢睡,比如測試逐工。但一些常規(guī)更新中,很可能漏掉無關(guān)的一些功能的測試漂辐,從“反求諸己”的原則自我要求的話泪喊,我們必須檢視,有沒有針對這種錯誤的改進(jìn)的空間髓涯?我們的代碼是否有足夠的自我防御能力袒啼,盡早發(fā)現(xiàn)這種錯誤呢?
一種可選的答案是Typeful 纬纪。也就是強類型且類型完全的蚓再,讓編譯器對類型進(jìn)行檢查,在編譯期間就為我們排出這種低級錯誤育八。
讓我們再次審視我們的代碼对途,它是否真的反應(yīng)了業(yè)務(wù)?業(yè)務(wù)用例說髓棋,用戶注冊要素包含手機號碼
实檀、驗證碼
等,再看看我們的User
類是怎樣對這兩個要素建模的按声。發(fā)現(xiàn)差異了嗎膳犹?loginName
和 otp
兩個參數(shù)都是String
類型, 我們用屬性名稱來建模签则,而沒有使用類型來建模须床!然而編譯器不會檢查變量名,但會檢查變量的類型(Scala是強類型語言)渐裂。
讓我們充分利用強類型豺旬,讓編譯器替我們干最臟最累的活吧钠惩!Let's Do it!
/** 手機號碼
*/
case class Mobile(value: String) extends AnyVal
/** 手機驗證碼
*/
case class OTP(value: String) extends AnyVal
/** 登錄名
*/
case class LoginName(value: String) extends AnyVal
/** 密碼
*/
case class Password(value: Vector[Char]) extends AnyVal
/** 用戶
*/
case class User(mobile: Mobile, otp: OTP, loginName: LoginName, password: Password)
嗯族阅,這樣一來篓跛,我們的代碼更加類型安全了!不會再出現(xiàn)錯傳參數(shù)的低級錯誤了坦刀。因此愧沟,我們要盡可能的Typeful
,讓編譯器檢查低級錯誤鲤遥。
更深一層考慮沐寺,我們在做一件事情:SOC(Separation Of Concerns
), 分離的是什么呢?我們分離的是編譯和運行盖奈,充分利用編譯
的類型檢查職責(zé)混坞,避免將類型的檢查延遲到運行時!之前的代碼很明顯沒有意識到這種分離钢坦。(除了使用變量名稱來指稱業(yè)務(wù)含義拔第,我們經(jīng)常犯的錯還包括在運行時對對象進(jìn)行類型檢查,根據(jù)對象的類型決定業(yè)務(wù)的走向)
這種重構(gòu)场钉,在實際的開發(fā)過程中出現(xiàn)過,比如在開發(fā)加解密工具包時懈涛,一開始對所有的參數(shù)都是用
Array[Byte]
類型逛万,導(dǎo)致外部調(diào)用方經(jīng)常講公鑰與私鑰參數(shù)順序搞反了,自己的單元測試完全通過批钠,但集成到其他模塊時就出錯宇植,查這種錯花了不少時間,可謂是教訓(xùn)深刻埋心!