Rails 应用程序的领域驱动设计:在基本示例中实现服务

Domain Driven Design for Rails App: Implementing a service in a basic example

两个模型:一个 Owner 和一个 Dog:

owner.rb

class Owner < ActiveRecord::Base
  has_one :dog
end

dog.rb

class Dog < ActiveRecord::Base
    belongs_to :owner
end

架构如下:

schema.rb

ActiveRecord::Schema.define(version: 123) do

  create_table "dogs", force: true do |t|
    t.string   "name"
    t.integer  "energy"
    t.integer  "owner_id"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "dogs", ["owner_id"], name: "index_dogs_on_owner_id"

  create_table "owners", force: true do |t|
    t.string   "name"
    t.string   "energy"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

end

非常简单的设置。

我想要一个 owner 带他的 dog 去散步。散步结束后,主人体力下降5,狗体力下降20

很明显,这个 walk_the_dog action/method,无论它存在于何处,都会影响两个对象:一个 owner 对象和一个 dog 对象(当然还有这个dog 对象恰好与该所有者相关联)。

我不知道把这段代码放在哪里。我知道我 可以 简单地在 owners_controller.rb 中创建一个动作,但这似乎是个坏主意。它看起来像这样:

owners_controller.rb

class OwnersController < ApplicationController
    def walk_the_dog
        @owner = Owner.find(params[:id])
        @owner.energy -= 5
        @owner.dog.energy -= 20   # this line in particular seems like bad OO design
        @owner.save
        @owner.dog.save
    end
    ...
 end

据我了解,对象应该只改变自己的状态,不应该改变其他对象的状态。所以这似乎是个坏主意,因为在所有者控制器中,我们不仅要更改 owner 对象的状态,还要更改关联的 dog 对象的状态。

我已阅读有关服务的信息。 walk_the_dog 似乎是服务的绝佳案例,因为据我了解,服务允许对象之间的交互以及多个对象的状态更改。我只是不知道该怎么做 it/implement。

是否应该有一个名为walk_the_dog的服务对象?它们是否应该只是服务目录中的一个文件,其中包含一堆服务方法——其中一个称为 walk_the_dogowners_controller.rb 只是在是控制器?我不知道下一步是什么。

注意:我看到有人说 "who cares if this breaks OO design. Just do it and if it works, it works." 不幸的是,这不是一个选项。我现在正在开发的应用程序遵循了这种思维方式。应用程序变得非常大,现在维护起来非常困难。我想在应用程序的重大重新设计中解决这种情况。

如果我重构这段代码,我会做以下几件事:

在你的代码中写数字是一件坏事,要么你将它们定义为像 ENERGY_PER_WALK_FOR_DOG = 20 这样的常量,要么更好的方法是在 Dog 的 table 中定义一个字段模型。这样,管理和分配这些值会更好。

add_column :dogs, energy_per_walk, :integer, default: 20
add_column :owners, energy_per_walk, :integer, default: 5

我会在 ApplicationController class:

中创建一个方法
def walk(resources = [])
  resources.each do |resource|
    resource.lose_walk_energy # you can refine it more!
  end
end

在文件夹app/models/concerns中,我将编写以下模块:

module Walkable
  extend ActiveSupport::Concern


  # subtract energy_per_walk form the energy saved in db
  def lose_walk_energy
    self.energy -= self.energy_per_walk
    save
  end

end

现在,您的方法简化为以下方法:

def walk_the_dog
  @owner = Owner.find(params[:id])
  walk([@owner, @owner.dog])
end

我会说这应该是Owner模型中的一个方法。您还需要在一个事务中执行这两个操作,以确保两个模型都已更新。

class Owner
  has_one :dog

  def walk_the_dog
    return false if dog.nil?

    transaction do
      decrement!(:energy, 5)
      dog.decrement!(:energy, 20)
    end
  end
end