使用普通的旧 Ruby 对象构建时尚的 Rails 组件

已发表: 2022-03-11

您的网站正在获得牵引力,并且您正在迅速发展。 Ruby/Rails 是您选择的编程语言。 你的团队更大了,你已经放弃了“胖模型,瘦控制器”作为 Rails 应用程序的设计风格。 但是,您仍然不想放弃使用 Rails。

没问题。 今天,我们将讨论如何使用 OOP 的最佳实践来使您的代码更清晰、更孤立、更解耦。

你的应用值得重构吗?

让我们首先看看您应该如何确定您的应用程序是否适合重构。

这是我通常会问自己以确定我的代码是否需要重构的指标和问题列表。

  • 缓慢的单元测试。 PORO 单元测试通常使用隔离良好的代码快速运行,因此运行缓慢的测试通常表明设计不佳和责任过度耦合。
  • FAT 模型或控制器。 具有超过 200 行代码 (LOC) 的模型或控制器通常是重构的良好候选者。
  • 过大的代码库。 如果您有超过 30,000 个 LOC 的 ERB/H​​TML/HAML 或超过 50,000 个 LOC 的 Ruby 源代码(没有 GEM),那么您很有可能应该重构。

尝试使用这样的东西来找出你有多少行 Ruby 源代码:

find app -iname "*.rb" -type f -exec cat {} \;| wc -l

此命令将搜索 /app 文件夹中所有扩展名为 .rb 的文件(ruby 文件),并打印出行数。 请注意,这个数字只是近似值,因为注释行将包含在这些总数中。

另一个更精确、信息更丰富的选项是使用 Rails rake 任务stats ,它输出代码行数、类数、方法数、方法与类的比率以及每个方法的代码行数比率的快速摘要:

 bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
  • 我可以在我的代码库中提取循环模式吗?

脱钩在行动

让我们从一个真实的例子开始。

假设我们要编写一个跟踪慢跑者时间的应用程序。 在主页上,用户可以看到他们输入的时间。

每个时间条目都有日期、距离、持续时间和附加的相关“状态”信息(例如天气、地形类型等),以及可以在需要时计算的平均速度。

我们需要一个显示每周平均速度和距离的报告页面。

如果条目的平均速度高于整体平均速度,我们将通过 SMS 通知用户(在本示例中,我们将使用 Nexmo RESTful API 发送 SMS)。

主页将允许您选择慢跑的距离、日期和时间,以创建类似于以下内容的条目:

我们还有一个statistics页面,它基本上是一份每周报告,其中包括每周的平均速度和距离。

  • 您可以在此处查看在线示例。

代码

app目录的结构类似于:

 ⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

我不会讨论User模型,因为它没有什么特别之处,因为我们将它与 Devise 一起使用来实现身份验证。

至于Entry模型,它包含我们应用程序的业务逻辑。

每个Entry都属于一个User

我们验证每个条目的distancetime_perioddate_timestatus属性的存在。

每次创建条目时,我们将用户的平均速度与系统中所有其他用户的平均速度进行比较,并使用 Nexmo 通过短信通知用户(我们不会讨论如何使用 Nexmo 库,虽然我想演示一个我们使用外部库的案例)。

  • 要点样本

请注意, Entry模型不仅仅包含业务逻辑。 它还处理一些验证和回调。

entries_controller.rb具有主要的 CRUD 操作(虽然没有更新)。 EntriesController#index获取当前用户的条目并按创建日期对记录进行排序,而EntriesController#create创建一个新条目。 无需讨论EntriesController#destroy的明显职责和职责:

  • 要点样本

statistics_controller.rb负责计算每周报告, StatisticsController#index获取登录用户的条目并按周对它们进行分组,使用 Rails 中 Enumerable 类中包含的#group_by方法。 然后它尝试使用一些私有方法来装饰结果。

  • 要点样本

我们在这里不讨论太多视图,因为源代码是不言自明的。

下面是列出登录用户 ( index.html.erb ) 条目的视图。 这是将用于在条目控制器中显示索引操作(方法)结果的模板:

  • 要点样本

请注意,我们正在使用部分render @entries ,将共享代码提取到部分模板_entry.html.erb ,以便我们可以保持代码干燥和可重用:

  • 要点样本

_form部分也是如此。 我们创建一个可重用的部分表单,而不是使用(新建和编辑)操作相同的代码:

  • 要点样本

