首頁 > 藝術

有了Bug,先看看“Type”

由 51CTO 發表于 藝術2023-01-02

簡介FormatShort(loc)==> £10k當然,建立這樣一個Money型別在開始的時候會有點費勁,但是一旦它被實現並經過測試,那麼程式碼庫的其他部分就可以帶來更大的安全性,並防止大多數的Bug的產生,否則這些Bug會隨著時

散列表的表長怎麼確定

開篇

不懂原始型別的程式設計師,往往由於急於求成,上手很快,最後卻發現被各種Bug耽誤進度。本文以經典的郵箱型別、貨幣型別、密碼型別為例,利用好型別系統能夠很好地改進編碼方式,同時為技術人找回“打穩地基”的快樂。

有了Bug,先看看“Type”

字串型別變身成為郵箱型別

筆者已經厭倦了使用原始型別,並試圖透過使用原始型別,來為一個領域進行建模。

字串值(String)型別不僅僅用來儲存使用者的電子郵件地址或國籍資訊,還可以有更豐富的用途。我需要一個EmailAd

dress的型別,並定義它不能為空,同時希望有單一的入口來建立該型別的物件。在返回一個值之前,需要被驗證和規範化。

同時,也希望該資料型別有一些方法,如。Domain()或。NonAliasValue(),在輸入foo+bar@gmail。com時,會分別呼叫這兩個方法並返回gmail。com和foo@gmail。com。

在型別設計中應該考慮這種有用的功能,該功能的引入有助於防止錯誤的發生,並提高了型別的可維護性。

設計良好、功能實用的型別

例如,一個EmailAddress可以提供兩個方法來檢查是否相等。

lEquals方法用來判斷兩個(規範化)的電子郵件地址是否相同,如果相同將返回true。

lEqualsInPrinciple

該方法對於foo@gmail。com和foo+bar@gmail。com的輸入會判斷相同,因此也會返回true。(這裡假設兩個郵箱都是同一個人註冊的,因此相同需要判斷兩個郵箱“相等”)

特定型別的方法在不同的使用場景下都會發揮不同的作用。如果使用者jane@gmail。com註冊,但又用Jane@gmail。com登入,那麼使用者的登入不應該失敗(僅僅存在首字母大小寫的區別)。同樣的,如果戶用使用電子郵件地址(foo@gmail。com)和另一個註冊賬戶(foo+svc@gmail。com)聯絡客戶支援,相同就需要對這兩個郵箱進行有效匹配。這些都是典型應用場景,如果沒有散落在程式碼庫中的業務邏輯,僅憑一個簡單的字串是無法滿足的。

注意:根據Office RFC描述,電子郵件地址中@符號之前的部分可以區分大小寫,但所有主要的電子郵件主機都將其視為不區分大小寫,因此,域名型別也考慮這方面的問題。

好的型別可以防止Bug

順著上面郵箱型別的例子,如果我們想走得更遠,假如希望一個電子郵件地址可以被驗證或未被驗證。通常的做法是,透過向個人的收件箱傳送一個獨特的程式碼來驗證電子郵件地址。這些 “商業 ”上的互動也可以透過型別系統來表達。例如,建立一個叫做VerifiedEmailAddress的第二個型別。該型別可以繼承自EmailAddress。並且確保程式碼中只有一個地方可以產生VerifiedEmailAddress的例項,即負責驗證使用者地址的服務。如此這般,應用程式的其他部分可以依靠這個新型別來防止Bug。

任何傳送電子郵件的功能都可以依靠該類來驗證的電子郵件地址的安全性。想象一下,如果電子郵件地址是透過簡單的字串來表達的,會是怎樣的情況。

因此,要找到相關的使用者賬戶,檢查一些模糊的標誌,如HasVerifiedEmail或IsActive,確保這些標誌設定是正確的,而不會在預設建構函式中被錯誤地初始化為真。有太多的錯誤空間由於使用了原始字串,導致有些檢查不到位的情況,這種使用原始型別的表達方式被認為是懶惰和缺乏想象力的程式設計。

