Clean Architecure in Go (1) The Introduction
Preface
当我们最初建立一个Golang项目时,很自然问题是:应该怎么layout比较好:即我们怎么应该组织项目代码,在Golang中有什么组织代码的约定”套路”吗?从这个看似简单的问题,表面上是我们如何安排目录结构,其实背后涉及到另一个有趣的问题:我们应该如何architect一个新项目,因为layout其实是project architecture的反映,architecure变化肯定会让layout产生变化。
本文中,我们先看大型golang项目在layout方面的普遍做法,即大型项目的目录结构。基于此进一步深入思考:这种做法能否得出一个清晰的project architecure:来得到更好的项目架构内在指标:低耦合度,可维护性/扩展性,可测性。 如果不能,怎样做才能更好的解决上述软件架构中的pain points? 本文给出的答案是Uncle Bob提出的Clean Architecure。
最后我们会通过一个简单的case来看我们如何实现Clean Architecture。
关于这个主题,我在team内部也做了一个分享,见这里
Golang “Standard” Layout
一般的大型Golang软件的目录结构如下:
这里的Standard其实并不是指golang官方标准,这里总结的layout来自于: ^1
在这里各主要目录解释如下
/cmd directory
项目中build成的binary的入口main.go应该放在此处,如果有多个binary,建立对应的子目录。
此目录下应该实现的是各binary的main module,不应该包含太多代码。作为daemon运行的service的binary也放在此处,这里cmd有些误导,并不专指binary command。
/internal directory
Go 1.4引入的特性,代码放在internal/下面,不可以被非parent的代码import,也就意味着不能被其他project import,所以一般放置仅限repo内部使用的library。
/pkg directory
与internal/相反,这里存放可以被其他project复用的library code,应该仔细考虑这里的code是否足够成熟以被其他project复用。
见Prometheus例子prometheus/prometheus/pkg
/api directory
用来存放api接口的约定,如swagger和protcolbuffer定义文件,参见OpenShift API
/configs directory
很好理解,存放配置文件或者配置文件模板,以及诸如consul-template,systemd service file(也可以存放在/init目录下)等等。
/build directory
一般有两个子目录:
- build/package directory: cloud (AMI), container (Docker), OS (deb, rpm, pkg) package配置及脚本。
- build/ci: CI(travis, circle, drone)配置及脚本。
miscellaneous directory
- /deployments: system and container orchestration deployment configurations
- /tools: supporting tools
- /scripts: supporting scripts
- /docs: 除了godoc,其他额外的doc放在此处
- /assets: logo, images及其他static files
Thinking in “Standard Layout”
上述的layout给出了golang project的大致划分和命名的建议,但我们可以看出,大部分的代码还是需要放在/internal下面的,因为一般项目并不会成熟到有很多代码会放在/pkg供别的项目引用。
对于一个庞大的service来说,我们对于处在internal/目录下的代码如何组织呢?我们怎么layout才能使项目变得更低耦合,可维护/更改及可测试呢?
显然这个layout除了一些很basic的划分并没有给出上面问题的答案,而这个问题正是layout的最关键问题,同时也引出了: layout背后反映的是怎样一种架构?我们如何更好的架构我们的项目?
Pain points in current golang layout
我们先来总结一下我们日常编程中,遇到的架构/layout的痛点:
- Hard to change, decisions taken too early
- Centered around frameworks/database, business logic is spread everywhere
- Hard to find things needed
- Test: slow, heavy; low test coverage, hard to mock;
- Circular dependency
其实这些问题绝大多数是由错误的dependency,导致项目不同module之间杂乱的couple在一起:
- business logic依赖细节如db实现,造成了不能轻易替换db甚至修改db schema
- 高层逻辑依赖细节的另一个坏处就是不容易进行测试,只能启动db实例才可以。
- 过度依赖技术framework,核心逻辑散落在各处,不易寻找。
所以我们的最初问题转换成了:寻找一个低耦合,依赖关系正确的architecure,来解决我们的痛点。
这个属于软件工程的经典话题,我们已经有了很好的指导原则:SOLID。
SOLID Principle
- Single responsibility principle
- Open–closed principle
- Liskov substitution principle
- Interface segregation principle: 明确细分interface的功能,而不是包含多种功能的 general interface
- Dependency inversion principle
这5个原则中,以下两个和Clean Architecture更为相关:
- Liskov替换原则:软件模块应该抽象出接口,同样接口的object可以替换而不需要调用者修改代码。
- Dependency inversion principle: 高层逻辑不应该依赖于细节实现,两者都应该依赖于抽象,即interface。
Dependency Inversion Principle
原则的官方定义:
- High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces)
- Abstractions should not depend on details. Details (classes) should depend on abstractions
这个耳熟能详的原则是理解Clean Architeture,即我们接下来讲述架构的关键。
我们先看下没有实践Dependency Inversion Principle的一个典型的架构:
从架构我们可以看到:控制流(A call B,控制流为A–>B)与依赖流完全一致,导致高层,抽象的模块依赖于低层模块;如果底层模块改变实现细节,可能会引起中层,高层模块的一系列修改。这种架构使模块之间紧紧的耦合在一起。
我们可以利用Dependency Inversion Principle来invert模块的依赖:使底层模块的实现依赖于高层模块定义的接口:
在上图中我们看到,HL1 package定义了interface,并又ML1 package来实现,使之前HL1依赖于ML1倒置过来了,变成ML1依赖HL1,这就是依赖倒置原则。
这个原则的关键在于:我们可以在我们想的地方invert任何依赖,从而修复诸如高层(更抽象的模块)错误依赖底层细节的现状。
Clean Architecture
讲到这里,郑重引出本文的主角: Clean Architecture(见本文开始的配图)
什么是Clean Architecture呢?是由Uncle Bob老师强力提出的一种软件架构方法,核心是分层架构,约束依赖,博客见这里:[^2]
Clean Architeture结构图的直观感受:
- 分层的结构,每个同心圆代表软件的一层
- 最外是device, UI, db实现等细节层,最内是核心业务逻辑的表现:use case层和entity层
- 越内的层越抽象,是policy;而越外的层越底层,细节,是mechanism;
- 清晰的依赖关系:依赖流只能由外向内,不能相反;细节永远依赖高层,抽象的部分,也即遵循dependency inversion principle
Clean Architecture就先介绍到这里,下一篇会包含:
- 每层划分的具体实践
- 什么是Use Case
- 分析简单例子来了解如何进行Clean Architecture
Stay tuned,真香警告
[^2]: The Clean Architecture