经常听到程序员们针对代码设计的一些讨论,A对B说:“我的这个设计用了策略模式和状态模式,假如后面客户会有这样的需求,可以无缝扩展,无比健壮。” B听着一脸狐疑,心中已经念叨了数遍 :"哼,这就是典型的过度设计",但B也不好说出口,因为上周才因为过度设计的分歧打了一次口水仗了。
我在项目中经常碰到这样的争论,因为设计的好坏本身没有一个标准权威的答案,这么多年各路大神提出的设计原则就够我们学几个学期,而且还是个似懂非懂。久而久之,每个人心中都有自己的一杆秤,导致了后来什么是好的设计就成了公说公有理婆说婆有理的问题,谁也说服不了谁。
设计是一个很大的话题,为了更好的起步,我尝试将范围缩小到敏捷团队程序员交付用户故事卡时的代码设计,不谈架构设计和系统设计。从变量、常量、方法、类、类与类之间的关系、对象的交互开始,一起来聊聊什么是设计的问题。
抽象的设计问题大大提升了初学者的学习门槛,想得太多怕被说过度设计,吃饱撑着没事找事。想少了,又怕被人认为能力不足,无脑编码。那到底怎么办,怎么样才能做出好的设计?SOLID、GoF的23种设计模式、STUPID、GRASP这些原则学会了就可以了吗?No,统统忘掉这些抽象不接地气的设计原则。起步,不要让自己那么难。来看看,极限编程领域的大神Kent Beck很早前就提出了几条容易理解的参考原则:
虽然Kent Beck提出的这四条原则里面也存在揭示意图
这样一个每个人持有不一样标准的概念,但至少这一条在很大程度上能够代表代码的可读性,优秀程序员对代码可读性有90%相似度,可能在10%有钻牛角尖般的争议,也没有那么重要了。
其他几条则看起来就非常具体了,而且都能见字如意,测试一定要通过、重复就应该消除、元素没必要就不要存在。代码读不懂,那就就多找几个人看看。
通过测试 通常会被一概地理解为通过自己在项目中所编写的各种自动化测试,这么理解,也没有什么问题,但是需要满足两个前提条件:
如果你的项目中没法满足这两点,当然,99%的项目是做不到的(还有1%我只听过)。此时你需要换一个角度去理解 通过测试。
想想你为什么写测试,测试在测什么,不就是为了增强你对系统满足了业务需求的信心吗?所以通过测试 广义理解为要满足业务需求,不论是自动化测试还是手工测试,你需要做的是满足业务需求,只不过我们推荐自动化能自动化的测试。
重复乃万恶之源 -- SJ
重复意味着低内聚、高耦合,导致的后果是难以修改(霰弹式修改),必然降低系统对变化的响应力。响应力的降低势必会造成维护工作量的提升,我的简单设计价值观 一文中的懒惰 将驱使我尽我所能消除这些重复,从而减少修改时的工作量,提升软件的响应力。
对于揭示意图,我们只聊程序员那90%认同区间。可从一增一减两个方向同时去努力,从而达到揭示意图:
不管是哪个方面,目的只有一个,回答一个问题:这个代码读者读起来更容易理解了吗?
那么在增强方面,比如在变量、方法以及类的命名等,我们都竭尽全力去赋予它们一个更加表达业务的名字,让它们能够自解释,从而让读者能够在深入细节之前就能够在较高层次上快速理解代码的意图。
减弱方面,比如在注释、方法的组织、类的交互设计等,可以去除不必要的注释,控制方法体的大小、降低类交互复杂度来让代码更纯净,从而让读者更好地聚焦在核心代码上。
既然说的是代码,那么充斥在你的代码库中的任何东西都可以理解是元素。当然,我们还是焦点聚焦在与代码相关的元素,比如,变量、常量、注释、注解、关键字、包。
最少元素 的核心思想是:在不必要的时候,尽可能减少代码元素来降低代码复杂度,保持简洁,贯彻less is more的思想,它道出了简单设计的精髓。
简单设计四原则给设计决策提供了有效的指导,在实际运用过程中,当面临冲突时,我们如何取舍,Kent Beck也给出了一个优先级顺序参考:通过测试 > 消除重复 >= 揭示意图 > 最少元素
以上四条优先级依次降低,这就话有点类似敏捷宣言中的最后一句:也就是说,尽管右项有其价值,我们更重视左项的价值。
通过测试,这条优先级最高的原则告诉我们任何时候我们编写的软件是要为客户创造真实价值的,如果为了消除重复、揭示意图或减少代码元素,而编写出不符合客户期望的软件,这就是好比捡了芝麻丢了西瓜。所以第一条没有满足的情况下,打着满足后三条的口号都是无稽之谈。
消除重复,一方面,已经存在的重复代码应当试图消除掉,降低修改的成本。另一方面,也在强调我们不应该为未来编码,即为一个尚未出现的重复或变化方向去增加额外的复杂度,比如建立一个抽象接口,却只有一个实现。
揭示意图,在我看来,跟消除重复 不相上下,绝大多数时候这两条是相辅相成的,不会因为消除重复而有损揭示意图,也不会通过引入重复增加揭示意图。在实际开发中我们应该尽量同时遵循这两条原则来提高软件的质量。
最少元素,这条优先级最低的原则告诉我们除非在增加了代码元素之后,能够消除重复或揭示意图或者通过测试,否则我们不应该增加代码元素。如果,你为了消除重复逻辑而抽取了一个公共方法,或者会为了揭示意图增加一个常量来替代魔鬼数字。你打着为了满足优先级更高的原则,而去违背优先级更低的原则,这个就符合了这个原则思想理念。
如果,在工作中你的领导的领导的领导的领导(4个人),当他们给你的指令有冲突是,你只用听更高级领导的指令。
示例:抽取公共方法
public class DateFormater {
public LocalDate formatUserBirthday(String birthdayStr) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
return LocalDate.parse(birthdayStr, formatter);
}
public LocalDate formatRegisterDate(String registerDateStr) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
return LocalDate.parse(registerDateStr, formatter);
}
}
public class DateFormater {
public LocalDate formatUserBirthday(String birthdayStr) {
return format(birthdayStr);
}
public LocalDate formatRegisterDate(String registerDateStr) {
return format(registerDateStr);
}
private LocalDate format(String birthdayStr) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
return LocalDate.parse(birthdayStr, formatter);
}
}
示例:常量代替魔鬼数字
public class Scheduler {
public void executeJobs(int jobNumbers){
if (jobNumbers < 1000000){
for (int i = 0; i < jobNumbers; i++) {
execute();
}
}
}
}
public class Scheduler {
public static final int MAC_CONCURRENT_EXECUTOR_NUMBERS = 1000000;
public void executeJobs(int jobNumbers){
if (jobNumbers < MAC_CONCURRENT_EXECUTOR_NUMBERS){
for (int i = 0; i < jobNumbers; i++) {
execute();
}
}
}
}
针对揭示意图、去除重复 这两条业界存在一些争论,觉大多数情况下,这两者并不冲突,在我的经验中,可能在一些测试用例中会通过引入重复来避免逻辑分隔等测试坏味道,从而对读者更加友好。Kent Beck也提出唯一让他有印象的冲突是发生在测试用例 [2]。
Kent Beck 提出的简单设计原则更多关注的是代码设计,简单设计思想其实可以运用在架构设计、沟通协作上。
架构设计
沟通协作
简单设计价值观甚至会影响你的生活方式,辅以断、舍、离的心态修炼,相信你的生活会逐渐变得简约而不简单。