富型別免受錯誤的侵擾

另一個很好的例子是貨幣!我已經數不清有多少應用程式使用十進位制來表達貨幣值。也已經數不清有多少應用程式使用十進位制型別表達貨幣值。為什麼呢?

這種型別有很多問題,甚至很難理解。每個與錢打交道的領域都應該有專門的貨幣型別。貨幣型別應該包括貨幣和運算子過載(或其他安全功能),以防止出現100美元與20英鎊相乘這樣的愚蠢錯誤。此外,並非每種貨幣在小數點後都只有兩位數。有些貨幣,如巴林或科威特第納爾有三位。如果你在致力處理投資或銀行貸款,那麼你最好確保你呈現的Unidad de Fomento有4個小數點。這些問題已經很重要了,足以保證有一個專門的Moneytype,但這還遠遠不夠。

除非在系統內部完成所有功能,否則就不得不與第三方系統打交道。例如,大多數支付閘道器都是以整數值來請求和響應資金。由於整數值不能涵蓋類似浮點數(雙數型別)的四捨五入運算,因此比浮點數更受歡迎。唯一需要注意的是,數值必須以小單位(如美分、便士、迪拉姆、格羅茲、科佩克等)傳輸,這意味著如果你的程式處理小數點數值,在與外部API對話時,你將不得不不斷地來回轉換它們。如前所述,並不是每種貨幣都使用兩個小數點,所以不是每次都是簡單的乘/除以100。事情很快就會變得很困難,如果這些業務規則被封裝成一個簡潔的單一型別,事情就會被大大簡化。

var x = Money。FromMinorUnit(100,

“GBP”

):£1

var y = Money。FromUnit(100。50,

“GBP”

):£1。50

Console。WriteLine(x。AsUnit()):1。5

Console。WriteLine(x。AsMinorUnit()):150

如果這還不夠複雜的話,各國也有不同的貨幣格式來表示貨幣。在英國,“一萬英鎊和五十便士 ”將被表示為10,000。50,但在德國,“一萬歐元和五十美分 ”將被顯示為10。000,50。試想一下,如果這些規則沒有放到統一的貨幣型別中,那麼在整個程式碼庫中會有多少與貨幣相關的程式碼被分割開來。

此外,一個專門的貨幣型別可以包括許多功能,這將使貨幣價值的工作變得輕而易舉。

var gbp = Currency。Parse(

“GBP”

);

var loc = Locale。Parse(

“Europe/London”

);

var money = Money。FromMinorUnit(1000050, gbp);

money。Format(loc) // ==> £10,000。50

money。FormatVerbose(loc) // ==> GBP 10,000。50

money。FormatShort(loc) // ==> £10k

當然,建立這樣一個Money型別在開始的時候會有點費勁,但是一旦它被實現並經過測試,那麼程式碼庫的其他部分就可以帶來更大的安全性,並防止大多數的Bug的產生,否則這些Bug會隨著時間的推移而慢慢出現。即使像Money。FromUnit(decimal v, Currency c)或Money。FromMinorUnit(int v, Currency c)這樣的小功能看起來並不多,但它使參與連續開發的程式設計師能夠意識到,使用者輸入或外部API收到的值是否包含在其中,這樣可以在一開始就防止Bug的產生。

聰明的型別設計減少副作用

富型別的偉大之處在於,可以以任何的方式來塑造它們。這裡展示另外一個例子,富型別如何將團隊從巨大的操作開銷中拯救出來,甚至防止安全漏洞。

相信很多系統中的程式碼庫都有一個類似於字串secretKey或字串password的東西,它作為函式的引數。那麼在什麼情況下有可能出錯呢?

如下(偽)程式碼:

try

{

var userLogin = new UserLogin

{

Username = username

Password=password

}

var success = _loginService。TryAuthenticate(userLogin);

if

(success)

RedirectToHomeScreen(userLogin)。

ReturnUnauthorized()。

}

