HelloWorld 洋葱架构教程
洋葱架构把系统按领域、应用、接口和基础设施分层,依赖只允许从外层指向内层,使业务模型独立于技术实现。做一个 HelloWorld 示例,核心是把“问好”的规则放在领域层,把输入输出、持久化或展现放在外层,通过接口抽象和依赖注入把实现接到外层,配合单元测试可以保证业务逻辑可测、可替换、可演进。下面一步步用最小可运行示例演示如何落地,并指出常见坑和优化思路,方便你自己动手实践。

Table of Contents
Toggle先说“为什么”——把复杂拆成可控的圈
洋葱架构(Onion Architecture)的核心思想其实很朴素:把业务模型放在最中心,周围按职责套上不同的“洋葱层”。这样做有两个直接好处:一是业务代码不被框架、数据库或 UI 污染,二是替换外部实现(比如从控制台换成 Web)不会改动业务逻辑。想象一下你写的问候语规则,永远不该因为换了数据库就变得复杂。
核心原则(简单说)
- 依赖方向:只能由外层依赖内层,内层不依赖外层。
- 领域优先:领域模型和业务规则居中,独立于技术细节。
- 接口抽象:外层通过接口与内层交互,具体实现位于外层。
HelloWorld 示例总体设计
我们按最小化实现来做:只实现一个“问候”功能。项目分四层(从内到外):Domain(领域)、Application(应用/用例)、Infrastructure(基础设施)、Presentation(呈现层)。每层的职责如下表所示:
| 层 | 职责 |
| Domain | 核心实体、值对象、领域规则(例如 Greeting 规则) |
| Application | 用例/服务协调,暴露接口(例如 IGreeter) |
| Infrastructure | 外部实现:日志、持久化、第三方依赖(例如 Console 或 Http 实现) |
| Presentation | 用户交互:CLI、Web API 或 UI |
一步步搭建:概念到代码(伪代码说明,语言无关)
用费曼法来讲,就是先把每一层讲清楚,然后实现最小可测的接口,再把它们组合起来。下面是实操步骤。
1. 定义领域模型(Domain)
在领域层,你只关心“问好”的规则。不要写任何 IO 代码。示例元素:
- 实体:Greeting(可以只是一个包含 message 的对象)
- 值对象/规则:GreetingFactory 或 GreetingPolicy(根据时间、语言决定内容)
示意(伪代码):
class Greeting {
string Message;
Greeting(string message) { Message = message; }
}
class GreetingPolicy {
string CreateGreeting(string name) {
if (name is empty) return "Hello, world!";
return $"Hello, {name}!";
}
}
2. 定义应用接口(Application)
应用层暴露的接口用于协调领域逻辑和外部调用,不包含具体实现。关键是定义一个抽象的 IGreeter:
interface IGreeter {
Greeting Greet(string name);
}
IGreeter 的实现会在更外层提供,应用层也可包含简单的用例包装(例如输入验证、事务边界)。
3. 基础设施实现(Infrastructure)
这里放具体实现,比如把问候写到控制台、文件或发送网络请求。重要的是实现 IGreeter,然后在启动时注入。
class ConsoleGreeter : IGreeter {
GreetingPolicy policy;
ConsoleGreeter(GreetingPolicy p) { policy = p; }
Greeting Greet(string name) {
var g = new Greeting(policy.CreateGreeting(name));
Console.WriteLine(g.Message);
return g;
}
}
4. 呈现层(Presentation)与组装
呈现层可以很简单:一个控制台程序读取用户名并调用 IGreeter。关键步骤是在启动处做依赖注入(手动或用容器),把实现注入接口。
- 手动组装示例:
var policy = new GreetingPolicy(); var greeter = new ConsoleGreeter(policy); greeter.Greet("Alice"); - 使用容器时,注册策略、实现到容器,解析 IGreeter。
关于测试:为什么洋葱架构测试友好
因为业务逻辑被隔离在内层,你可以对领域规则和用例写纯内存的单元测试,不需要数据库或 UI。举例:
- 测试 GreetingPolicy.CreateGreeting 的各种输入输出。
- 用 Mock 或 Stub 替代 Infrastructure,测试 Application 层如何协调。
单元测试样例思路
- 给空名 -> 返回默认问候。
- 给特定名 -> 返回包含名字的问候。
- 故障注入 -> 模拟基础设施抛异常,确认应用层是否按预期处理。
常见误区与实战建议(很实用,别犯)
- 误区:把数据库操作放领域层 —— 不要。领域层只定义接口和规则,具体持久化属于基础设施。
- 误区:过度抽象 —— 为了“纯洁”不停抽象,结果复杂度上升。先做最简单的接口,再重构。
- 建议:接口按行为而非按技术命名 —— IGreeter、IUserRepository 比 IConsoleWriter 更能表达意图。
- 建议:从业务场景写用例 —— 先写你要实现的用例(Greet),再抽取出接口和实现。
小优化点(实践中常用)
- 把边界(DTO、接口)放在应用层或独立的 Contracts 项目,避免依赖环。
- 把跨cutting concerns(日志、异常处理)通过装饰器或中间件实现。
- 对复杂规则拆分单元,写更多小测试,降低单测脆弱性。
举例项目目录(最小可运行结构)
下面是一个简单的文件夹建议,帮你快速上手:
- MyApp.Domain/
- Greeting.cs
- GreetingPolicy.cs
- MyApp.Application/
- IGreeter.cs
- GreetUseCase.cs
- MyApp.Infrastructure/
- ConsoleGreeter.cs
- FileGreeter.cs(可选)
- MyApp.Presentation/
- Program.cs(或 index.js)
- MyApp.Tests/
- GreetingPolicyTests.cs
- GreeterIntegrationTests.cs
常见问题快速回答(像在跟你边聊边写)
有人会问:“为什么不把接口放在基础设施?” 哎,这个容易混淆,但记住:接口是为了定义边界,放在更靠内或独立的 Contracts 项目通常更稳妥。还有人会说:“这样层次多,做起来慢。” 确实开始会写多一点样板,但长期看可维护性和可更换性省下的代价大得多。
参考读物(如果你想更系统)
- Jeffrey Palermo 的 “Onion Architecture” 博文
- Eric Evans 的《Domain-Driven Design》
你可以先按上面的最小实现跑一遍,遇到不顺的地方就回头把接口调整为更贴近业务的表达。实现的过程里,你会发现领域模型越简单,外层改动越不痛,这就是洋葱架构想要的效果。反正我是边写边想,希望这篇能让你一看就能动手,先试试最小用例,然后逐步演进。