本系列文章从场景代码入手,通过代码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环节,技术经理开始审查代码了。很快审查报告出来了,说代码输出功能可扩展性差。有了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对象上,一般也想不到扩展,因为没有需求吗?过渡设计扩展意义不大。拆分一般出现在对功能扩展的时候,如果出现了重复的功能、重复的代码,要敏锐的想到是否需要进行重构拆分了。如果不关注,代码可能会这样慢慢膨胀💥,越来越难维护。