【代码精进】| 总结/Edison Zhou
作为一个后端工程师,想必在职业生涯中都写过一些不好维护的代码。本文是我学习《代码之丑》的学习笔记,今天第三天,品品大类和长参数列表的味道。
上一篇:一天一点代码坏味道(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小结
本文总结了两类坏味道,一是大类,二是长参数列表。无论是长函数方法、大类 还是 长参数列表,它们的背后都在告诉我们一件事情,即编写“短小”的代码的重要性,而要编写“短小”的代码,需要我们在设计的时候就能“分离关注点”。
最后,感谢郑晔老师的这门《代码之丑》课程,让我受益匪浅!我也诚心把它推荐给关注EdisonTalk公众号的各位童鞋!
参考资料
郑晔,《代码之丑》(推荐订阅学习)
Martin Flower著,熊杰译,《重构:改善既有代码的设计》(推荐至少学习第三章)
👇欢迎关注EdisonTalk公众号