catch (Exception ex)

{

Logger。LogError(ex,

“User login failed for {login}”

, userLogin);

}

這裡出現的問題是,如果在認證過程中丟擲一個異常,那麼這個應用程式將使用者的明文密碼寫入日誌。當然,這段程式碼一開始就不應該存在,這種情況會隨著時間的推移而發生。大多數這樣的錯誤都是隨著時間的推移而逐步發生的。

最初,UserLogin類可以有一組不同的屬性,在最初的程式碼審查中,這段程式碼可能沒有問題。幾年後,有人可能修改了UserLogin類以包括明文密碼。這個功能甚至不會出現在程式碼提交的差異中,因此會逃過程式碼審查。於是就引入了安全漏洞。然而,如果引入一個富型別(專有型別),就可以避免類似錯誤的發生。

在C#中(以這個語言為例),當一個物件被寫入日誌時,ToString()方法會被自動呼叫。有了這些知識,我們就可以設計一個這樣的密碼型別。

public

readonly

record struct

Password

()

{

public override string

ToString

()

{

return

“****”

}

public string

Cleartext

()

{

return

_cleartext。

}

}

雖然是一個微小的變化,但在系統的任何地方都不可能意外地輸出一個明文密碼。這不是很好嗎?

當然,在實際的認證過程中,你可能仍然需要明文值,那麼就需要透過非常明確的命名方法Cleartext()來實現的,所以對這個操作的敏感性沒有任何含糊,它自動引導開發者有意和謹慎地使用這個方法。

處理使用者的PII(如國家保險號、稅號等)也是同樣的原則。使用專門的型別對這些資訊進行建模。覆蓋預設函式,如。ToString()。ToString()的預設函式,並透過相應的命名函式暴露敏感資料。你永遠不會把PII洩露到日誌和其他地方,以後可能需要一個巨大的操作來再次刷掉它。

小伎倆發揮了大作用!

形成習慣

每當開發者處理那些有特殊規則、行為或敏感資料的時候,不妨考慮如何能透過建立一個顯式型別來幫助自己。

讓我們再舉一個密碼型別的例子,可以走得更遠!

密碼在被儲存到資料庫之前會進行雜湊計算,但這個雜湊值不是一個簡單的字串。在某些時候,我們將不得不在登入過程中把以前儲存的雜湊值與新計算的雜湊值進行比較。但並不是每個開發人員都是安全專家,比較兩個雜湊字串可能會使程式碼受到攻擊。

檢查兩個密碼雜湊值是否相等的推薦方法是以非最佳化的方式進行。

[MethodImpl(MethodImplOptions。NoInlining | MethodImplOptions。NoOptimization) ]

private static bool ByteArraysEqual(byte[] a, byte[] b)

{

if

(a == null &&b == null)

{

return

true

}

if

(a == null || b == null || a。Length != b。Length)

{

return

false

}

var areSame =

true

for

(var i = 0; i < a。Length; i++)

{

areSame &= (a[i] == b[i])。

}

return

areSame。

}

注:程式碼示例取自原始ASP。NET Core資源庫

因此,將這一特殊功能編碼為一個專門的型別才是合理的。

public

readonly

record struct PasswordHash

{

public override bool Equals(PasswordHash other)

{

return

ByteArraysEqual(this。Bytes(), other。Bytes())。

}

}

如果一個PasswordHasher只返回PasswordHash型別的值,即使是對業務不太瞭解的開發者也會使用一種安全的形式來檢查相等。

在建立領域模型方面要考慮周全! 當然,程式設計中的一切都沒有明確的對錯之分,人們的個人使用情況總是有更多的細微差別,這些不是在一篇文章中所能表達的,但筆者建議是,考慮如何使型別系統對開發者的幫助很大。現在許多現代程式語言都有非常豐富的型別系統,我們可能忽視了它們沒有利用好這些型別改進編碼方式。

Tags:型別程式碼gmailcom電子郵件