Original link: https://www.piglei.com/articles/the-answer-is-in-the-code-fulfill-requirements/
Realize the “rock, paper, scissors” game
One day, I saw an interesting discussion in a Python technical group. The discussion started with such a requirement:
Topic: Write code to simulate the “rock, paper, scissors” game. Players A and B play 10 random games and print the results. Requirements: Use the number 0 to represent rock, 1 to represent scissors, and 2 to represent paper.
Immediately following is a piece of Python code that implements the requirement. As follows:
import random def game(): """生成一局随机游戏,并打印游戏结果。""" a = random.randint(0, 2) b = random.randint(0, 2) print(f"玩家A:{a},玩家B:{b}") if a == b: print("平局") elif a == (b + 1) % 3: print("玩家B 获胜") else: print("玩家A 获胜") if __name__ == '__main__': for num in range(10): print(f">>> Game #{num}") game()
It is not difficult to see that the way the code implements the requirements is a bit ingenious, mainly reflected in elif a == (b + 1) % 3
. To deduce this line of code, the original author needs to go through the following steps of thinking:
-
[石头, 剪刀, 布]
correspond to [0, 1, 2] numbers respectively -
[石头, 剪刀, 布]
This sorting order happens to be that the former wins the latter, for example, “Rock (0)” grams “Scissors (1)”- From this, the judgment statement is derived:
a == (b + 1)
- From this, the judgment statement is derived:
- When it comes to “cloth”, the previous rule goes back to the head of the list: “cloth(2)” wins over “stone(0)”
- This leads to the modulo operation:
a == (b + 1) % 3
- This leads to the modulo operation:
For this piece of code, the main point of debate was “performance”, that is, what impact would it have on the execution performance of the code after the branches were reduced through the modulo operation. But when I saw the code, another lingering question popped up in my mind: “Does this code actually fulfill the requirement?”
Undoubtedly, judging from the execution results, it does fulfill the requirements:
>>> Game #0玩家A:2,玩家B:1玩家B 获胜>>> Game #1玩家A:0,玩家B:1玩家A 获胜...
But the key point of the problem is that the description of “implementation requirements” actually has double meanings, and this code only meets the first level.
The first meaning of “implementing requirements” is literal, referring to whether the code meets the expected functions. For example, when the requirement says: “Prompt the user after clicking the button”, we add a piece of event monitoring code to pop up a dialog box; when the requirement says: “Simulate a ‘rock-paper-scissors’ game”, we write the above code. The first meaning is for ordinary users.
In addition to the first meaning, there is another deeper and more hidden meaning. In this context of heavy meaning, we focus on another question that has nothing to do with users: “Can people who read the code see that the code fulfills the requirements?” It is aimed at the readers of the code.
Code reading experiences vary. When we read good code, we can easily picture the requirements in our brains, and each line of code and the original requirements are connected by invisible lines. With the help of the medium of code, the requirements are crystal clear in front of us, fully visible.
And reading bad code is like looking for a lost item in a pond. We never know if we dig down with this hand, it will be mud or a watch. The needs are hidden in the muddy muddy water, with blurred outlines.
Reading the “rock-paper-scissors” code above is like being in a pond.
Improved “Rock, Paper, Scissors”
In order to better “realize the requirements”, I rewrote a “rock-paper-scissors” code. As follows:
import random ROCK, SCISSOR, PAPER = range(3) # 构建“赢”的基础规则:“我:对手” WIN_RULE = { ROCK: SCISSOR, SCISSOR: PAPER, PAPER: ROCK, } def build_rules(): """构建完整的游戏规则""" rules = {} for k, v in WIN_RULE.items(): rules[(k, v)] = True rules[(v, k)] = False return rules def game_v2(rules): """生成一局随机游戏,并打印游戏结果。""" a = random.choice([ROCK, SCISSOR, PAPER]) b = random.choice([ROCK, SCISSOR, PAPER]) print(f"玩家A:{a},玩家B:{b}") if a == b: print("平局") elif rules[(a, b)]: print("玩家A 获胜") else: print("玩家B 获胜") if __name__ == '__main__': rules = build_rules() for num in range(10): print(f">>> Game #{num}") game_v2(rules)
The main change of the new code is to explicitly express the rules of the “rock-paper-scissors” game. By defining the WIN_RULE
dictionary, we clearly convey to readers the most important part of the entire requirement, which is the game rules themselves: “rock beats scissors”, “scissors beats paper”, “book rocks”.
All the remaining codes are basically supplements and extensions to this important information. For example, through build_rules()
function, the rules are expanded into a result table that can be directly evaluated; in the branch statement, directly access rules
to obtain the result.
In every sense of the word, the new code fulfills the requirements well.
related extension
The new version of the “rock, paper, scissors” code uses the “data-driven” technique-taking a game rule table to drive the entire program. Doing so has some additional benefits besides allowing the code to explicitly align the requirements. For example, it becomes very easy to adjust the rules of the game, just modify WIN_RULE
.
In addition to “data-driven”, there are many ideas and norms in the field of programming, which are actually serving the second meaning of “realizing requirements”.
good naming and structure
When writing code, if you pay more attention to variable and function names and make them more descriptive, you can effectively reduce the cost of people understanding the code. Try comparing the following two pieces of code:
# 来自“石头剪刀布”旧版本a = random.randint(0, 2) b = random.randint(0, 2) # 来自“石头剪刀布”新版本a = random.choice([ROCK, SCISSOR, PAPER]) b = random.choice([ROCK, SCISSOR, PAPER])
The new version is obviously better understood, and it is closer to the requirement of “let A and B punch randomly”. In contrast, the old version’s use of randint()
is easily confusing.
Introducing additional abstractions
While code that is overly abstract is bad, in reality, code that lacks abstraction is far more common. If there is a lack of abstraction in the code, the truth of the requirements will be submerged in countless details. Even if the code is as big as a nail piece, it needs to be read over and over again to understand.
Therefore, programmers must be good at using various tools (functions, classes, modules) to create appropriate abstractions so that requirements can be perfectly integrated into the code.
When dealing with some small requirements with extremely narrow contexts (such as solving an algorithm problem), it is especially easy for people to ignore abstraction. They tend to write only one function, which is long and full, and crams all algorithms and logic into it.
For such small needs, we still need to keep the second meaning in mind. When necessary, split out some small functions, which will make the algorithm easier to understand and maintain.
Object-Oriented Programming
An important reason for the popularity of object-oriented programming is that it corresponds well to the models in the real world, and the requirements are hidden in these models and the relationships between them.
For example, in the object-oriented world, we can easily create a duckling class and add a “quack” method to it. Someone reading the code can easily identify our intent.
class Duck: def __init__(self, name): self.name = name def quack(self): print(f"{self.name}: Quack!") Duck('Donald').quack() # 输出:Donald: Quack!
But in the world of functional programming, it is also to realize a “croaking” duck, the way of code to realize the requirements is more tortuous, and the expressive ability is slightly inferior.
Domain Driven Design
In the book “Domain-Driven Design: Coping with the Complexity of Software Core” , the author Eric Evans first proposed the concept of “Ubiquitous Language (unified language)”. “Unified language” refers to a precise language system that is commonly used between developers and users. It is composed of various domain models used in the project, and is usually formulated jointly by domain experts and developers.
The contribution of “universal language” to the second meaning of “realizing requirements” is that it encourages everyone to use the same set of mental models to communicate requirements. With this kind of unity, the final code produced by developers is more likely to be close to the most original user needs.
epilogue
The reason why “implementation requirements” has a double meaning is that code has two different types of consumers: ordinary users and programmers. The former consumes the functions implemented by the code and does not care about the code itself. The latter consumes the readability of the code, so whether the code can effectively self-interpret the requirements is particularly important.
If reading code is to stand in a pond and look for lost things, then may our pond be clear forever. Understanding requirements through code is as natural and simple as observing the pebbles at the bottom of a pond through the water surface.
This article is transferred from: https://www.piglei.com/articles/the-answer-is-in-the-code-fulfill-requirements/
This site is only for collection, and the copyright belongs to the original author.