HelloWorld 洋葱架构教程

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

HelloWorld 洋葱架构教程

先说“为什么”——把复杂拆成可控的圈

洋葱架构(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》

你可以先按上面的最小实现跑一遍,遇到不顺的地方就回头把接口调整为更贴近业务的表达。实现的过程里,你会发现领域模型越简单,外层改动越不痛,这就是洋葱架构想要的效果。反正我是边写边想,希望这篇能让你一看就能动手,先试试最小用例,然后逐步演进。

返回首页