至于周报页面视图, statistics/index.html.erb显示了一些统计数据,并通过对一些条目进行分组来报告用户的周表现:

  • 要点样本

最后,条目的帮助器entries_helper.rb包括两个帮助器readable_time_periodreadable_speed ,它们应该使属性更易于阅读:

  • 要点样本

到目前为止没有什么花哨的。

你们中的大多数人会争辩说,重构这违反了 KISS 原则,并且会使系统更加复杂。

那么这个应用程序真的需要重构吗?

绝对不是,但我们将其仅用于演示目的。

毕竟,如果您查看上一节,并且表明应用程序需要重构的特征,很明显我们示例中的应用程序不是重构的有效候选者。

生命周期

因此,让我们从解释 Rails MVC 模式结构开始。

通常,它从浏览器发出请求开始,例如https://www.toptal.com/jogging/show/1

Web 服务器接收请求并使用routes来找出要使用的controller

控制器完成解析用户请求、数据提交、cookies、会话等工作,然后要求model获取数据。

这些models是 Ruby 类,它们与数据库对话、存储和验证数据、执行业务逻辑以及完成繁重的工作。 视图是用户看到的:HTML、CSS、XML、Javascript、JSON。

如果我们想显示 Rails 请求生命周期的顺序,它看起来像这样:

Rails 解耦 MVC 生命周期

我想要实现的是使用普通的旧 ruby​​ 对象 (PORO) 添加更多抽象,并使create/update操作的模式类似于以下内容:

Rails 图创建表单

对于list/show操作,如下所示:

Rails 图表列表查询

通过添加 PORO 抽象,我们将确保 SRP 职责之间的完全分离,这是 Rails 不太擅长的。

指导方针

为了实现新设计,我将使用下面列出的指南,但请注意,这些不是您必须遵循的 T 规则。将它们视为使重构更容易的灵活指南。

  • ActiveRecord 模型可以包含关联和常量,但仅此而已。 所以这意味着没有回调(使用服务对象并在那里添加回调)和验证(使用 Form 对象来包括模型的命名和验证)。
  • 将控制器保持为薄层并始终调用服务对象。 你们中的一些人会问为什么要使用控制器,因为我们想继续调用服务对象来包含逻辑? 好吧,控制器是进行 HTTP 路由、参数解析、身份验证、内容协商、调用正确的服务或编辑器对象、异常捕获、响应格式化以及返回正确的 HTTP 状态代码的好地方。
  • 服务应该调用 Query 对象,并且不应该存储状态。 使用实例方法,而不是类方法。 与 SRP 保持一致的公共方法应该很少。
  • 查询应该在查询对象中完成。 查询对象方法应该返回一个对象、一个散列或一个数组,而不是一个 ActiveRecord 关联。
  • 避免使用 Helpers 并改用装饰器。 为什么? Rails 助手的一个常见缺陷是它们会变成一大堆非 OO 函数,它们都共享一个命名空间并相互踩踏。 但更糟糕的是,没有很好的方法来使用 Rails 助手的任何类型的多态性——为不同的上下文或类型提供不同的实现,覆盖或子类化助手。 我认为 Rails 辅助类通常应该用于实用方法,而不是用于特定用例,例如为任何类型的表示逻辑格式化模型属性。 让它们保持轻盈和微风。
  • 避免使用关注点,而是使用装饰器/委托器。 为什么? 毕竟,关注点似乎是 Rails 的核心部分,并且在多个模型之间共享时会导致代码枯竭。 尽管如此,主要问题是关注点不会使模型对象更具凝聚力。 代码组织得更好。 换句话说,模型的 API 没有真正的变化。
  • 尝试从模型中提取值对象,以保持您的代码更简洁并对相关属性进行分组。
  • 始终为每个视图传递一个实例变量。

重构

在我们开始之前,我想再讨论一件事。 当你开始重构时,通常你会问自己: “那真的是很好的重构吗?”

如果您觉得您在职责之间进行了更多分离或隔离(即使这意味着添加更多代码和新文件),那么这通常是一件好事。 毕竟,解耦应用程序是一种非常好的做法,并且使我们更容易进行适当的单元测试。

我不会讨论诸如将逻辑从控制器转移到模型之类的东西,因为我假设您已经这样做了,并且您对使用 Rails(通常是 Skinny Controller 和 FAT 模型)感到满意。

为了保持这篇文章的紧凑,我不会在这里讨论测试,但这并不意味着你不应该测试。

