單一職責原則

本系列文章從場景代碼入手,通過代碼 review 指出當前存在的問題,然後思考改進,最後進行提煉總結,即通過” 代碼 - 問題 - 改進 - 總結 “的方式學習編程模式,感受思考的樂趣,To be a better coder.

場景代碼

現在有一個 struct statistic,它的功能是統計給定目錄下的文件的代碼行數,並將統計結果輸出。小明接到這個需求,心裏很 Happy😁,這還不簡單,分分鐘搞定,於是寫下了如下的代碼。

type statistic struct {
 data map[string]int
}

func (s *statistic) Statistic(path string) error {
 // TODO 統計每個文件中的代碼函數,存儲到data中
 // data中的key爲文件名 value爲代碼行數
 return nil
}

func (s statistic) Output(writer io.Writer) {
 for path, result := range s.data {
  fmt.Fprintf(writer, "%s -> %d\n", path, result)
 }
}

過了一會,同事小 A 說,能不能加個功能,將統計結果以 csv 格式輸出,這樣我可以直接用 excel 軟件打開,方便查看。小明說沒問題,加個方法不就可以了。於是,得到如下代碼。

func (s statistic) OutputCSV(writer io.Writer) {
 for path, result := range s.data {
  fmt.Fprintf(writer, "%s,%d\n", path, result)
 }
}

代碼 Review

代碼提交之後來到了 review 環節,技術經理開始審查代碼了。很快審查報告出來了,說代碼輸出功能可擴展性差。有了 OutputCSV 說不定以後還有 Outputxxx,職責不夠單一,添加一種輸出方式就需要修改 statistic 代碼,不滿足 single responsibility principle(SRP) 原則。

如何改進

小明按照評審官的意見重新審視自己的代碼,並輸出了 statistic 的結構圖,如下圖所示。

statistic 的功能是統計,按職責來說輸出信息並不是它要做的事。所以需要進行拆分,將輸出信息分離出去單獨成爲一個 Printer class(在 golang 中把 class 理解爲 struct)。現在我們分別從 statistic 和 Printer 以及函數調用方的角度來看他們之間的關係, statistic 只需完成自己的統計功能,Printer 只需完成輸出功能,它的輸入數據來自 statistic, 因爲 statistic 數據存儲在 map 中,所以 Printer 接收參數定義爲 map,可以實現 Printer 和 statistic 完全解耦。調用方 main 函數拿到 statistic 和 Printer 對象便可以完成統計輸出。在來看擴展性,我們將 Printer 定義爲接口,新增一種輸出方式,只需要擴展一個 class,不用改已有的代碼,非常好。小明很快完成改進,得到如下代碼。這次代碼終於得到評審官的肯定😁。

type statistic struct {
 data map[string]int
}

func (s *statistic) Statistic(path string) error {
 // TODO 統計每個文件中的代碼函數,存儲到data中
 // data中的key爲文件名 value爲代碼行數
 return nil
}

func (s *statistic) GetData() map[string]int{
 // TODO 拷貝s.data返回
 return nil
}

type Printer interface {
 Output(data map[string]int)
}

type defaultPrinter struct {
 Writer io.Writer
}

func (d *defaultPrinter) Output(data map[string]int) {
 for path, result := range data {
  fmt.Fprintf(d.Writer, "%s -> %d\n", path, result)
 }
}

type CSVPrinter struct {
 Writer io.Writer
}

func (c *CSVPrinter) Output(data map[string]int) {
 for path, result := range data {
  fmt.Fprintf(c.Writer, "%s,%d\n", path, result)
 }
}

提煉總結

本文代碼改進中我們遵循了單一職責原則(SRP), 單一職責原則的核心要點是什麼呢?一個類只負責一個職責或者功能,就是類(struct)的設計不要大而全,用一個類搞定一切,要設計粒度小、功能單一的類型。單一職責的目標是實現代碼高內聚、低耦合,提高代碼的複用性、可讀性和可維護性。

怎麼判斷一個類是否職責單一呢?有什麼直觀的評價依據嗎?這其實沒有明確的標準,對一個類型的職責是否單一,不同的人可能有不同的判斷結果。在工程實踐中要結合場景具體業務具體分析,不能生搬硬套,如果遇到一個類的代碼行數很多,一個 struct 中定義了很多字段,有可能不滿足單一職責原則,考慮是否可以拆分簡化代碼複雜性。

什麼時候進行拆分呢?有同學會說像上面的代碼在第一次寫的時候想不到拆分怎麼辦?像上面的例子,如果沒有後面新需求要輸出 csv 格式,將 Output 方法直接定義在 statistic 對象上,一般也想不到擴展,因爲沒有需求嗎?過渡設計擴展意義不大。拆分一般出現在對功能擴展的時候,如果出現了重複的功能、重複的代碼,要敏銳的想到是否需要進行重構拆分了。如果不關注,代碼可能會這樣慢慢膨脹💥,越來越難維護。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/T3XVWifxLfPeQZulqmQLkQ