作为一个后端工程师,想必在职业生涯中都写过一些不好维护的代码。本文是我学习《代码之丑》的学习总结,今天第一天发车,先来看看在命名上的一些常犯的坏味道。
0 为何要品代码坏味道
Martin Flower在《重构》一书中给不好维护的这一类代码取了一个艺名:代码的坏味道,而这些坏味道一旦堆积多了,整个系统虽然还是可以平稳运行,但是就是让程序员不敢动,更别提重构了。
因为......
哎,都是打工人,谁容易啊?
1 不精准的命名
直接来看一段坏味道代码:
public void ProcessChapter(long chapterId) { Chapter chapter = _repository.GetChapterById(chapterId); if (chapter == null) { throw new ArgumentException($"Unknown chapter [{chapterId}]"); } chatper.TransactionState == TransactionState.TRANSLATING; _repository.UpdateChapter(chapter); }
此段代码存在的问题:方法名ProcessChapter命名过于宽泛,不能精准描述意图,是代码难以理解的根源所在。
同类型的词汇请各位看官自查,例如:data,info,flag,process,handle,build,maintain,manage,modify等等等,它们应该都出现在你我的项目代码中过。
重构方法:命名需要能够描述出这段代码在做的事情,根据代码的逻辑含义,调整其命名为 ChangeChapterToTranslating,是不是无论谁来接手这个方法的代码就都好理解了?
public void ChangeChapterToTranslating(long chapterId) { ...... }
一个好的名字应该是描述意图,而非细节的。那么,我们再重构一下,结合具体的业务,将其命名改为 StartTranslation,即开始翻译,是不是就更能体现业务含义了?
public void StartTranslation(long chapterId) { ...... }
2 用技术术语命名
这是一个我也经常犯的坏味道:
List<Book> bookList = service.GetBooks();
这个bookList的原因是因为它的类型是List,还有xxxDict,xxxMap等,它不费脑子,但它却只是一种基于实现细节的命名方式。
重构方法:使用面向意图的名字。
List<Book> books = service.GetBooks();
又如,如果在业务代码中出现了Redis这类的中间件:
public Book GetByIsbn(string isbn) { Book cachedBook = redisBookStore.Get(isbn); if (cachedBook != null) { return cachedBook; } Book book = service.GetByIsbn(isbn); redisBookStore.Put(isbn, book); return book; }
通常来说,这里只是需要一个缓存,那么Redis也就只是这个缓存的一个具体实现,如果换为其他的NoSQL存储如Memcached呢?
因此,我们其实缺少了一个模型,在这里其实就是一个接口,假设这个接口如下:
public interface ICacheClient { object Get(string key); void Put(string key, object value); }
使用接口替代之后,我们就是面向接口编程了:
public Book GetByIsbn(string isbn) { Book cachedBook = ICacheClient.Get(isbn); if (cachedBook != null) { return cachedBook; } Book book = service.GetByIsbn(isbn); ICacheClient.Put(isbn, book); return book; }
技术人喜欢用技术名词命名,因为这是大家习惯的语言。但是,对于业务项目而言,需要尽可能将技术术语隔离开,因为业务语言才是最好的命名方式。
DDD领域驱动设计方法就号召我们建立统一语言,这个统一语言是可以和业务部门的人员无障碍交流的语言,换句话说,也就是业务语言。另一方面来看,这就倒逼技术团队需要建立团队的业务词汇表,让团队成员达成统一共识,这也是要统一共识需要付出的额外成本,但是这个成本是有价值的。
编写可维护的代码之路
比如,我们经常会写下面的参数命名:
public void ApproveChapter(long chapterId, long userId) { ... }
假设通过业务分析,这里的user其实是审核人,而我们为了方便就命名为userId了。那么,将其改为reviewerUserId是不是更加贴近业务?
public void ApproveChapter(long chapterId, long reviewerUserId) { ... }
在实际开发中,这样子的例子还有很多。
3 乱用英语来命名
作为一个受了多年义务教育与高等教育的我们来说,English一点也不陌生,但是就是始终感觉用不到位。
如果能用中文来命名,我们是不会到处查单词用英文来命名的,但是,用中文,没有B格啊,我放弃!
违反语法规则的命名
类是一个名字,表示一个对象。而方法名一般是一个动词或动宾短语,表示一个动作。但是,有时候,我们喜欢忽视这个规则。
public void CompletedTranslate(List<string> chapterIds) { List<Chapter> chapters = _repository.GetByChapterIds(chapterIds); chapters.ForEach(chapter => chapter.TranslationState = TranslationState.TRANSLATED); _repository.SaveChapters(chapters); }
CompletedTranslate 是个啥(四不像结构)?改为 CompleteTranslation 是不是好一点(动宾结构)?
public void CompleteTranslation(List<string> chapterIds) { ...... }
又比如,一个方法名叫 ReTranslation(额,Translation好像是个名词吧),意为重新翻译,但作为方法名,应该选择动词,改为 ReTranslate 会更好(Translate是个动词,棒!)。
不准确的英语词汇
由于英文单词可以有多个含义,且在不同场景下含义也可以不同,这就难为我们中国人了。
比如,下面的一个枚举,想表达的是审核状态:
public enum ChapterAuditStatus { PENDING, APPROVED, REJECTED }
但是,Audit这个词虽然也有审核的含义,但是它更偏重于财务审计这块,而这里的业务是文稿的审核。
因此,改为Review可能会更加贴近一些:
public enum ChapterReviewStatus { ...... }
从上也可以看出,技术团队建立一个统一的业务词汇表,很有必要!这是集体的智慧,而非个体的。一个人的英语可能不太好,但是一群人在一起,总有那么一个会找出合适的说法。
英语单词拼写错误
这个,我相信大家各自项目中都有不少拼错的案例,很多人查了单词之后,都还是拼错...
但是,往往很多时候就是差了或者多了那么一个字母,就会让人懵逼了。
4 小结
本文总结了命名相关的两类坏味道,一是命名是否具有业务含义,二是命名是否符合英语语法。
最后,感谢郑晔老师的这门《代码之丑》课程,让我受益匪浅!我也诚心把它推荐给关注Edison博客的各位童鞋!
参考资料
郑晔,《代码之丑》(推荐订阅学习)
Martin Flower著,熊杰译,《重构:改善既有代码的设计》(推荐至少学习第三章)