In domain Design: Aggregations and aggregations roots, I mentioned two situations that lead to a disconnect between design and code:

  • The code does not reflect the software architecture: the architecture diagram says one thing and the code says another
  • The design is not expressive: the design does not embody certain constraints, and you need to read the code implementation to understand the details

Domain design: aggregation and aggregation root through taobao shopping example to illustrate the “insufficient expression of design” problem. This article uses the example of bowling ball scoring software in Agile Software Development: Principles, Patterns, and Practices to illustrate the problem of “code not reflecting software architecture”.

Bowling scoring rules

Before we begin, we need to understand the requirements, and here are the “bowling scoring rules” :

  • A bowling round consists of 10 rounds, and in each round contestants may toss twice in an attempt to knock down all the pins.
  • If the contestant knocks down all the pins at once, the round is called “strike” and the round ends.
  • If the contestant does not knock out all the bottles on the first try and does so on the second try, it is called a “patch”.
  • If both throws fail to knock down all the pins in a round, the round ends.
  • Scoring rules for strikes: 10 points for knockdowns in this round, plus the number of pins knocked down in the next two throws, plus the number of points scored in the previous round
  • Bonus round scoring rules: 10 points scored by knockdown in the current round, plus the number of pins knocked down in the next toss, plus the points scored in the previous round
  • Other round scoring rules: the number of pins knocked down twice in the round, plus the previous round score
  • If a strike is scored in the tenth round, the contestant can take two more shots to complete the score for the strike
  • Correspondingly, if the tenth round is a spare-up, the contestant may make one more shot to complete the score for the spare-up.

The preliminary design

From the above rules, we can get a preliminary design:

  • A Game has 10 frames.
  • One to three throws per round (Frame)
  • Strike is a throw
  • The other is two flips
  • If the last round is a strike or a spare, it is three tosses
  • That is, the game can take up to 23 shots
  • The scoring rules for each round are as follows:
  • Full round: 10 points in this round + the score of the next two throws + the score of the previous round
  • Supplementary round: 10 points in this round + points in the next throw + points in the previous round
  • Other rounds: the total score of the two throws in this round + the score of the previous round
  • In other words, the game score is the score of the current round

Preliminary object relationship is as follows:

! [architecture oriented program (oop)] (https://p6-tt.byteimg.com/origin/pgc-image/06312b2d5f874bb380d18771109fc7ee?from=pc)

Code in Agile

Agile devotes a chapter to this software development process. The preliminary design is shown in the figure above, and then the code evolution is carried out step by step through pair programming +TDD (please read Agile for detailed derivation process, which will not be described here), and finally the following code is obtained:

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;
 }
}
Copy the code
  • A Game class represents the Game itself
  • A Scorer class is used to calculate a Game’s score
  • Frames and throws from the original design are hidden in the code

From the code itself, the implementation is simple enough that variable names and method names make sense, conform to development principles, and have complete unit tests. However, the code structure does not reflect the business logic.

The code structure above is as follows:

! [architecture oriented program (oop)] (https://p1-tt.byteimg.com/origin/pgc-image/6086da09c6e94d99afbf7ac38b0ca24b?from=pc)

From this class diagram, you can only see that there is a Game and a Scorer for that Game! This is code from a programming point of view, step by step, it may be the derivation of the process of course, but after a period of time, when you look at the code, you may not remember what the code is about!

On the other hand, when someone else takes over the code, do you tell them the business logic first and then show them the code? But because of the separation of the code structure and design, resulting in although the business logic has been understood, the code structure is also very clear, but still need to read the source code to understand what this code is specifically! Does that make it harder to understand?

The reason is that the structural relationship does not embody business logic! Ideally, after developers understand the business, the implementation should be understood from the code structure!

Derive from business

In bowling scoring logic, there are two concepts of wheel (Frame) and Throw (Throw), so keep these two classes in your code!

public class Frame {}
public class Throw {}
Copy the code

A Game has ten rounds, so ten frames are initialized when the Game is created. In addition, the current Frame calculation requires the score of the previous Frame, so all frames except the first Frame have references to the previous Frame, and each Frame knows its roundNum!

public class Game { private static final int MAX_ROUND = 10; Private Frame[] frameList = new Frame[]; 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; // Office, starting from 0 private Frame preFrame; public Frame(int roundNum) { this.roundNum = roundNum; } public void setPreFrame(Frame preFrame) { this.preFrame = preFrame; }}Copy the code

Each Throw has a knockdown count, so we need a field in the Throw to indicate the number of knockdowns. Also, since the count is not modifiable after a Throw, the count is passed in by the constructor, which has only the get method instead of the set method:

public class Throw { private int num; Public Throw(int num) {this.num = num; } public int getNum() { return num; }}Copy the code

Frames can include one to three throws, and scores vary depending on strikes, spares, and other hits. If you write this logic completely, the code will be relatively complex. Because depending on the type of knockdown, you need to decide whether or not to take the next two throws. Can we make some adjustments? We’re actually counting the score of the toss, so it doesn’t really matter which round the toss is in? In other words, the throwing and scoring rules can be adjusted as follows:

  • One to three throws per round (Frame)
  • Strike is one throw in the current round + two throws in the next round
  • Fillers are two current throws + one subsequent throw
  • The other is two flips
  • That is, the game can take up to 23 shots
  • The score of each round is the sum of the score of the current Frame throw + the score of the previous round

Now the Frame score calculation is unified!

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;
 }
}
Copy the code

