简单项目设计
前面的章节已经分别说明 struct、enum class 和类。把它们放进小项目时,重点不是对象数量,而是每个对象是否有清楚、可验证的职责。完整示例在 simple-project-design.cpp。
对象守住状态
棋盘的格子状态和不可覆盖落子的规则一起变化,所以放进 Board。调用方只能通过 Place 改变状态,不能直接改数组。
simple-project-design.cpp
// 一个 3×3 棋盘。
// 负责保存格子状态,并拒绝无效或重复的落子。
class Board {
public:
// 尝试在指定坐标落子。
// 坐标无效、状态为空或格子已占用时返回 false。
[[nodiscard]] bool Place(CellPosition position, CellState state) {
}
};这不是因为所有数据都必须藏进类,而是因为格子状态和落子规则必须始终一致。Place 的返回值把“是否接受这一步”交给调用方处理。
值类型传数据
格子状态是固定集合,用 enum class 表达。一个位置只包含行和列,没有独立的不变量,用 CellPosition 这样的 struct 按值传递。
simple-project-design.cpp
// 棋盘格子的状态。
enum class CellState : std::uint8_t {
kEmpty = 0,
kCross = 1,
kNought = 2,
};
// 棋盘格子的行列坐标,行和列从 0 开始。
struct CellPosition {
int row;
int col;
};类型名称让调用处知道一组数据代表什么,类则负责那些必须保持成立的规则。不要只因两个值经常一起出现,就马上增加 setter、继承层或单独的管理器。
不拆单次模块
示例中的 ToIndex 只把已经验证的坐标变成 Board 的内部下标。它没有第二个调用方,也不表达独立业务规则,因此留在 Board 的 private 区域。
simple-project-design.cpp
private:
// 将已验证的坐标转换为行优先下标。
[[nodiscard]] static constexpr std::size_t ToIndex(CellPosition position) {
return static_cast<std::size_t>((position.row * kCols) + position.col);
}把它拆成 Indexer、Validator 或通用工具文件,会让读者必须在更多文件之间跳转,却没有增加复用。出现第二处真实、稳定且语义相同的需求后,再考虑提取公共接口。
外层输入输出
Board 不打印棋盘,也不读取终端。外层程序决定如何显示 CellState,再调用 Place,同一份规则就能被终端、测试或其他适配方式复用。
常见错误
每个动作一个类
ToIndex 只属于 Board 的存储方式,做成独立类只会增加名称、文件和依赖。
- 先把只服务一个对象的辅助逻辑留在该对象内部。
核心对象读终端
Board 一旦直接读写终端,测试规则时就必须准备输入输出环境。
- 让核心对象返回结果。由外层决定怎样读取和显示。
自测问题
为什么
CellPosition适合用struct?查看答案
它只组合行和列,按值传递即可表达一个位置。它不需要自行管理资源或维护复杂规则。
为什么
ToIndex不需要独立文件?查看答案
它只服务
Board的内部数组布局,没有第二个真实调用方。保留在 private 区域可以缩小外部接口。为什么
Board不直接打印棋盘?查看答案
打印属于输入输出适配。把它留在外层,规则可以直接由测试或不同界面复用。
本章回顾
你已经能用值类型传递数据、用类守住不变量、外层输入输出,并在真实重复出现时才增加抽象。运行 simple-project-design.cpp 可以看到一次中心格落子。