作为一个后端工程师,想必在职业生涯中都写过一些不好维护的代码。本文是我学习《代码之丑》的学习笔记,今天第三天,品品大类和长参数列表的味道。
上一篇:一天一点代码坏味道(2)
1 大类
对于我们来说,一个人理解的东西是有限的,没有人能够同时面对所有细节。
因此,人类选择面对复杂事物的解决方案都是分而治之。
那么,如果一个类里面的内容太多,它就会超过一个人的理解范畴。
问题来了,大类是如何变大的?
职责不单一
单一职责原则是衡量软件设计好坏的一把简单而有效的尺子,通常来说,很多类之所以巨大,大部分情况下都是因为其违反了这个原则。
坏味道代码:
public class User { public long UserId { get; set; } public string Name { get; set; } public string NickName { get; set; } public string Email { get; set; } public string PhoneNumber { get; set; } public AuthorType AuthorType { get; set; } public ReviewStatus AuthorReviewStatus { get; set; } public EditorType EditorType { get; set; } ... }
有经验的童鞋应该一眼就发现了其中包含了不同类型的用户的信息,既有用户基本信息,还有作者相关信息,最后还有编辑类型...
三种不同的角色,三种不同诉求的业务方关心的是不同的内容,只是因为她们都是这个系统的用户,就把它们都放在了用户类中。后续需求一变动,这个用户类就会被反复修改。
针对上面这个场景,需要对其中的不同角色进行拆分:
public class User { public long UserId { get; set; } public string Name { get; set; } public string NickName { get; set; } public string Email { get; set; } public string PhoneNumber { get; set; } ... } public class Author { public long UserId { get; set; } public AuthorType AuthorType { get; set; } public ReviewStatus AuthorReviewStatus { get; set; } ... } public class Editor { public long UserId { get; set; } public EditorType EditorType { get; set; } ... }
字段未分组
拆分之后,User类还是很大,再仔细看,发现其实可以将部分字段进行分组, 比如Email和PhoneNumber都属于用户的联系方式,便可以再次分解。
这里引入一个Contact类,将Email和PhoneNumber放了进去,以后如果还有其他联系方式如QQ、微信之类的需求,也都可以统一放到Contact类中。
public class User { public long UserId { get; set; } public string Name { get; set; } public string NickName { get; set; } public Contact Contact { get; set; } ... } public class Contact { public string Email { get; set; } public string PhoneNumber { get; set; } ... }
由此,我们可以看出,将大类分解成小类,其实也是在做设计工作。欢迎体会软件设计之美!
2 长参数列表
方法之间传递参数再常见不过,但是如果不限制参数个数,长参数列表就会出现在你我的项目代码之中,它带来的不可维护度是巨大的。
和大类一样,我们也需要对长参数列表进行拆解。
那么,有哪些拆解方式呢?
将参数列表封装成对象
这是一个熟知的重构方法,记得我在10年前阅读王涛老师《你必须知道的.NET》一书中就了解了这个技巧。
那么,不妨看一个长参数列表的坏味道:
public void CreateBook(string title, string introduction, URL coverUrl, BookType type, BookChannel channel, string protagonists, string tags, bool completed) { var book = new Book() { Title = title, Introduction = introduction, CoverUrl = coverUrl, Type = type, Channel = channel, Protagonists = protagonists, Tags = tags, Completed = completed }; _repository.Save(book); }
将其封装为一个类型:
public class NewBookParameters { public string Title { get; set; } public string Introduction { get; set; } public URL CoverUrl { get; set; } public BookType Type { get; set; } public BookChannel Channel { get; set; } public string Protagonists { get; set; } public string Tags { get; set; } public bool Completed { get; set; } }
那么,CreateBook方法就改为这个样子?
public void CreateBook(NewBookParameters parameters) { var book = new Book() { Title = parameters.Title, Introduction = parameters.Introduction, CoverUrl = parameters.CoverUrl, Type = parameters.Type, Channel = parameters.Channel, Protagonists = parameters.Protagonists, Tags = parameters.Tags, Completed = parameters.Completed }; _repository.Save(book); }
我想,可能我们还是会觉得怪怪的,没有什么大的简化。那么,如果我们给NewBookParameters方法再改改呢?
public class NewBookParameters { public string Title { get; set; } public string Introduction { get; set; } public URL CoverUrl { get; set; } public BookType Type { get; set; } public BookChannel Channel { get; set; } public string Protagonists { get; set; } public string Tags { get; set; } public bool Completed { get; set; } public Book NewBook() { return new Book() { Title = this.Title, Introduction = this.Introduction, CoverUrl = this.CoverUrl, Type = this.Type, Channel = this.Channel, Protagonists = this.Protagonists, Tags = this.Tags, Completed = this.Completed }; } }
这个时候的CreateBook方法就可以极大简化了:
public void CreateBook(NewBookParameters parameters) { var book = parameters.NewBook(); _repository.Save(book); }
一般情况下,将长长的参数列表封装为一个类,可以解决大部分场景下的问题。
动与静的分离
还有一些场景,不能简单地将长参数封装为一个类,比如有些原本属于静态结构的部分却以动态参数的方式进行传递,无形之间使得参数列表变长了。
那么,不妨看一个这样的坏味道:
public void GetChapters( long bookId, HttpClient httpClient, ChapterProcessor processor) { var requestUri = GenerateRequestUri(bookId); var response = httpClient.GetAsync(requestUri).Result; var chapters = GenerateChapters(response); processor.Process(chapters); }
在这三个参数中,几乎每次传递的bookId是不一样的,但是httpClient和processor却是一样的。换句话说,bookId是变化的,而httpClient和processor却是不怎么变化的。
用专业术语来讲,这就是动数据与静数据的耦合,需要将其拆开。可以将静态不变的数据作为所在类的一部分,通过依赖注入的方式注入进去即可。
重构代码如下:
public void GetChapters(long bookId) { var requestUri = GenerateRequestUri(bookId); var response = _httpClient.GetAsync(requestUri).Result; var chapters = GenerateChapters(response); _processor.Process(chapters); }
移除标记参数
有些时候,我们喜欢将flag参数写在参数列表中,各种flag满天飞,一不小心堆积多了,也就会容易产生混乱。
比如,下面这个坏味道:
public void EditChapter( long chapterId, string title, string content, bool isApproved) { ... }
之所以有最后这个flag参数,是因为逻辑代码会根据这个flag参数走不通的处理流程。于是,又到了追问自己的时刻,这个方法的初心(业务)是为了什么?
为了贴近业务,对于flag参数需要适量移除:
// 普通编辑,需要审核 public void EditChapter(long chapterId, string title, string content) { ... } // 资深编辑,无须审核 public void EditChapterWithApproval(long chapterId, string title, string content) { ... }
可以看到,分解成两个方法之后,就消除了flag参数,但需要注意的是不要重复,对于公共部分需要封装尽可能复用以保持两个方法的尽可能独立。
3 小结
本文总结了两类坏味道,一是大类,二是长参数列表。无论是长函数方法、大类 还是 长参数列表,它们的背后都在告诉我们一件事情,即编写“短小”的代码的重要性,而要编写“短小”的代码,需要我们在设计的时候就能“分离关注点”。
最后,感谢郑晔老师的这门《代码之丑》课程,让我受益匪浅!我也诚心把它推荐给关注Edison的各位童鞋!
参考资料
郑晔,《代码之丑》(推荐订阅学习)
Martin Flower著,熊杰译,《重构:改善既有代码的设计》(推荐至少学习第三章)