Finally, there is how to add a Throw to the Frame. Depending on the design above, a Throw may belong to the current round, the previous round, or even the previous round! How can we judge? To determine whether a Frame is full, complementary, or something else, a Frame needs a method to determine whether it is full, complementary, or something else!

Public class Frame {private Boolean isSpare() {// Whether to 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; }}Copy the code

After a Throw is added to a Frame, we also determine whether the Frame is finished, i.e.

  • If the Frame is a full or a complement, does it already contain three flips

  • If the Frame is a normal knockdown, does it already contain two throws

    public class Frame {

    public boolean isFinish() { if (throwList.size() == 3) return true; if (throwList.size() == 2 && ! isStrike() && ! isSpare()) { return true; } return false; }

    }

At the same time, to decide whether to go to the next round:

public class Frame { public int add(Throw aThrow) { this.throwList.add(aThrow); if (isStrike() || isSpare() || isFinish()) return Math.min(9, roundNum + 1); return roundNum; }}Copy the code

Game is the logic that adds throws to the current round and to the previous round and the previous round:

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); }}}Copy the code

The adjusted design is as follows:

  • A Game has 10 frames.
  • A Throw score may be in the range of one to three frames.
  • Belong to the current wheel
  • If the previous round was a strike or a spare-up, the toss also belongs to the previous round
  • If the upper wheel is a strike, the throw is also an upper wheel
  • The game can be thrown up to 23 times
  • The score of each round is the sum of the score of the current Frame throw + the score of the previous round

The corresponding class structure is as follows:

! [architecture oriented program (oop)] (https://p1-tt.byteimg.com/origin/pgc-image/5f4569635a814ec588281e49f8257bda?from=pc)

This structure is consistent with the design and, as long as you understand the business logic, you can comb out the code structure along the business, even if you don’t look at the source code, you can guess the logic of the code!

The number of valid lines of code in Agile is 71, and the number above is 79, 8 more lines! But in terms of understanding, the latter is easier to understand! See below for the complete code.

The complete code

public class Game { private static final int MAX_ROUND = 10; Private Frame[] frameList = new Frame[]; 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; // Office, starting from 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; }}Copy the code

conclusion

This article uses the example of bowling in Agile to explain why code does not reflect design and proposes a method to ensure that code and design are consistent.

Design itself is a kind of trade-off, there is no completely correct method, only suitable method. Starting from the code itself, it can build the code in line with the coding principles, but it may be different from the design itself, which may increase the difficulty of subsequent understanding and increase the difficulty of modifying the code. On the other hand, the design trigger can build the code matching the design, but the code itself may compromise the readability, code quantity, compliance with the coding principle.

In my opinion, for the code whose business logic is not complex but whose calculation logic is very complex, the code should be mainly written in accordance with the code principle, and the code logic should be written in accordance with the business logic as a supplement, so as to ensure the simplicity of the code; For the code with complex business logic but uncomplicated calculation logic, the code should mainly be written in accordance with business logic, and the code should be written in accordance with code principles as a supplement, so as to ensure an intuitive match between code structure and business logic.

The above content is only personal opinion, welcome to discuss!

The resources

  • Agile Software Development: Principles, Patterns, and Practices