個人開発で作ったサービスをClean Architectureの視点で作り替える

豆腐、納豆、豆乳、玄米、鶏胸肉、プロテインオートミールという生活に飽きてきた24歳です。

 

最近、オブジェクト指向設計って何か、Cleanなソフトウェアを作るってなんなのかに興味が湧いています。

設計を学んでいくと、なぜ凝集度や結合度、そしてリーダブルなコードを書かなければならないのかの輪郭を掴めてきました。

今回は、一番身近な例を使ってClean Architectureに対する理解を深めていきたいと思います。

 

 

Zerokenがお金を生み出す(仮)場所を考える

まずはお金を生み出す、もしくは節約する最重要ビジネスロジック、データの塊を見つける。

そのために、どんなメッセージが一番重要なのか考える必要がある。

「提供するお酒の広告収入」かな?

なんか違う気がするな。

「提供するお酒の広告収入」は、ユーザー数が多い程多くもらえる。

つまり、ユーザーが Zerokenに求めてる価値が「お金を生み出す」場所になるのか。

となると、下記になると思う。 「お酒の強さを診断して次の飲み会で飲むお酒の順番を確認する」

このドメインを育てていくことが最大目標と思うと、しっくりくる。

そのためのエンティティを洗い出していきたい。

エンティティ

ユーザー

  • 値オブジェクト 名前 メールアドレス 体重 好きなお酒の種類
  • 振る舞い お酒の強さを診断する

企業

  • 値オブジェクト 名前 お酒の名前
  • 振る舞い お酒を出品する お酒を考案する ユーザーを選別する

お酒

  • 値オブジェクト アルコール度数 計算式 容量
  • 振る舞い 血中アルコール濃度計算式 酔いの程度計算式 お酒の強さの計算式

オブジェクト指向設計をする上で、アプリに必要なメッセージを考える必要がある。 現状思いつくメッセージは下記のようなものか。

メッセージ

下記のようなメッセージは欲しいかな。

  • ユーザーがTastを使ってお酒の強さを5段階で診断する
  • 診断結果を基に次の飲み会で飲むお酒を提案する
  • 診断結果は履歴として保存される
  • マイお酒を一つ登録しておくことができる
  • 下戸と診断された場合、下戸証明書が発行される
  • お酒の種類のタグをクリックしたら一覧に飛ぶ
  • 一日一回だけ診断することができる
  • お酒は蒸留酒醸造酒・混成酒に分けられる
  • 診断結果が0以下の場合は下戸、0以上の場合は酒豪である
  • 血中アルコール濃度によって酔い度が決まる
爽快期:アルコール血中濃度>0.05,
ほろ酔い: アルコール血中濃度≥0.10,
酩酊初期: アルコール血中濃度≥0.15
酩酊: アルコール血中濃度≥0.30

  • アルコール量
**A(アルコール量g) =** 飲酒量ml ×アルコール濃度/100 × アルコール比重(0.8もしくは0.792)
# アルコール濃度は,%又は度で表示されている。

  • 血中アルコール濃度mg/ml
**C(血中アルコール濃度mg/ml)=** A(アルコール量g)/体重kg×γ(アルコール体内分布係数)
(γ=低0.60ないし高0.96)

  • 血中アルコール濃度は時間の経過とともに減少し,t時間後の濃度は,次の式で算出される。
**Ct(t時間後の血中アルコール濃度mg/ml)=** C-β(アルコール減少率)× t
(β=低0.11ないし高0.19)

モデルをクリーンにするために、関心事の分離を行う

上位の方針に依存するような形で、層を分けていきたい。

User←Interface←Controller・Service←Presenterって感じか。

そもそもServiceってなんなんだ。。

サービスとは

ドメインの知識のうち、どのドメインモデルエンティティ*2にも属さない振る舞いあるいは複数のドメインモデルエンティティを操作するような振る舞いをDDDでは「サービス」と呼びます。 ドメインエキスパート等の仕様策定者が名前をつけて呼ぶ単語の中で動詞としてよく登場するものはサービスである可能性が高いです。(例: 会員登録) 以下のような特徴を持つ機能はサービスとして定義することを考えてみてください。

  • モデルAR継承クラスのクラスメソッドとして定義されている/しようとしている
  • メソッドの引数として別のモデルAR継承クラスのインスタンスを必要とする
  • 物理層(永続化層)における複数テーブルに対してINSERT/UPDATEを行う(トランザクション制御が必要)
  • DB登録、API通信、メール送信など外部システムや外部リソースとの連携を同時に行う

https://a-suenami.hatenablog.com/entry/2013/12/06/092146

ほう、なるほど。 今回は勉強のために、関心事の分離を意識しながら、新規登録のサービスクラスを作っていきたい。

  • ドメインオブジェクト まあユーザーがドメインオブジェクトになるのか。お金を生み出すし。

振る舞いは、以下二つでいいか。

「ユーザーはTast(診断フォーム)に回答する」

「診断結果を共有する」

class User
  attr_reader :name, :email

  def initialize(attrs)
    @name = attrs[:name]
    @email = attrs[:email]
  end
## public interfaces

## AnswerTastServicesに責務を渡すべき
  def answer_tast
    raise NotImplementedError
    p '{self.answer_tast}が定義されていません'
  end

## ShareResultServicesに責務を渡すべき
  def share_result
        raise NotImplementedError
    p '{self.share_result}が定義されていません'
  end
end

  • Serviceオブジェクト

新規登録を管理するServiceオブジェクトを作成する。一番わかりやすかった。

module UserRegistrationService
  class Base
  def initialize
    @params = params
  end

  def exec
    return if false
      User.transaction do
        # TODO 会員登録処理を実行する
        # 具体的な処理は別のクラスへ委譲する

      sub_service = UserRegistrationService::SendingWelcomeMessage.new(user)
      sub_service.exec
  end
  end
end

module UserRegistrationService
  class SendingWelcomeMessage
    def initialize(member)
      @member = member
    end

    def exec
      # TODO 登録ありがとうメールを送る
    end
  end
end

  • Formオブジェクト

ユーザーからのインプットを加工する役割を持つオブジェクト

Serviceで作ったオブジェクトの生成を依存注入すると、アプリ内の責務を分けられて良い

class UserRegistrationForm
  include ActiveModel::Model

  attr_reader :name, :email,

  validates :name,  presence: true
  validates :email, presence: true

  def save(attr)
    return if false
      User.transaction do
       user_params =  UserRegistrationService::Base.new({ name: attr[:name], email: attr[:email] })

        persisted?

        user.save
     end
  end
end

  • Controller

モデルの責務をservice, formに分けるとコントローラーが肥大化しなくて済む

class UserController
  def create
    @form = UserRegistrationForm.new(user_params)

    if @form.save
      redirect_to user_path(user), notice: 'some massage.
    else
      render :new, notice: 'some massage.'
    end
  end

private

  def user_params(params)
     params.require(:user).permit(:name, :email)
  end
end

Railsでサービスとフォームを導入してみる話 - assertInstanceOf('Engineer', $a_suenami)

Autoloading and Reloading Constants - Ruby on Rails Guides

オブジェクト指向設計実践ガイド

現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法

Clean Architecture 達人に学ぶソフトウェアの構造と設計