我读了很多博客,经常遇到的主题之一是担忧(至少是 Rails 定义它们的方式)对软件造成损害。总的来说,我同意 - 简单地将行为纳入模型中违反了单一责任原则。你最终会得到一个做得太多的神级。
但与从博客中收集的许多意见一样,很少提供替代架构。
所以让我们举一个示例应用程序,它大致基于我必须维护的一个应用程序。它本质上是一个 CMS,就像许多 Rails 应用程序一样。
目前每个模型都有大量的关注点。让我们在这里使用一些:
class Article < ActiveRecord::Base
include Concerns::Commentable
include Concerns::Flaggable
include Concerns::Publishable
include Concerns::Sluggable
...
end
您可以想象“可评论”只需要在文章中添加少量代码即可。足以与评论对象建立关系并提供一些实用方法来访问它们。
可标记,允许用户标记不适当的内容,最终会向模型添加一些字段:例如flaged、flaged_by、flaged_at
。以及一些添加功能的代码。
Sluggable 添加了一个 slug 字段以供在 URL 中引用。还有更多代码。
Publishable 添加了发布日期和状态字段,以及更多代码。
现在如果我们添加一种新的内容会发生什么?
class Album < ActiveRecord::Base
include Concerns::Flaggable
include Concerns::Sluggable
include Concerns::Publishable
...
end
专辑有点不同。您无法评论它们,但您仍然可以发布它们并标记它们。
然后我添加更多内容类型:例如,事件和个人资料。
我发现这个架构存在一些问题:
- 我们有多个具有完全相同字段的数据库表
(
flaged_by、published_on
等) - 我们无法使用单个 SQL 查询同时检索多种内容类型。
- 每个模型都支持重复的字段名称以及所包含的关注点,从而赋予每个类多重职责。
那么有什么更好的方法呢?
我看到装饰器被推广为一种在需要的地方添加功能的方式。这可以帮助解决包含代码的问题,但数据库结构不一定得到改进。它还看起来不必要地繁琐,并且涉及到向代码添加额外的循环来装饰模型数组。
到目前为止我的想法是这样的:
创建一个通用的“内容”模型(和表格):
class Content < ActiveRecord::Base
end
关联的表可能很小。它可能应该有某种“类型”字段,也许还有一些对所有内容来说都是通用的东西——比如 URL 的类型段。
然后,我们可以为每个行为创建一个关联模型,而不是添加关注点:
class Slug < ActiveRecord::Base
belongs_to :content
...
end
class Flag < ActiveRecord::Base
belongs_to :content
...
end
class Publishing < ActiveRecord::Base
belongs_to :content
...
end
class Album < ActiveRecord::Base
belongs_to :content
...
end
...
其中每一个都与一个内容相关联,因此外键可以存在于功能的模型上。所有与该功能相关的行为也可以单独存在于该功能的模型上,这让 OO 纯粹主义者更加高兴。
为了实现通常需要模型 Hook 的行为(例如 before_create
),我可以看到观察者模式更有用。 (发送“content_created”事件后就会创建一个 slug)
这看起来会把事情清理得无休无止。我现在可以使用单个查询搜索所有内容,数据库中没有重复的字段名称,并且不需要将代码包含到内容模型中。
在我在下一个项目中愉快地使用它之前,有人尝试过这种方法吗?会起作用吗?或者将事情拆分这么多最终会创建大量的 SQL 查询、连接和困惑的代码吗?你能建议一个更好的选择吗?
最佳答案
问题基本上只是 mixin 模式的一个薄包装。对于围绕可重用特征编写软件来说,这是一种非常有用的模式。
单表继承
跨多个模型具有相同列的问题通常可以通过单表继承来解决。然而,STI 仅在模型非常相似时才真正适合。
那么让我们考虑一下您的 CMS 示例。我们有几种不同类型的内容:
Page, NewsArticle, BlogPost, Gallery
具有几乎相同的数据库字段:
id
title
content
timestamps
published_at
published_by
# ...
因此,我们决定消除重复并使用公用表。人们很容易将其称为内容
,但这非常含糊 - 内容的内容......
所以让我们复制 Drupal 并调用我们的通用类型 Node
。
class Node < ActiveRecord::Base
include Concerns::Publishable
end
但是我们希望每种类型的内容都有不同的逻辑。所以我们为每种类型创建子类:
class Node < ActiveRecord::Base
self.inheritance_column = :type
include Concerns::Publishable
end
class NewsArticle < Node
has_and_belongs_to_many :agencies
end
class Gallery < Node
has_and_belongs_to_many :photos
end
# ...
这很有效,直到 STI 模型开始彼此之间出现太大差异。那么,数据库模式中的一些重复可能比试图将所有内容硬塞到同一个表中所造成的巨大复杂性要小得多。大多数基于关系数据库构建的 CMS 系统都面临着这个问题。一种解决方案是使用无模式非关系数据库。
撰写关注点
其中没有任何内容表明关注点需要您存储在模型表中。让我们看看您列出的几个问题:
Flaggable
Sluggable
Commentable
其中每一个都将使用表flags
、slugs
、comments
。关键是使与它们标记、slug 或注释的对象的关系具有多态性。
comment:
commented_type: string
commented_id: int
slugs:
slugged_type: string
slugged_id: int
flags:
flagged_type: string
flagged_id: int
# ...
class Comment
belongs_to: :commented, polymorphic: true
end
module Concerns::Commentable
# ...
has_many: :comments
end
我建议您查看一些解决此类常见任务的库,例如 FriendlyId , ActsAsTaggedOn等等来看看它们是如何构造的。
结论
并行继承的想法并没有什么根本性的错误。并且仅仅为了安抚某种极端的面向对象纯粹性理想而放弃它的想法是荒谬的。
特征是面向对象的一部分,就像任何其他组合技术一样。然而,担忧并不是许多博客文章让您相信的那样能解决所有问题的神奇方法。
关于ruby-on-rails - 在设计 Rails 数据库模式时如何避免使用关注点?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30956825/