标签:构造函数 除了 增加 first ike class 架构 += indent
在领域设计:聚合与聚合根一文中,提到了两个导致设计与代码脱节的情况:
领域设计:聚合与聚合根通过淘宝购物的例子说明了「设计的表现力不足」的问题。本文将通过《敏捷软件开发:原则、模式与实践》中保龄球计分软件的例子来说明「代码未反映出软件架构」的问题。
在开始之前,我们需要了解需求,这里就是「保龄球的记分规则」:
从上面的规则,我们可以得到初步的设计:
对象初步关系如下:
《敏捷》花了一章的内容来讨论这个软件的开发过程。初步设计如上图所示,然后通过结对编程+TDD的方式一步步的进行代码演进(具体推导过程请阅读《敏捷》,这里不再赘述),最终得到的如下代码:
public class Game { private int itsCurrentFrame = 0; private boolean firstThrowInFrame = true; private Scorer itsScorer = new Scorer(); public int score() { return scoreForFrame(itsCurrentFrame); } public void add(int pins) { itsScorer.addThrow(pins); adjustCurrentFrame(pins); } public int scoreForFrame(int theFrame) { return itsScorer.scoreForFrame(theFrame); } private void adjustCurrentFrame(int pins) { if (lastBallInFrame(pins)) { advanceFrame(); } else { firstThrowInFrame = false; } } private boolean lastBallInFrame(int pins) { return strike(pins) || !firstThrowInFrame; } private boolean strike(int pins) { return (firstThrowInFrame && pins == 10); } private void advanceFrame() { itsCurrentFrame = Math.min(10, itsCurrentFrame + 1); } } public class Scorer { private int ball; private int[] itsThrows = new int[21]; private int itsCurrentThrow = 0; public void addThrow(int pins) { itsThrows[itsCurrentThrow++] = pins; } public int scoreForFrame(int theFrame) { ball = 0; int score = 0; for (int currentFrame = 0; currentFrame < theFrame; currentFrame++) { if (strike()) { score += 10 + nextTwoBallsForStrike(); ball++; } else if (spare()) { score += 10 + nextBallForSpare(); ball += 2; } else { score += twoBallsInFrame(); ball += 2; } } return score; } private int twoBallsInFrame() { return itsThrows[ball] + itsThrows[ball + 1]; } private int nextBallForSpare() { return itsThrows[ball + 2]; } private int nextTwoBallsForStrike() { return itsThrows[ball + 1] + itsThrows[ball + 2]; } private boolean spare() { return (itsThrows[ball] + itsThrows[ball + 1]) == 10; } private boolean strike() { return itsThrows[ball] == 10; } }
从代码本身来看,实现足够简单,变量名、方法名取得都有意义,符合开发原则,有完整的单元测试。但是,代码结构没有体现出业务逻辑。
上面的代码结构如下:
从这个类关系图中,只能看出来有一个游戏(Game)和这个游戏的得分(Scorer)!这是从编程的角度一步步推导出来的代码,在推导的过程中可能是理所当然的,但是过了一段时间后,你再来看这段代码的时候,可能就不记得这段代码是干嘛的了!
另一方面,当别人来接手这段代码时,你是否是先告诉他业务逻辑,然后让他看代码?但是因为代码结构与设计的脱离,导致了虽然已经理解了业务逻辑、代码结构也很清晰,但是还是需要读了源码才能清楚这段代码具体是干嘛的!这是否是增加了理解的难度?
原因就是这个结构关系没有体现出业务逻辑!理想情况应该是在开发人员理解业务以后,从代码结构就可以理解具体的实现!
在保龄球记分逻辑中,是有轮(Frame)和投掷(Throw)这两个概念的,所以在代码中需要保留这两个类!
public class Frame {} public class Throw {}
一局游戏有十轮,所以在创建Game时就初始化十个Frame。同时,当前Frame的计算,需要前一个Frame的得分,所以除了第一个Frame,其它Frame都持有前一个Frame的引用,同时每个Frame都知道自己是第几局(roundNum)!
public class Game { private static final int MAX_ROUND = 10;// 一局有十轮 private Frame[] frameList = new Frame[MAX_ROUND]; public Game() { for (int i = 0; i < MAX_ROUND; i++) { frameList[i] = new Frame(i); if (i > 0) { frameList[i].setPreFrame(frameList[i - 1]); } } } } public class Frame { private int roundNum; // 所在局,从0开始 private Frame preFrame; public Frame(int roundNum) { this.roundNum = roundNum; } public void setPreFrame(Frame preFrame) { this.preFrame = preFrame; } }
每一次投掷都会有击倒数量,所以Throw中需要有字段表示击倒数量,同时因为一次投掷后,数量是不可修改的,所以数量由构造函数传入,只有get方法而没有set方法:
public class Throw { private int num; // 击倒数量 public Throw(int num) { this.num = num; } public int getNum() { return num; } }
Frame可以包括1到3次Throw,而按照全中、补中、其它击中的不同,记分方式也有所不同。如果完全按照这个逻辑编写,代码会相对复杂。因为需要根据击倒方式的不同,判断是否要获取后两次的投掷。我们是否可以做一些调整?我们实际上是要计算投掷的得分,那么这个投掷属于哪一轮,是不是就不是那么重要了?也就是说,投掷和记分规则可以调整为下面这样:
现在Frame分数的计算就统一了!
public class Frame { private List<Throw> throwList = new ArrayList<>(); public int score() { int throwScore = throwList.stream().mapToInt(it -> it.getNum()).sum(); if (preFrame != null) { throwScore += preFrame.score(); } return throwScore; } }
最后,就是怎么将一个Throw添加到Frame中,按照上面的设计调整,一次Throw可能既属于当前轮,也属于上一轮甚至上上轮!怎们样来判断呢?根据Frame是全中、还是补中还是其它来判定,所以Frame中需要有方法来判定自身是全中、补中还是其它!
public class Frame { private boolean isSpare() { // 是否是补中 return throwList.size() >= 2 && throwList.get(0).getNum() < 10 && (throwList.get(0).getNum() + throwList.get(1).getNum() == 10); } private boolean isStrike() { // 是否是全中 return throwList.size() >= 1 && throwList.get(0).getNum() == 10; } }
一次Throw添加到Frame后,还要判断这个Frame是否已经结束,即:
public class Frame { public boolean isFinish() { if (throwList.size() == 3) return true; if (throwList.size() == 2 && !isStrike() && !isSpare()) { return true; } return false; } }
同时还要判断,是否进入下一轮:
public class Frame { public int add(Throw aThrow) { this.throwList.add(aThrow); if (isStrike() || isSpare() || isFinish()) return Math.min(9, roundNum + 1); return roundNum; } }
Game就是将Throw添加到当前轮和上一轮及上上轮的逻辑:
public class Game { public void add(int pins) { Throw aThrow = new Throw(pins); add2PreFrame(aThrow);// 根据逻辑判定是否要添加到上一轮,或上上轮 currentFrameIdx = frameList[currentFrameIdx].add(aThrow); // 添加当前轮后,是否进入下一轮 } private void add2PreFrame(Throw aThrow) { if (currentFrameIdx - 1 >= 0 && !frameList[currentFrameIdx - 1].isFinish()) { frameList[currentFrameIdx - 1].add(aThrow); } if (currentFrameIdx - 2 >= 0 && !frameList[currentFrameIdx - 2].isFinish()) { frameList[currentFrameIdx - 2].add(aThrow); } } }
调整后的设计如下:
对应的类结构如下:
此结构与设计相符和,只要理解了业务逻辑,顺着业务就可以梳理出代码结构,即使不看源码,也能猜到代码的逻辑!
《敏捷》中有效代码行数为71行,上面的有效代码为79行,多了8行代码!但是从理解上来看的话,后者更易于理解!完整代码见下文。
public class Game { private static final int MAX_ROUND = 10;// 一局有十轮 private Frame[] frameList = new Frame[MAX_ROUND]; private int currentFrameIdx = 0; public Game() { for (int i = 0; i < MAX_ROUND; i++) { frameList[i] = new Frame(i); if (i > 0) { frameList[i].setPreFrame(frameList[i - 1]); } } } public int score() { return frameList[currentFrameIdx].score(); } public void add(int pins) { Throw aThrow = new Throw(pins); add2PreFrame(aThrow); currentFrameIdx = frameList[currentFrameIdx].add(aThrow); } private void add2PreFrame(Throw aThrow) { if (currentFrameIdx - 1 >= 0 && !frameList[currentFrameIdx - 1].isFinish()) { frameList[currentFrameIdx - 1].add(aThrow); } if (currentFrameIdx - 2 >= 0 && !frameList[currentFrameIdx - 2].isFinish()) { frameList[currentFrameIdx - 2].add(aThrow); } } public int scoreForFrame(int theFrame) { return frameList[theFrame - 1].score(); } } public class Frame { private int roundNum; // 所在局,从0开始 private Frame preFrame; private List<Throw> throwList = new ArrayList<>(); public Frame(int roundNum) { this.roundNum = roundNum; } public int score() { int throwScore = throwList.stream().mapToInt(it -> it.getNum()).sum(); if (preFrame != null) { throwScore += preFrame.score(); } return throwScore; } public int add(Throw aThrow) { this.throwList.add(aThrow); if (isStrike() || isSpare() || isFinish()) return Math.min(9, roundNum + 1); return roundNum; } public boolean isFinish() { if (throwList.size() == 3) return true; if (throwList.size() == 2 && !isStrike() && !isSpare()) { return true; } return false; } private boolean isSpare() { return throwList.size() >= 2 && throwList.get(0).getNum() < 10 && (throwList.get(0).getNum() + throwList.get(1).getNum() == 10); } private boolean isStrike() { return throwList.size() >= 1 && throwList.get(0).getNum() == 10; } public void setPreFrame(Frame preFrame) { this.preFrame = preFrame; } } public class Throw { private int num; // 击倒数量 public Throw(int num) { this.num = num; } public int getNum() { return num; } }
本文通过《敏捷》中保龄球的例子,来说明了代码不能体现设计的原因及提出一种保证代码和设计相一致的方法。
设计本身就是一种取舍,没有完全正确的方法,只有适合的方法。从代码本身出发,能够构建出符合编码原则的代码,但是可能和设计本身有出入,这可能会增加后续的理解难度,变相增加了修改代码的难度;反之从设计触发,能构建出和设计相匹配的代码,但是可能代码本身的易读性、代码量、符合编码原则上会有所妥协。
个人认为,对于业务逻辑不复杂,但是计算逻辑很复杂的代码,以按照代码原则来编写代码为主,以按照业务逻辑编写代码逻辑为辅,以保证代码的简洁明了;而对于业务逻辑复杂,但是计算逻辑不复杂的代码,以按照业务逻辑编写代码为主,以按照代码原则编写代码为辅,以保证代码结构与业务逻辑的直观匹配。
以上内容仅为个人观点,欢迎探讨!
标签:构造函数 除了 增加 first ike class 架构 += indent
原文地址:https://www.cnblogs.com/ivaneye/p/14127388.html