导语:一个关于易经的短视频,催生了一个起卦小应用——看似轻量的需求,背后却是一套完整的易经起卦应用架构命题:六十四卦如何建模、随机熵源如何取舍、起卦结果如何保证可复现、渲染流水线如何解耦。本文站在资深架构师的第一人称视角,把这套“小而美”的应用拆到骨头里。
先说结论:这类应用的技术难点从来不在“画六个爻”,而在于把一套古老的符号体系翻译成可验证的软件模型。我在评审过大量“文化类小工具”之后发现,90% 的实现都栽在三个地方——卦象数据结构随手写成字符串、随机数直接调 Math.random() 导致结果不可复现、渲染逻辑和业务逻辑糊在一个组件里。下面我按架构分层逐层拆解。
企业级架构规范
flowchart TD
subgraph 接入层 ["表现层与交互接入 (Presentation Tier)"]
A["用户交互: 摇卦/时间/数字起卦"] --> B["输入校验与参数归一化"]
end
subgraph 规则层 ["起卦策略与上下文层 (Policy & Context Tier)"]
B --> C["起卦策略路由 (Method Router)"]
C --> D["熵源适配器 (Entropy Adapter)"]
end
subgraph 领域层 ["核心卦象领域引擎 (Hexagram Domain Tier)"]
D --> E["爻位生成与变爻推导"]
E --> F["六十四卦查表与卦名解析"]
F --> G["卦辞爻辞与五行属性装配"]
end
subgraph 存储层 ["数据与持久化 (Data Tier)"]
G --> H[("卦象静态知识库 (JSON/SQLite)")]
G --> I[("历史记录本地存储 (IndexedDB)")]
end
subgraph 渲染层 ["视图渲染 (Render Tier)"]
G --> J["卦象 SVG/Canvas 渲染器"]
J --> K["结果卡片与解读面板"]
end
一、为什么“起卦”值得认真做一次领域建模
很多开发者第一反应是:这不就是个随机数加查表吗?我见过最典型的反模式,是把整个卦象塞进一个字符串,比如 "111010",然后用 substring 去取爻。这种写法在需求变更时会立刻崩盘——加一个“变卦”功能,你得重写一半逻辑;想支持“错卦”“综卦”,又得再写一套。
正确的做法是把易经的符号体系映射成值对象(Value Object)。爻(Yao)是最小单元,只有阴阳与动静两个维度;卦(Hexagram)由六个爻按序组成;变卦(Changed Hexagram)由动爻翻转推导而来。这三层一旦建模清晰,后续所有功能都是组合而非重写。
1.1 爻与卦的值对象设计
我推荐用不可变数据结构表达爻,阴阳用布尔或枚举,动静用独立标志位。注意:“老阳/老阴”才是动爻,这是新手最容易搞错的地方——很多人以为变卦是随机再摇一次,实际上变卦完全由本卦的动爻决定,是确定性推导。
// 爻:不可变值对象
const YIN = 0, YANG = 1;
function createYao(value, isMoving = false) {
if (value !== YIN && value !== YANG) throw new Error('非法爻值');
return Object.freeze({ value, isMoving });
}
// 卦:六爻自下而上(初爻到上爻)
function createHexagram(yaos) {
if (!Array.isArray(yaos) || yaos.length !== 6) {
throw new Error('卦必须由六个爻组成');
}
return Object.freeze({ yaos: Object.freeze([...yaos]) });
}
// 变卦:动爻翻转,纯函数、可测试
function deriveChangedHexagram(hexagram) {
const flipped = hexagram.yaos.map(y =>
y.isMoving ? createYao(y.value === YANG ? YIN : YANG, false) : y
);
return createHexagram(flipped);
}
把“变卦推导”实现为纯函数,是整个应用可测试性的基石。它不依赖随机数、不依赖时间、不依赖 IO,输入输出完全确定,单测可以覆盖全部 64×64 组合。
二、六十四卦数据建模:静态知识库的选型与加载策略
六十四卦的卦名、卦辞、爻辞、上下卦组合、二进制编码,是一份完全静态、体量极小、查询高频的数据集。这类数据的架构决策其实很清晰:不要上数据库服务,不要走网络请求,直接内置为本地知识库。
2.1 编码方案:用二进制索引替代字符串匹配
六爻天然就是六位二进制。约定自下而上、阳为 1、阴为 0,则任意卦象可映射为 0–63 的整数索引。这个设计带来两个直接收益:查表 O(1),且卦象比较退化为整数比较。我在多个项目里验证过,这比用卦名字符串做 key 稳定得多——毕竟“乾为天”和“乾”在不同资料里写法并不统一。
| 评估维度 | 字符串卦名匹配 | 二进制整数索引 | 架构收益 |
|---|---|---|---|
| 查询复杂度 | O(n) 遍历或哈希 | O(1) 数组下标 | 性能稳定 |
| 数据一致性 | 易受别名干扰 | 编码唯一确定 | 消除歧义 |
| 变卦推导 | 需反查映射 | 位运算直接翻转 | 逻辑极简 |
| 存储体积 | 较大 | 仅需 64 项数组 | 可全量内置 |
// 六爻 -> 0..63 编码(自下而上,阳=1)
function encodeHexagram(hexagram) {
return hexagram.yaos.reduce(
(acc, yao, idx) => acc | (yao.value << idx), 0
);
}
// 静态知识库:64 项,索引即编码
const HEXAGRAM_TABLE = [
{ name: '坤为地', upper: '坤', lower: '坤', judgment: '元亨,利牝马之贞……' },
{ name: '地雷复', upper: '坤', lower: '震', judgment: '亨。出入无疾……' },
// ……其余 62 项
];
function lookupHexagram(hexagram) {
return HEXAGRAM_TABLE[encodeHexagram(hexagram)];
}
爻序方向是这类应用最高频的 bug 来源。传统排卦是“自下而上”,而数组遍历默认“自上而下”。务必在领域层统一约定并在文档中显式声明,否则卦象会整体上下颠倒,且测试用例很难发现。
三、起卦引擎:熵源设计与结果可复现性
起卦的本质是引入随机性,但工程上必须回答一个问题:用户摇出这一卦之后,能不能复现?如果答案是不能,那这个应用就失去了“记录与回看”的价值,也无法做任何自动化测试。这是易经起卦应用架构里最容易被忽视、却最影响工程质量的一环。
3.1 策略模式隔离多种起卦法
传统方法有铜钱法(三枚铜钱掷六次)、蓍草法(大衍之数五十)、时间起卦法、数字起卦法。它们的共同点是“输入若干随机或半随机量,输出六个爻”。差异全部收敛在一个策略接口后面,领域引擎完全无感知。
// 统一策略接口:输入熵源,输出六爻
const strategies = {
// 铜钱法:每次三枚,正面记 3、反面记 2,求和得 6/7/8/9
coin(entropy) {
const yaos = [];
for (let i = 0; i acc + (entropy.next() ? 3 : 2), 0
);
// 6 老阴(动) 7 少阳 8 少阴 9 老阳(动)
const map = { 6: [YIN, true], 7: [YANG, false], 8: [YIN, false], 9: [YANG, true] };
const [value, isMoving] = map[sum];
yaos.push(createYao(value, isMoving));
}
return createHexagram(yaos);
},
// 时间起卦:以农历年月日时推导,天然可复现
time(entropy, ctx) {
const seed = ctx.year + ctx.month + ctx.day + ctx.hour;
return strategies.coin(createSeededEntropy(seed));
}
};
3.2 可复现熵源:种子化随机
我的做法是抽象一个 Entropy 接口,生产环境用 crypto.getRandomValues 保证真随机,同时把种子持久化到历史记录里;回放和测试时用种子化 PRNG(如 mulberry32)重建完全相同的序列。这样“随机”与“可复现”就不再矛盾。
// 种子化 PRNG:同一 seed 永远产出同一序列
function createSeededEntropy(seed) {
let s = seed >>> 0;
return {
next() {
s |= 0; s = (s + 0x6D2B79F5) | 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296 > 0.5;
}
};
}
// 生产环境:真随机 + 记录种子
function createCryptoEntropy() {
const buf = new Uint8Array(1);
return {
next() {
crypto.getRandomValues(buf);
return buf[0] > 127;
}
};
}
切勿在起卦逻辑中直接调用 Math.random()。它既不可注入、不可回放,也无法在测试中固定输出,会导致整个领域层无法做确定性单测。这是我在代码评审中一票否决的红线。
四、渲染流水线与离线优先存储
卦象的视觉表达是六条横线,阴爻中间断开。这个图形用 SVG 渲染最合适——矢量、可缩放、可做动效,且能直接映射到领域模型。关键在于渲染器只消费领域对象,不参与任何业务判断。
4.1 领域对象到 SVG 的纯映射
渲染函数接收一个 Hexagram,输出一组 <rect>。动爻加标记、变卦并列展示,全部由上层组合决定。这样渲染层可以独立做视觉回归测试,领域层可以独立做逻辑单测,两层互不污染。
const YAO_HEIGHT = 8, GAP = 10, WIDTH = 120;
function renderHexagram(hexagram) {
// 自下而上绘制,SVG 坐标系需翻转
return hexagram.yaos.map((yao, idx) => {
const y = (5 - idx) * (YAO_HEIGHT + GAP);
if (yao.value === YANG) {
return ``;
}
const half = (WIDTH - GAP) / 2;
return `` +
``;
}).join('');
}
4.2 历史记录:IndexedDB 而非 localStorage
起卦记录会持续累积,且每条包含卦象、时间、种子、解读文本,localStorage 的 5MB 上限和同步阻塞特性都不合适。IndexedDB 支持结构化存储与索引查询,配合 Service Worker 可以做到完全离线可用——这对一个“随手一摇”的工具类应用是体验分水岭。
点击展开:IndexedDB 历史记录存储与查询示例
const DB_NAME = 'iching';
const STORE = 'records';
function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = () => {
const db = req.result;
const store = db.createObjectStore(STORE, { keyPath: 'id', autoIncrement: true });
store.createIndex('byTime', 'createdAt');
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function saveRecord(record) {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, 'readwrite');
tx.objectStore(STORE).add({ ...record, createdAt: Date.now() });
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
五、工程落地清单与架构复盘
回到最初那个问题:一个起卦应用到底值不值得认真架构?我的判断是——越是小应用,越能暴露架构功力的真实水位。因为小应用没有复杂度可以藏拙,每一处耦合都会在需求变更时立刻现形。这套易经起卦应用架构的核心思想可以浓缩为几条可复用的原则:
- 符号体系值对象化:爻、卦、变卦全部不可变,业务规则以纯函数表达。
- 静态知识库内置化:64 项数据全量打包,二进制索引查表,零网络依赖。
- 随机性可注入:熵源抽象为接口,生产真随机、回放种子化,测试完全确定。
- 渲染与领域解耦:渲染器只消费领域对象,视觉回归与逻辑单测互不干扰。
- 离线优先:IndexedDB + Service Worker,保证弱网与断网场景可用。
如果后续要扩展“错卦/综卦/互卦”等衍生卦象,只需在领域层新增纯函数,渲染层与存储层零改动。这正是把易经起卦应用架构做对之后,最直接的长期收益。
最后提醒一句:传统文化类应用容易陷入“玄学即正确”的思维惰性,但工程上它和任何业务系统没有本质区别——数据要建模、算法要可测、边界要清晰。把这套易经起卦应用架构拆明白,你收获的不只是一个起卦工具,而是一套面对任何“小而怪”需求时的拆解方法论。
本文技术背景源自 V2EX 社区开发者分享的起卦应用实践线索,结合领域驱动设计与前端工程化权威知识体系进行架构级重构与深度剖析:
V2EX 原帖:刷到了一个关于易经的短视频,做了一个起卦的应用
