「关於函式的首要准则,就是要简短。第二项准则,就是要比第一项的简短函式还要更简短。这是一个我无法证明的主张」
「我曾经写过令人难受的 3000 行函式怪物,写过数不清的 100 至 300 行大小的函式,也写过只有 20 到 30 行的函式。这些经验告诉我,函式应该要非常简短」
取自: Clean Code (p.40)
先来个例子,请试着浏览下列 Code [1]并大致想像功能:
public class HtmlUnit {
public static String testableHtml(
PageData pageData,
boolean includeSuiteSetup
) throws Exception {
WikiPage wikiPage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();
if (pageData.hasAttribute("Test")) {
if (includeSuiteSetup) {
WikiPage suiteSetup =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_SETUP_NAME, wikiPage
);
if (suiteSetup != null) {
WikiPagePath pagePath =
suiteSetup.getPageCrawler().getFullPath(suiteSetup);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -setup .")
.append(pagePathName)
.append("\n");
}
}
WikiPage setup =
PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
if (setup != null) {
WikiPagePath setupPath =
wikiPage.getPageCrawler().getFullPath(setup);
String setupPathName = PathParser.render(setupPath);
buffer.append("!include -setup .")
.append(setupPathName)
.append("\n");
}
}
buffer.append(pageData.getContent());
if (pageData.hasAttribute("Test")) {
WikiPage teardown =
PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
if (teardown != null) {
WikiPagePath tearDownPath =
wikiPage.getPageCrawler().getFullPath(teardown);
String tearDownPathName = PathParser.render(tearDownPath);
buffer.append("\n")
.append("!include -teardown .")
.append(tearDownPathName)
.append("\n");
}
if (includeSuiteSetup) {
WikiPage suiteTeardown =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_TEARDOWN_NAME,
wikiPage
);
if (suiteTeardown != null) {
WikiPagePath pagePath =
suiteTeardown.getPageCrawler().getFullPath (suiteTeardown);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -teardown .")
.append(pagePathName)
.append("\n");
}
}
}
pageData.setContent(buffer.toString());
return pageData.getHtml();
}
}
P.S. 笔者先自首,这是我第三次阅读本书,事实上我没有一次花超过 10 秒钟在看这段 Code... 匆匆浏览的感想是这应该是一段跟 Html Render 有关的 Code,带有 Mock (Test) 的功能切换、也许还做了一些不明的 Setup?
上述的 Code 不仅符合前面所提到的命名、就连缩排风格笔者也用 Formatter 美化过了 (原书中更乱)
究竟出了什麽问题,导致程序码的可读性下降?
接下来我们透过提取几个函式来重构上面的 Code...
public class HtmlUnit {
public static String renderPageWithSetupsAndTeardowns(
PageData pageData,
boolean isSuite
)
throws Exception {
boolean isTestPage = pageData.hasAttribute("Test");
if (isTestPage) {
WikiPage testPage = pageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent());
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
return pageData.getHtml();
}
}
我想上述的 Code 已经 Clean 到不需要注解和文字介绍了,任何修过程序设计的学生应当都能猜出这段 Code 在做什麽了。顺带一提,上面的函式是可测试的 (Testable),我们会在後面的章节介绍「测试驱动设计 (TDD)」
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
甚至只有 2, 3 或 4 行 (关於这点笔者是持保留看法的...)「函式应该只做一件事情」
思考:何谓「一件事」?
上述例子其实做了三件事:
那麽该如何判断呢?
「函式只做函式名称下 『同一层抽象概念』 的几个步骤」
因此,判断函式是否做超过「一件事」的方法为
「看你是否能从此函式中,提炼出另一个新函式」
且,此新函式的提取会导致抽象概念的进一步简化或改变
class Bird {
double getSpeed() {
switch (type) {
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
case NORWEGIAN_BLUE:
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
throw new RuntimeException("Should be unreachable");
}
}
上述的 Switch 内包含了太多细节了。这导致此函式破坏了 「单一职责原则 (SRP)」 及 「开放封闭原则 (OCP)」 (笔者会在 Clean Architecture 篇详细介绍此类设计原则) abstract class Bird {
abstract double getSpeed();
}
class European extends Bird {
double getSpeed() {
return getBaseSpeed();
}
}
class African extends Bird {
double getSpeed() {
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
}
}
class NorwegianBlue extends Bird {
double getSpeed() {
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
}
speed = bird.getSpeed();
透过这样子的更改,不仅封装了底层细节、也提升了程序的可扩充性和维护性。详细的说明读者可参见 Reference参数数量,最理想的是 0 个,至多用到 3 个
无论如何都不该超过 3 个参数,除非有非常特殊的理由
includeSetupPage()
比 includeSetupPage(newPageContent)
更容易理解。因为参数会强迫你去了解更多目前不重要的细节
Circle makeCircle(double x, double y, double radius);
// 将相似概念的参数放在一起
Circle makeCircle(Point center, double radius);
避免输出型参数 (Output Parameter)
StringBuffer transform(StringBuffer in)
会比 void transform(StringBuffer out)
更洽当不要使用旗标参数 (Flag Parameter)
「使用旗标参数是一种非常烂的做法」
与其将 boolean 值传递给函式,不如直接 Return 处理完後的 boolean 值。或者直接拆成不同函式
render(true)
render(false)
// vs.
renderForSuite()
renderForSingleTest()
「回传 null 是在给自己增加额外的工作量,也是在给呼叫者找麻烦」
「传递 null 到方法里是更糟糕的行为,应该尽可能避免传递 null」
取自: Clean Code (pp.123-124)
「函式应该要能做某件事,或能回答某个问题,但两者不该同时发生」
例子:
// Confusing
if (set("name", "bob")){
...
}
vs.
// Concrete
if (attributeExists("name")){
setAttribute("name", "bob");
}
[补充]: Command 和 Query 的混杂不仅在代码层级会造成阅读混淆,考虑到 Database 大量读写的情境,则可能导致一致性 (Consistency) 和权限控管不易的问题
上升到架构层面後衍生出 「命令与查询分离 (CQS)」 、 「命令与查询责任隔离 (CQRS)」 ...等模式
可参见 Reference [10], [11]
public void delete(Page page) {
try{
deletePageAndAllReferences(page);
}
catch (Exception e){
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception{
// ...
}
「虽然 Clean Code 是易读的,但它也必须是耐用的。当我们将错误处理看作是另一件重要的事,将之处理成独立於主要逻辑的可读程序,代表我们写出了整洁又耐用的程序码。在程序的维护性方面也向前迈进了一大步」
取自: Clean Code (p.126)
以四则运算为例子 [13],有时候我们只关心运算过程如何出错,例如 Catch 到 "ArithmeticException",对於更详细的 "DivideByZeroException" 则非呼叫者 (四则运算器) 所关注的细节
可以透过 Wrapper 设计技巧让程序只回传共用的例外型态
LocalPort port = new LocalPort(0);
try
{
port.open();
}
catch (PortDeviceFailure e)
{
// error logging...
}
finally
{
// ...
}
public class LocalPort
{
private ACMEPort innerPort;
public LocalPort(int portNumber)
{
innerPort = new ACMEPort(portNumber);
}
public void open() {
try
{
innerPort.open();
}
catch (DeviceResponseException e)
{
throw new PortDeviceFailure(e);
}
catch (ATM1212UnlockedException e)
{
throw new PortDeviceFailure(e);
}
catch (GMXError e)
{
throw new PortDeviceFailure(e);
}
}
}
上述包裹第三方函式库的做法是非常好的技巧,可以减少对第三方 API 的依赖
<<: 全端入门Day19_前端程序撰写之JavaScript
>>: Python - 修正 python pandas 模组的 runtimeError: package fails to pass a sanity check 错误处理参考笔记
中秋连假结束~ 参考线完成开始放入标题~ 把textview跟参考线连在一起~ 使用TextView...
这是 Roblox 从零开始系列,游戏环境章节的第五个单元,今天你将学会如何在游戏内播放背景音乐 【...
在昨天我们安装完PMM监控,今天来认识几个监控收集的资料帮助我们了解资料库各方面的执行详情与效能监...
此系列文章会同步发文到个人部落格,有兴趣的读者可以前往观看喔。 终於来到铁人赛第30天!谢谢观看我...
密钥分发是将加密密钥从一方发送到另一方的过程。对称和非对称密码术都面临着密钥分发的挑战。这个问题询问...