相反,您应该始终从测试开始,以确保一切正常,然后再继续。 这是必须的,尤其是在重构时。

然后我们可以实施更改并确保代码的相关部分的测试全部通过。

提取值对象

首先,什么是值对象?

马丁福勒解释说:

值对象是一个小对象,例如货币或日期范围对象。 它们的关键特性是它们遵循值语义而不是引用语义。

有时您可能会遇到这样一种情况,即一个概念值得自己抽象,其平等不是基于价值,而是基于身份。 示例包括 Ruby 的日期、URI 和路径名。 提取到值对象(或域模型)非常方便。

何必?

Value 对象的最大优势之一是它们有助于在您的代码中实现的表现力。 您的代码往往会更加清晰,或者至少在您有良好的命名习惯的情况下可以这样。 由于值对象是一种抽象,它可以使代码更简洁,错误更少。

另一个重大胜利是不变性。 对象的不变性非常重要。 当我们存储某些可以在值对象中使用的数据集时,我通常不希望这些数据被操纵。

这什么时候有用?

没有单一的、一刀切的答案。 做最适合你的事情,在任何特定情况下做有意义的事情。

不过,除此之外,还有一些指导方针可以帮助我做出决定。

如果您认为一组方法是相关的,那么使用 Value 对象它们更具表现力。 这种表现力意味着一个 Value 对象应该代表一组不同的数据,普通开发人员可以通过查看对象的名称简单地推断出这些数据。

这是怎么做到的?

值对象应遵循一些基本规则:

  • 值对象应该有多个属性。
  • 属性在对象的整个生命周期中应该是不可变的。
  • 平等是由对象的属性决定的。

在我们的示例中,我将创建一个EntryStatus值对象以将Entry#status_weatherEntry#status_landform属性抽象到它们自己的类中,如下所示:

  • 要点样本

注意:这只是一个不继承自ActiveRecord::Base的普通旧 Ruby 对象 (PORO)。 我们已经为我们的属性定义了读取器方法,并在初始化时分配它们。 我们还使用了一个可比较的 mixin 来使用 (<=>) 方法比较对象。

我们可以修改Entry模型以使用我们创建的值对象:

  • 要点样本

我们还可以修改EntryController#create方法以相应地使用新的值对象:

  • 要点样本

提取服务对象

那么什么是服务对象呢?

Service 对象的工作是保存特定业务逻辑的代码。 与“胖模型”风格不同,其中少量对象包含许多用于所有必要逻辑的方法,使用服务对象会产生许多类,每个类都有一个目的。

为什么? 有什么好处?

  • 脱钩。 服务对象可帮助您实现对象之间的更多隔离。
  • 能见度。 服务对象(如果命名良好)显示了应用程序的功能。 我可以浏览一下服务目录,看看应用程序提供了哪些功能。
  • 清理模型和控制器。 控制器将请求(参数、会话、cookie)转换为参数,将它们传递给服务并根据服务响应重定向或呈现。 而模型只处理关联和持久性。 从控制器/模型中提取代码到服务对象将支持 SRP 并使代码更加解耦。 模型的职责将仅是处理关联和保存/删除记录,而服务对象将具有单一职责(SRP)。 这会带来更好的设计和更好的单元测试。
  • 干燥并拥抱变化。 我使服务对象尽可能简单和小。 我将服务对象与其他服务对象组合在一起,并重用它们。
  • 清理并加速您的测试套件。 服务易于快速测试,因为它们是具有一个入口点(调用方法)的小型 Rub​​y 对象。 复杂服务由其他服务组成,因此您可以轻松拆分测试。 此外,使用服务对象可以更轻松地模拟/存根相关对象,而无需加载整个 rails 环境。
  • 可从任何地方调用。 服务对象可能会从控制器以及其他服务对象、DelayedJob / Rescue / Sidekiq Jobs、Rake 任务、控制台等调用。

另一方面,没有什么是完美的。 Service 对象的一个​​缺点是它们对于一个非常简单的操作来说可能是多余的。 在这种情况下,您很可能最终使代码复杂化,而不是简化。

什么时候应该提取服务对象?

这里也没有硬性规定。

通常,Service 对象更适合大中型系统; 那些具有超出标准 CRUD 操作的大量逻辑的人。

因此,每当您认为代码片段可能不属于您要添加它的目录时,重新考虑并查看它是否应该转到服务对象可能是个好主意。

