这篇是公司里的怪叔叔殴打小朋友的产物,自此翻译,简而言之,接下来的两篇我是没打算翻译的。
为什么不支持二级标题?
(题外话:DDT 污染影响延续至今,请诸位关注生态.) DDD, 全称 Domain-Driven Design. 可以毫无想象力直译为领域驱动设计 ( 很装 X 的说法).
开发软件时,总是要处理层出不穷的复杂度的问题。所谓家家都有难念的经,家境一般的女孩子想嫁豪门,豪门家的女孩子确是 Lesbian. 每个应用都都有各自的奇葩到匪夷所思的问题。比如说,假如要打造次时代的 Twitter, 五毛人士自行脑补 Weibo, 可拓展性及容错机制就是你要推倒的两座大山。但是开发企业应用的人士对这两座高峰从来都是"呵呵"一笑而过的,让他们头疼的是因琐碎而复杂,因复杂而更让人头疼的业务流程。许多公司的业务流程复杂到令人发指。水涨船高命中注定般的,梳理问题域也需要极其丰富的经验和知识,才能使应用低耦合化,最终提供灵活易维护的服务,
Eric Evans, 《领域驱动设计》的作者,开创了许多应对处理领域复杂度的术语及经验做法,《领域驱动设计》一书也是从事企业应用开发的必读书。我 (Victor Savkin, 一下不再赘述) 特别推荐此书。
在从事开发诸多大型 Rails 应用的过程中,我 (很中二地) 察觉到 DDD 和 Rails 之间有许多互为悖逆之处。因此我决定替天行道,撰写天书三篇,以警世人。切记要取中庸之道,调和二者,而非取一舍它。
在此,我很想介绍一下将 DDD 引入实际应用的经验,"可惜这里空白的地方太小,写不下". 作为秘鲁上升补偿流,我提供一锦囊秒计,Check it Out.
人生何处不相逢啊。人生处处是妥协。本天书也不例外。中二啊说错了,中庸才是王道。
DDD 的一个核心概念就是分层架构。简单介绍一下它的益处以及典型的 Rails 应用与之相违之处。
顾名思义,分层架构意味着你需要将应用架构分层:
分层架构的刺客信条是 每层只能依赖于下面的层次。依赖方向只是统一向下的。举例来说,领域层可以依赖于基础层,但却不能依赖于应用层。
典型 Rails 应用中也有诸多违反分层架构的信条的地方。譬如:
as_json
方法时,你都违反了刺客信条:你违逆了信条规定的依赖方向。领域层受制于展现层。(注:为了展现的特定需要而改写领域层.)update_attributes
方法保存 (中二的说法:持久化). 下次你在 Controller 里想调用update_attributes
时,为了世界的和平与安宁,请驻足三思。幸好,许多 Rails 开发者已经意识到了这将使 Controller 变得难以维护。我坚信,即使是小小的玩具应用也没有借口继续这种丧心病狂的行为。extend
)ActiveRecord 实现了 model 类的话,恭喜你你成功的将领域层和基础层耦合在一起。该如何测试?(好在 ActiveRecord 值得信赖.)以上问题将由我替天行道,一一解决。
不要在 Model 中重写as_json
及同类方法。Model 的职责只是描述业务概念。老夫纵横江湖多年,从未在业务逻辑里看到 JSON 这一字眼。所以请将 JSON 之类的的方法移出 Model.
譬如,有这样一个 action
def update
p = Person.find(params[:id])
if p.update_bank_information params[:bank_information]
render json: p.as_json
else
render json: "error"
end
end
如果你想自定义 JSON 序列结果,请不要在 Person 类里实现,请另建一个 module, 单一职责法则。
module PersonJsonSerializer
def self.as_json person
if person.errors.present?
person.as_json(root: "root-attrs")
else
{errors: person.errors.full_messages}
end
end
end
更改 controller.
def update
p = Person.find(params[:id])
p.update_bank_information params[:bank_information]
render json: PersonJsonSerializer.as_json(p)
end
这样做的优点是:
简而言之,Controller 的职责是处理用户输入数据和 (render
) 呈现正确的视图 (view
). 其余的业务逻辑代码请移至领域层。但这可能写成万能 Model, 两难之下,坚决选择有中国社会主义特色的中庸道路。把它丢到一个 Service 类里面吧。
比如,有以下的一个 Action.
def sell_book
@book = Book.find(params[:id])
if book.sold?
bokk.errors.add :base, "Already sold"
else
book.sell
end
end
更好的方法是:
def sell_book
@book = Book.find(params[:id])
BookSellingService.sell_book(@book)
end
优点是:
一句话,Model 类内部禁止直接调用外部服务。举个博客引擎的例子,具体需求是发表一篇文章时自动同步到 Tweet. 最直接的方法是用 (after_create
) 钩子函数。不得不吐槽一句,钩子函数这直译的叫人心碎。
class Post < ActiveRecord::Base
has_many :comments
after_create :send_tweet
def send_tweet
twitter = Twitter.login(username, password)
twitter.send_tweet generate_tweet_from_subject(subject)
end
......
end
尽管这用几行代码,但却相关的缺点却很多。
解耦合的方法有很多,一种方法是使用观察者模式。
class Post < ActiveRecord::Base
has_many :comments
end
class TwitterNotification < ActiveRecord::Observer
observe :post
def after_create post
twitter = Twitter.login(username, password)
twitter.send_tweet generate_tweet_from_subject(post.subject)
end
end
依据单一职责信条,还可进一步的改为:
class TwitterNotification < ActiveRecord::Observer
observe :post
def after_create post
TwitterService.send_tweet post.subject
end
end
class TwitterService
def self.send_tweet subject
twitter = Twitter.login(username, password)
twitter.send_tweet generate_tweet_from_subject(subject)
end
...
end
优点是:
谦虚点讲,这即使是对于我,这也是个不可能的任务。人们早已经习惯了 Model 与 ActiveRecord 的耦合。但是,Model 本应该只是数据库模式的抽象,而不影响我们设计实体 (译者:ActiveRecord 提供了许多方便的动态方法,从而潜移默化的使开发者为了能使用这些方法而去设计应用).
至少在没有数据库的情况下可以测试你的 Model.
伟大的 DDD 教育我们要将分离持久化模块 (这么中二的词,无力吐槽).
class PostsRepository
def find_by_id id
...
end
def new_posts_of_author author
...
end
def save post
...
end
end
这样分离可以简化分离,并且可以灵活切换不同的接口实现。简单的来说可以。普遍的做法是提供 (SQLPostsRepository
) 和 (InMemoryPostsRepositoty
). 前者可以用作集成测试,后者用作单元测试。但这是个真实的世界,你怎么可能不用 ActiveRecord. 天知道,ActiveRecord 是伟大。我只能无奈走具有中国特色社会主义的中庸大道了。
只能妥协。
module PostsRepository
def new_posts_of_author author
...
end
end
class Post < ActiveRecord::Base
extend PostsRepository
end
Post.new_posts_of_author "Jim"
优点:
mock&&stub
)Post.extend InMemoryPostsRepository
)处理领域复杂度是个艰难的问题。复杂度随应用规模增长而增长。在玩具应用中或许可以将代码堆成一座 Shit Mountain, 但在规模较大的应用中,估计会崩溃。假如遇到代码失控的情况,试试 DDD 分层架构吧。
但是,分层架构只是 DDD 的冰山一角而已。实体&&值,服务&&工厂,聚合根,领域边界,发腐层等等。理解并应用这些概念和信条对 Rails 开发者很有帮助。我会围绕这个主题继续写两篇博客。