文中图片来源网络
码农的博弈
如果有一天身为程序员的你华丽转身变成了甲方客户,当你收到你订制的软件产品,也许你很快就会从你过往的从业经历中发现某些糟糕、多余的设计,并在心中咒骂:
“又TM被那些美其名曰项目经理和程序员的孙子们给忽悠了,这些功能我其实都用不到,但我还花了这么多冤枉钱去购买,下次议价时一定要砍掉80%的预算。”
可能你会牢记这次教训,并运用到你下一次的采购上,很快你就发现自己和乙方陷入无休无止地争斗中(群体攻击增强300%,单体理智降低80%),并且无论你怎么要求都会发现一点你不需要的东西,这种感觉很糟糕是不是?回到现实中,你很庆幸自己还不是那个为无用设计买单的客户,同时又有点担忧自己的无用设计会被发现而被克扣钱款。所以为了避免码农的世界被破坏,并从根本上保障码农群体可怜的经济来源,就应当想办法给客户这样一种错觉:
“你要的功能必须值这个价,如果想要新增一个功能就应该要额外收费。”
开发人员想在这场博弈中获胜的最佳方法就是砍掉那些完全只为满足自我虚荣心(以此证明自己技艺是如何炉火纯青)的多余设计和实现,只完美地产出客户真正需要和关心的功能,这就是简单设计。
似乎简单的直接设计
上面的理论看着非常easy,但是,请注意这里的但是,由于汉字的博大精深与丰富内涵,再遇上程序员这种伴随二进制进化的只有0和1二个极端的特殊生物,“简单”一词的含义被引申到了更广的范围,出现了另一种在编码中随处可见的风景——直接设计。
直接设计看上去像是一种“按图索骥”的编程方法,开发人员按照流程图上的处理及分支用直白的代码表达出来,这里有一个小例子:
模块对从端口的获得信息默认情况下需要进行处理,当端口被配置为A或B类型时,则该端口获得的信息无需处理,按照直接设计产生的代码会是下面这样:
@Override
public void onMsgRecvdFromPort(RecvMsg msg) {
checkNotNull(msg);
if (msg.getIn().getPortType() == A) {
doRecord();
} else if (msg.getIn().getPortType() == B) {
doRecord();
} else {
handleMsg(msg);
}
}
这样的代码应该是日常工作中信手拈来,随处可见的,如果有那么一两个了解过clean code和重构的程序员见到了,那么这段代码可能演变成:
@Override
public void onMsgRecvdFromPort(RecvMsg msg) {
checkNotNull(msg);
if (msg.getIn().getPortType() == A ||
msg.getIn().getPortType() == B) {
doRecord();
} else {
handleMsg(msg);
}
}
或者这样
@Override
public void onMsgRecvdFromPort(RecvMsg msg) {
checkNotNull(msg);
if (!isPortTypeAOrB(msg)) {
handleMsg(msg);
}
doRecord();
}
private boolean isPortTypeAOrB(RecvMsg msg) {
PortType type = msg.getIn().getPortType();
return type == A || type == B;
}
这个代码看似挺简单,而且代码也好理解了,那是不是可以认为一切就完美了呢?其实这只是转嫁了问题,只是一种对最初代码的变体。
实际上不论编码资历深浅,大多数开发人员都写过类似的代码,这些直接设计总是自觉或不自觉地跑出来,像个幽灵一样。那么直接设计从何而来?审视自己的经历原因有不少,归结起来有以下几种可能性:
- 惯性使然
- 新手们被要求严格地按规划的流程编码,这是最快地让新手熟练起来的方法
- 开发人员误解了简单的含义,认为简单就是直接,忽视了设计,也即简单而不设计
不只是程序员,人人都爱直接设计,因为那样不费脑力、有章可循且按图索骥后责任就变成了流程的设计者,既可以轻轻松松,又能趋利避害,不这么做似乎于情于理都很难说过去。
直接设计并不代表代码质量有问题,相反只要意图足够清晰和简单,那么还是要推荐直接设计,但是直接设计有一个很突出的缺陷——总是会把过多的细节暴露出来,比如上面的代码,当用户觉得现在的实现还不能满足需要时,就会要求更多,也许会有5个、10个甚至近百个条件,那时对于开发人员而言就要不断地增加新的分支代码,就像下面的代码这样。
@Override
public void onMsgRecvdFromPort(RecvMsg msg) {
checkNotNull(msg);
if (msg.getIn().getPortType() == A) {
doRecord();
} else if (msg.getIn().getPortType() == B) {
doRecord();
} else if (msg.getIn().getPortType() == C) {
doRecord();
} else if (msg.getIn().getPortType() == D) {
doRecord();
} else if (msg.getIn().getPortType() == E) {
doRecord();
}
...
...
else {
handleMsg(msg);
}
}
并且在新增分支时还要小心翼翼地考虑与原有分支的逻辑关系,嵌套分支看来是在所难免了,用不了几个迭代,曾经漂亮的代码就会变得一堆意大利面条。
也许,万幸的是,功能都实现,你幸福地点上一根烟,满足地看着自己的杰作,突然,有个新手菜鸟心怀崇敬地问你:“大牛,这段代码是什么意思?”,你盯着代码半天心里嘀咕着,这TM是什么鬼,我怎么也看不懂了,然后只好敷衍地回答一句“这个不明白吗?回去看看设计文档!”,好不容易打发走了这个新手,项目经理找到了你,告诉了你一个晴天霹雳,客户又改需求了,可能又要新增十几个分支,你眼前一黑,感叹一声又要加班了,但又不得不重新重头解读一遍自己创作的一切,看看哪里能够插入一个新需求。
简单设计更需要设计
直截了当地设计过多地暴露细节造成扩展性和维护性也直截了当地下降,如此看来简单设计并不简单,关键是设计,因为简单设计更需要设计。我认为简单设计原则可以分成三个层次:
- 实现具有用户价值的需求,简单的说就是用户要什么你就给他什么
- 代码设计应当职责清晰,简单地说就是做好一件事
- 设计应尽可能针对一到两个问题展开,做到即设计要简单,足够针对性的解决问题即可
根据这个原则重新分析上面的例子,提炼出一些规则:
A -> it should not handle message
B -> it should not handle message
other -> it should handle message
从规则出发可以看出之前代码做了太多可以委托他人去做的事情,可以考虑将功能分离,尤其是判断逻辑与功能主体剥离,使得单个主体的功能尽量简单来满足简单设计的第二条原则,按照上述思路,转化为如下代码:
@Override
public void onMsgRecvdFromPort(RecvMsg msg) {
checkNotNull(msg);
ParseMsg(msg);
}
private void ParseMsg(RecvMsg msg) {
if (!filter(msg)) {
handleMsg(msg);
}
doRecord();
}
private boolean filter(RecvMsg msg) {
return DisabledPortFilter.getInstance()
.contains(msg.getIn());
}
其中DisabledPortFilter负责管理禁用端口,提供注册及过滤功能:
public class DisabledPortFilter {
private HashMap<InPort, FilterRule> disableHandleList =
Maps.newHashMap();
private static DisabledPortFilter portFilter =
new DisabledPortFilter();
private DisabledPortFilter() {
}
public static DisabledPortFilter getInstance() {
return portFilter;
}
public void registDisabledPort(InPort inPort,
FilterRule rule) {
disableHandleList.put(inPort, rule);
}
public void unregistDisabeldPort(InPort inPort) {
disableHandleList.remove(inPort);
}
public boolean contains(InPort in) {
return !disableHandleList.get(in)
.matchFilter(in);
}
}
FilterRule定义如下:
public interface FilterRule {
public boolean matchFilter(InPort inPort);
}
将之前的代码在一个方法中执行的过程分解到多个类中,每个类的职责更为单一,将复杂的过滤逻辑通过转化放在各个实现类中,也可以帮助开发者及维护者能够在某一时间点只关注其中某一中过滤规则。完成上述转化后,原来可能冗余繁复的分支处理消失了,取而代之的是短短的几行简单易懂的代码。并且转化后还带来了维护上的便利与代码扩展性的提升,当客户新增需求时,只需要增加对应的FilterRule实现,并注册到DisabledPortFilter中就可以,而不用去修改原有代码,不知不觉中又契合了OCP原则。
其实上面的设计仍然不是最佳,虽然想法正确但实现的过于复杂了,如果利用前面两篇有关“FizzBuzz”的博文中提到的语义模型来实现会更加容易。
对照前后例子,发生变化原因是针对逻辑判断与功能主体分离这一点问题进行了设计,后面的设计都是在此基础上展开,一次只设计一个切入点使得开发人员更容易控制开发思路,而不至于过多复杂的设计带来的思维混乱,因此简单设计原则中的第三条显得尤为重要,很多时候是我们自己想的太多而导致停滞不前,举步维艰。
简单设计之路
简单设计不只是设计的选择,而是信念和坚持的选择,大多数时候并非我们不知道简单设计,而是在一次次与时间、进度博弈的过程中自觉或不自觉地放弃了简单设计,不少简单设计只需要我们再多思考那么一点点,捅破这层窗户纸并不难,往往这片刻的思考就会对我们的编码产生巨大的影响,而你要做的只是多想一点,多看一眼。