重构 DDD 4 Rails Developers.Part 2 : Entities and Values

zgm · 2014年01月26日 · 最后由 xiaoronglv 回复于 2014年01月27日 · 8280 次阅读

在我前面关于 DDD for Rails Developers 的文章中提到使用分层架构来解决域的复杂度.我展示了一些典型的不符合分层架构的例子并给出了一些改正的建议.

DDD 的构建模块

这回我要开始聊一聊 DDD 的构建模块以及他们如何被用于建模当中.

实体与值

在 DDD 中, 实体和值对象的区别很大.

  • “一个实体是由一个持续性和一致性的线程而不是其属性定义的对象”. 例如银行住户是一个实体. 系统可以共存多个银行账户. 它们中的一部分可以被分配个同一个支行或者拥有同一个主人, 只要他们有不同的身份系统就必须将他们看做不同的账户. 在 Rails 应用中, 实体的身份通常用一个自动生成的主键来表示.

  • “值对象是一个描述了一些特性或者属性但是没有身份概念的对象” 由于没有身份的概念, 若两个值对象的所有属性相同, 那它们就是相同的. 货币就是一个值对象的例子.

深入实体

Rails 社区对于什么是实体有着很好的理解. 几乎所以扩展了 ActiveRecord::Base 的对象, 都是一个实体.

实体有如下特性:
  • 身份对于实体来说很重要. 它通常是用一个自动生成的主键来表示, 可用于比较两个实体.

  • 它们是可变的并且通常只有主键是不能被改变的.

  • 它们很长寿, 大多数实体会一直呆在数据库里.

  • 由于实体的多变并且长寿, 它们有一个复杂的生命周期:

    1. 一个实体对象被创建.

    2. 它被保存到数据库中.

    3. 它被从数据库中读取出来.

    4. 它被更新.

    5. 它被删除 (或者被标记为删除).

由于实体的多变和复杂的生命周期, 它的处理也变得复杂起来. 因此, 你每次创建一个实体, 思考一下你将会如何持久化它, 哪些属性将会被频繁更新, 会被哪个聚合包含?(更多关于聚合的精彩内容, 见下篇文章).

深入值对象

Rails 社区对之对象的理解并不充分, 因此大多数 Rails 应用都会有 “原始类型困扰”.

如整形或字符串等原始类型的数据用于表示域中重要的概念.

首先, 处理一组属性的逻辑会被分散到很多类中. 原始类型困扰是重复代码的源头. 其二, 使用原始数据代替特定于域的高度抽象的服务将会使代码变得不清晰. 值对象为原始类型困扰提供了一个很好的补救.

值对象有如下特性
  • 值对象没有身份.

  • 他们是不可变得. 对于对象来说, 3 增加到 5 不会改变他们的值而是被一个新的对象代替. 理想状态下, 使用值对象就如同使用原始类型一样.

  • 值对象没有复杂的生命周期.

在 Rails 中创建值对象

在 Rails 中有很多种方式可以创建和管理值对象, 我将会列举三条.

使用 composed_of

想象着, 我们正在开发一个 blog 应用. 我们决定使用如下模型:

  • blog 有多个 posts

  • 每个 post 有多个 comments

  • post 和 comment 都拥有 location 字段

我们可以这样创建 posts 和 comments:

blog = Blog.create
post = blog.make_post text: 'great post', location_country: 'Canada', location_city: 'Toronto'
post.make_comment text: 'great comment', location_country: 'Canada', location_city: 'Toronto'

我们还可以通过 location 来搜索他们:

class Blog < ActiveRecord::Base
  ...

  def all_posts_from country, city
    ...
  end

  def all_comments_from country, city
    ...
  end
end

除此之外, 需要一个 presenter 用来显示 location 字段:

class LocationPresenter
  def initialize country, city
    ...
  end
end

现在我们需要将 location_contry 和 location_city 包到 location 对象当中.

class Post < ActiveRecord::Base
  composed_of :location, mapping: [%w(location_country country),
                                   %w(location_city city)]

  def self.all_posts_from location
    Post.where location: location
  end
end

我们重构的结果如下:

blog.make_post text: 'great post 2', location: Location.new('Canada', 'Toronto')

重构后获得的最大收获是对包含在原码中的域有了一个重要的概念, 同时, 提取出值对象帮助我们提高抽象水平. 使代码更具可读性.

def Toronto
  Location.new('Canada', 'Toronto')
end

...

blog.make_post text: 'great post', location: Toronto
ActiveRecord::Base 拓展的值对象

有些人说从 ActiveRecord::Base 拓展的任何对象都是一个实体. 我不同意这个观点. 我认为, 无论你如何实现你的值对象, 只要他们没有状态和身份就好了.

我们定义一个 location 类:

class Location < ActiveRecord::Base
  validates :city, :uniqueness => {:scope => :country}

  def self.get country, city
    location = Location.find_by_country_and_city(country, city)
    raise "There is no '#{city}' in '#{country}'" unless location
    location.readonly!
    location
  end

  ...
end

使用起来与上面的 location 差不多:

toronto = Location.get('Canada', 'Toronto')
blog.make_post text: 'great post 2', location: toronto

像这样动态地管理一个任意位置列表, 或者为每个对象增加一些额外信息 (例如, 一个连接到 wikipedia 的文章) 会让你决定支持这种方式.

原始的 Ruby 对象

那些通过 Rails 学习 Ruby 的开发人员更乐于使用 Rails 的组件来解决问题 —— 持久话个对象? 只有 ActiveRecord. 需要一个值对象? 使用 compose_of. 没了 Rails 就感觉浑身不自在, 例如所有不是从 ActiveRecord::Base 扩展来的模型都会被他们塞到 lib 目录之下. 即使是一个小应用, 创建一个复杂模型也需要使用各种 Factories, Services, Value Objects, 等等. 因此, 不要惧怕离开 Rails 实现和使用值对象.

总结

总而言之, 实体和值对象及其重要. 他们是对象模型的核心要素. 软件工程师们一定要深入理解他们之间的不同之处.

匿名 #1 2014年01月26日

握爪. 第三篇也交给前辈您了. 翻译的太专业了.

匿名 #2 2014年01月26日

顺便说一句, 前辈您的个人网站的域名已经到期了. 记得续费.

#1 楼 @Turristan 你看看,人家明显翻译得比舒服,包括排版;-)

匿名 #4 2014年01月26日

#3 楼 @Weston 对初学者不友好。果然有代沟啊。

翻译的不错。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号