以下是何时使用 Service 对象的一些指标:

  • 动作很复杂。
  • 该操作涉及多个模型。
  • 该操作与外部服务交互。
  • 该操作不是底层模型的核心关注点。
  • 有多种方法可以执行该操作。

你应该如何设计服务对象?

为服务对象设计类相对简单,因为您不需要特殊的 gem,不必学习新的 DSL,并且可以或多或少地依赖您已经拥有的软件设计技能。

我通常使用以下准则和约定来设计服务对象:

  • 不要存储对象的状态。
  • 使用实例方法,而不是类方法。
  • 应该有很少的公共方法(最好是支持 SRP 的一种。
  • 方法应该返回丰富的结果对象,而不是布尔值。
  • 服务位于app/services目录下。 我鼓励您将子目录用于业务逻辑繁重的域。 例如,文件app/services/report/generate_weekly.rb将定义Report::GenerateWeeklyapp/services/report/publish_monthly.rb将定义Report::PublishMonthly
  • 服务以动词开头(不以 Service 结尾): ApproveTransactionSendTestNewsletterImportUsersFromCsv
  • 服务响应调用方法。 我发现使用另一个动词有点多余:ApproveTransaction.approve() 读起来不好。 此外,调用方法是 lambda、procs 和方法对象的实际方法。

如果您查看StatisticsController#index ,您会注意到与控制器耦合的一组方法( weeks_to_date_fromweeks_to_date_toavg_distance等)。 那不是很好。 如果要在statistics_controller之外生成每周报告,请考虑后果。

在我们的例子中,让我们创建Report::GenerateWeekly并从StatisticsController中提取报告逻辑:

  • 要点样本

所以StatisticsController#index现在看起来更干净了:

  • 要点样本

通过应用服务对象模式,我们将代码捆绑在特定、复杂的操作周围,并促进创建更小、更清晰的方法。

作业:考虑为WeeklyReport使用 Value 对象而不是Struct

从控制器中提取查询对象

什么是查询对象?

Query 对象是代表数据库查询的 PORO。 它可以在应用程序的不同位置重用,同时隐藏查询逻辑。 它还提供了一个很好的隔离单元来测试。

您应该将复杂的 SQL/NoSQL 查询提取到它们自己的类中。

每个 Query 对象负责根据条件/业务规则返回一个结果集。

在这个例子中,我们没有任何复杂的查询,所以使用 Query 对象效率不高。 但是,出于演示目的,让我们在Report::GenerateWeekly#call中提取查询并创建generate_entries_query.rb

  • 要点样本

Report::GenerateWeekly#call中,让我们替换:

 def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end

和:

 def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end

查询对象模式有助于使您的模型逻辑与类的行为严格相关,同时也使您的控制器保持精简。 由于它们只不过是普通的旧 Ruby 类,查询对象不需要从ActiveRecord::Base继承,并且应该只负责执行查询。

提取服务对象的创建条目

现在,让我们提取为新服务对象创建新条目的逻辑。 让我们使用约定并创建CreateEntry

  • 要点样本

现在我们的EntriesController#create如下:

 def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end

将验证移动到表单对象中

现在,这里的事情开始变得更有趣了。

请记住,在我们的指南中,我们同意我们希望模型包含关联和常量,但没有其他内容(没有验证和回调)。 因此,让我们从删除回调开始,并改用 Form 对象。

Form 对象是一个普通的旧 Ruby 对象 (PORO)。 它在需要与数据库通信的任何地方从控制器/服务对象接管。

为什么要使用 Form 对象?

在寻求重构您的应用程序时,牢记单一职责原则 (SRP) 始终是一个好主意。

SRP 可帮助您围绕类应负责的内容做出更好的设计决策。

例如,您的数据库表模型(Rails 上下文中的 ActiveRecord 模型)表示代码中的单个数据库记录,因此它没有理由关心您的用户正在执行的任何操作。

这就是 Form 对象的用武之地。

Form 对象负责在您的应用程序中表示一个表单。 所以每个输入字段都可以被视为类中的一个属性。 它可以验证这些属性是否满足某些验证规则,并且可以将“干净”的数据传递到它需要去的地方(例如,您的数据库模型或您的搜索查询构建器)。

什么时候应该使用 Form 对象?

  • 当您想从 Rails 模型中提取验证时。
  • 当单个表单提交可以更新多个模型时,您可能需要创建一个 Form 对象。

这使您可以将所有表单逻辑(命名约定、验证等)放在一个位置。

如何创建 Form 对象?

  • 创建一个普通的 Ruby 类。
  • 包含ActiveModel::Model (在 Rails 3 中,您必须包含命名、转换和验证)
  • 开始使用你的新表单类,就好像它是一个常规的 ActiveRecord 模型一样,最大的区别是你不能持久化存储在这个对象中的数据。

请注意,您可以使用改革 gem,但坚持使用 PORO,我们将创建如下所示的entry_form.rb

  • 要点样本

我们将修改CreateEntry以开始使用 Form 对象EntryForm

 class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end

注意:有些人会说不需要从 Service 对象访问 Form 对象,我们可以直接从控制器调用 Form 对象,这是一个有效的参数。 但是,我希望有清晰的流程,这就是为什么我总是从 Service 对象调用 Form 对象。

将回调移动到服务对象

正如我们之前所同意的,我们不希望我们的模型包含验证和回调。 我们使用 Form 对象提取了验证。 但我们仍在使用一些回调( Entry模型compare_speed_and_notify_user中的after_create )。

为什么我们要从模型中移除回调?

Rails 开发人员通常会在测试期间开始注意到回调的痛苦。 如果您没有测试您的 ActiveRecord 模型,那么随着应用程序的增长以及需要更多逻辑来调用或避免回调,您将开始注意到痛苦。

after_*回调主要用于保存或持久化对象。

一旦对象被保存,对象的目的(即责任)就已经完成。 因此,如果我们仍然看到在保存对象后调用回调,我们可能会看到回调到达对象的责任范围之外,这就是我们遇到问题的时候。

在我们的例子中,我们在保存条目后向用户发送短信,这与条目的域没有真正的关系。

解决问题的一个简单方法是将回调移动到相关的服务对象。 毕竟,为最终用户发送 SMS 与CreateEntry服务对象有关,而不是与 Entry 模型本身有关。

这样做,我们不再需要在我们的测试中存根compare_speed_and_notify_user方法。 我们使创建条目变得简单,无需发送 SMS,并且我们通过确保我们的类具有单一职责 (SRP) 来遵循良好的面向对象设计。

所以现在我们的CreateEntry看起来像:

  • 要点样本

使用装饰器而不是助手

虽然我们可以轻松地使用 Draper 的视图模型和装饰器集合,但为了这篇文章,我将坚持使用 PORO,就像我到目前为止所做的那样。

我需要的是一个将调用装饰对象上的方法的类。

我可以使用method_missing来实现它,但我将使用 Ruby 的标准库SimpleDelegator

下面的代码展示了如何使用SimpleDelegator来实现我们的基础装饰器:

 % app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end

那么为什么使用_h方法呢?

此方法充当视图上下文的代理。 默认情况下,视图上下文是视图类的实例,默认视图类是ActionView::Base 。 您可以按如下方式访问视图助手:

 _h.content_tag :div, 'my-div', class: 'my-class'

为了更方便,我们在ApplicationHelper中添加了一个decorate方法:

 module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end

现在,我们可以将EntriesHelper助手移动到装饰器:

 # app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end

我们可以像这样使用readable_time_periodreadable_speed

 # app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
 - <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>

重构后的结构

我们最终得到了更多文件,但这不一定是坏事(请记住,从一开始,我们就承认这个示例仅用于演示目的,不一定是重构的好用例):

 app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

结论

尽管我们在这篇博文中专注于 Rails,但 RoR 并不是所描述的服务对象和其他 PORO 的依赖项。 您可以将此方法用于任何 Web 框架、移动设备或控制台应用程序。

通过使用 MVC 作为 Web 应用程序的架构,一切都保持耦合并让您运行得更慢,因为大多数更改都会影响应用程序的其他部分。 此外,它还迫使您考虑将一些业务逻辑放在哪里——应该放在模型、控制器还是视图中?

通过使用简单的 PORO,我们将业务逻辑转移到不从ActiveRecord继承的模型或服务中,这已经是一个巨大的胜利,更不用说我们有更简洁的代码,它支持 SRP 和更快的单元测试。

干净的架构旨在将用例放在结构的中心/顶部,以便您可以轻松查看应用程序的功能。 它还使采用更改变得更容易,因为它更加模块化和隔离。

我希望我演示了如何使用普通旧 Ruby 对象和更多抽象来解耦关注点、简化测试并帮助生成干净、可维护的代码。

相关: Ruby on Rails 有什么好处? 经过两个十年的编程,我使用 Rails