業務ロジックをわかりやすく整理する
概要
この記事は、現場で役立つシステム設計の原則〜変更を楽で安全にするオブジェクト指向の実践技法を読み学習した内容を Ruby on Rails のコードに例を変換してまとめ直したものです。
また、書籍では Java のコードを例にしており、Ruby on Rails の慣習とは一部異なる部分があるので、その部分はオミットした内容となっています。
この記事では特に、オブジェクト指向における業務ロジックの適切な配置について記述しています。
手続き型設計とは
C 言語などの従来の手続型設計は以下のようにデータクラスとロジックを記述するクラスを分ける形が基本となる。
-
データクラス
- データを格納するためだけのクラス
- getter/setter メソッドのみを持つ
-
機能クラス
- データを使った判断/加工/計算を行う
- 業務アプリケーションでは以下のような三層アーキテクチャで整理するのが一般的
- プレゼンテーション層: 画面の表示や出力
- アプリケーション層: 業務ロジック、業務ルール
- データソース層: データベース入出力
手続き型設計の問題点
しかし、データクラスと機能クラスを分離する手続型設計には以下のような問題があり、ロジックの記述が重複しやすい。
-
データクラスを使うと同じロジックがあちこちに重複する
- プレゼンテーション層/アプリケーション層/データソース層では同じデータクラスを参照できるが、そのデータクラスを使用するロジックはどのクラスでも記述できてしまうので、同じロジックが三層の異なるクラスに重複して記述されがち。
- 三層のどこにでもロジックを記述できるゆえに変更箇所を特定するのに全ての層をチェックする必要があるり、副作用がないかを確認するために広い範囲をテストしなければならない。
-
データクラスを使うと業務ロジックの見通しが悪くなる
- アプリケーション層に業務ロジックを集めても以下の二つのパターンで見通しが悪くなりがち。
- 画面と機能クラスを 1 対 1 で関連付けて、複数の機能クラスに同じ業務ロジックが重複してしまう(例: 注文登録機能と注文変更機能で金額計算ロジックが重複)
- データテーブルの CRUD 操作単位に機能クラスを実装してしまい、重要な業務ロジックをどこに配置すべきかが明確でなくなる(例: 注文テーブルと出荷テーブルそれぞれの CRUD 操作ごとの機能クラスを実装したが、注文合計金額の算出をどちらにも配置すべきか明確でない)
- アプリケーション層に業務ロジックを集めても以下の二つのパターンで見通しが悪くなりがち。
-
共通機能ライブラリを作成しても失敗することがある
- 共通化できそうなロジックがそれぞれニーズが微妙に異なり、汎用化のために共通メソッドの引数が多くなってしまい使用しづらいメソッドになってしまう。結果、誰にも利用されなくなる。
- 増えすぎた引数を廃し、用途ごとに細分化した多数の共通関数を作成しても、メソッド数が膨れ上がり、ニーズに合致したメソッドを探し当てるのが面倒となり使われなくなる。
業務ロジックをわかりやすく整理する基本のアプローチ
手続き型設計の問題点のような状況を解決し、業務ロジックをわかりやすく整理するオブジェクト指向における基本のアプローチは以下の通り。
- データとロジックを一体にして業務ロジックを整理する
- 三層のそれぞれの関心事と業務ロジックの分離を徹底する
データとロジックを一体にして業務ロジックを整理する
クラスにデータと一緒にそのデータを使う判断/加工/計算を行うロジックを記述すれば、そのクラスを使う側にロジックを記述する必要がなくなりコードの重複が解消される。
そのクラスを使う側のコードがシンプルとなるオブジェクト指向らしいクラスを設計するには以下の点に配慮する。
メソッドをロジックの置き場所とする
データクラスがうまくいかないのは、自分が持っているデータをそのまま別のクラスに渡してしまうから。
以下のようにインスタンス変数を返すのみの getter メソッドを書くのはアンチパターン。
class Person
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
# インスタンス変数を返すだけのメソッド
def first_name
@first_name
end
def last_name
@last_name
end
end
以下のコードのfull_name
メソッドのようにインスタンス変数を使用して何らかの処理を行うのがメソッドの正しい使い方。
class Person
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def full_name
"#{@first_name} #{@last_name}"
end
end
業務ロジックをデータを持つクラスに移動する
例えば数値データを get するメソッドを作成する場合を想定すると、データを get するクラスではそのデータを使って何らかの計算をしたいと考えられる。
その計算ロジックを getter メソッドを持つクラスに移動すれば以下のようになる。
- データを持つ側のクラスにロジック(計算式)が増える
- データを get していたクラスからロジックがなくなる
- 使う側のクラスは、データを get するのではなく、そのデータを使った計算結果を受け取るようになる
このようにデータを持つクラスに業務ロジックを集めることで、コードの重複や散財を防ぎ、変更の対応箇所をそのクラス内に限定できるようになる。
使う側のクラスに業務ロジックを書き始めたら設計を見直す
設計の初期段階においては、ロジックを持たないデータクラスを作成したり、データを持たないクラスにロジックを書いてしまうことは問題ない。
ただし、アプリケーションがとりあえず動くようになった後も、データを持つクラスから get してそのデータを使ってデータを持たないクラスが判断/加工/計算を行うようなロジックを書き始めたらその都度設計を見直すことを繰り返す。
メソッドを短く書くとロジックの移動がやりやすくなる
長いメソッドを小さく分けて独立させると、本来ならばそのクラスにふさわしくないコードの塊を発見しやすくなる。
数行のコードをメソッドとして独立させ、そのメソッドをデータを持つクラスに移動させることを繰り返すと、データとロジックが同じ場所に集められプログラムがわかりやすくなる。
メソッドは必ずインスタンス変数を使う
以下のような引数のみを使い、インスタンス変数を使わないメソッドはそのクラスに置く必要がないので置き場所を再検討する必要がある。(別のクラスに移動。または、呼び出し元のクラスに移動する)
def total(unit_price, quantity)
total =unit_price * quantity
total.round
end
クラスが肥大化したら小さく分ける
以下のようなインスタンス変数が多いクラスに関連する業務ロジックを集めると、そのクラスは次第に肥大化してしまう。
このような状態になるとクラス内部の見通しが悪くなり、変更時に副作用がないか心配する範囲が広がる。
このような状態となったら、関連性の強いデータとロジックを抜き出して新しいクラスに分けることを検討する。
まず、インスタンス変数とメソッドの関係を調べ、同じインスタンス変数を使うメソッド群を一つのグループとして抜き出す。
class Customer
def initialize(
first_name,
last_name,
postal_code,
city, address,
telephone,
mail_address,
telephone_not_preferred
)
# インスタンス変数をグループ分け
# 名前
@first_name = first_name
@last_name = last_name
# 住所
@postal_code = postal_code
@city = city
@address = address
# 連絡先
@telephone = telephone
@mail_address = mail_address
@telephone_not_preferred = telephone_not_preferred
end
def full_name
"#{@first_name} #{@last_name}"
end
...
end
上記のような場合、full_name メソッドで使用しているのは @first_name と @last_name の二つのインスタンス変数のみで、他のインスタンス変数は使用していないので、これらを抜き出して新しいクラスに分割すると下記のようなクラスができる。
class PersonName
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def full_name
"#{@first_name} #{@last_name}"
end
end
他のグループも同様にしてクラスへ分割すると最終的に以下のようにすっきりした Customer クラスへ整理できる。
抽出した PersonName, Address, ContactMethod クラスは密接に関連したデータ・ロジックのみとなり独立性が高く別クラスでも再利用しやすい部品となっている。
class Customer
def initialize(person_name, address, contact_method)
@person_name = person_name
@address = address
@contact_method = contact_method
end
end
抽出した PersonName クラスなどのように関連性の強いデータとロジックだけを集めたクラスを凝集度が高いと表現する。
オブジェクト指向のクラス設計では、「切っても切れない」関係のデータとロジックをまとめる「凝集」が基本となる。
パッケージを使ってクラスを整理する
インスタンス変数とメソッドの関係に注目しながらクラスの抽出を繰り返すと、クラスの数が増えてくる。
クラスが増えてきた際に、それらを整理する手段がパッケージである。
Ruby on Rails
では、パッケージは主にモジュール(Module)とディレクトリ構造を使って表現される。
# app/models/admin/user.rb
module Admin
class User < ApplicationRecord
# 管理者用のUserクラス
end
end
# app/models/customer/user.rb
module Customer
class User < ApplicationRecord
# 顧客用のUserクラス
end
end
Ruby on Rails
の規約では、ディレクトリ構造がそのまま名前空間に対応する。
app/
├── models/
│ ├── admin/
│ │ ├── user.rb
│ │ └── product.rb
│ ├── customer/
│ │ ├── order.rb
│ │ └── cart.rb
│ └── concerns/
│ └── searchable.rb
├── controllers/
│ ├── api/
│ │ └── v1/
│ │ └── users_controller.rb
│ └── admin/
│ └── dashboard_controller.rb
パッケージを用いてクラスを整理する基本のアプローチは以下の通り。
- 関連性の強いクラスは同じパッケージに集める。
- クラスやメソッドのスコープは可能な限り、パッケージスコープにする( public 宣言しない)
- パッケージのクラスが増えたらさらにサブパッケージに分ける
- クラス数が少なくとも長いパッケージ名をつけたくなったら、パッケージを階層にして、一つ一つのパッケージ名を短くする
- 開発が進むにつれて、対象領域の知識が増え、全体の構造への理解が深まるたびにパッケージの命名・階層の設計を都度見直す
三層のそれぞれの関心事と業務ロジックの分離を徹底する
業務ロジックを小さなオブジェクトに分けて記述する
関連する業務データと業務ロジックを一つにまとめたクラスをドメインオブジェクトと呼ぶ。(ドメインは対象領域・問題領域の意)
業務データと業務ロジックを小さい単位で整理したドメインオブジェクトを設計し、それらを組み合わせてより大きな業務の関心事を表現するクラスを設計する。
例えば、「注文」オブジェクトは以下のように「注文者」「配送先」「支払情報」「注文明細」「ステータス」「合計金額」といったいくつもの小さいドメインオブジェクトを組み合わせて構築する。
業務ロジックの全体を俯瞰して整理する
さまざまなドメインオブジェクトを作成し数が増えたら、「パッケージ」を用いて整理することで業務ロジック全体を見通しよく整理できる。
以下のように業務の流れの時間軸に沿ったいくつかのドメインオブジェクトを内包するパッケージ図を作成し、そのパッケージ間の参照関係を矢印で表現すると、業務ロジックの全体を俯瞰して整理できる。
以下だと、赤矢印が業務の時間の流れに沿ったフローであり、黒矢印が参照可能なパッケージを表している。
このように、業務データと業務ロジックを一緒にしたドメインオブジェクトを設計する際に、どのパッケージに置くべきか、そしてどのパッケージの内容を知っても良いか、知ってはいけないかを整理することで、業務ロジックの全体的な関係が明確になる。
同時に「どこ」に「何」が書いてあるかが明確になり、今後の変更やメンテナンスの指針となる。
このように業務アプリケーションのドメインをオブジェクトのモデルとして整理したものをドメインモデルと呼ぶ。
三層 + ドメインモデルで関心の分離をわかりやすくする
ドメインモデルを作成することで、データクラス + 機能クラスの構成ではあちこちに散らばっていた業務ロジックの置き場所が明確になり、業務アプリケーションの三層アーキテクチャと以下のように明確な関心の分離ができる。
また、ドメインモデルにロジックを分離することで、三層それぞれのコードはシンプルで簡潔となり、読みやすくなる。
名前 | 説明 |
---|---|
プレゼンテーション層 | UI など外部との入出力 |
アプリケーション層 | 業務機能などのマクロな手順の記述 |
データソース層 | データベースとの入出力 |
ドメインモデル | 業務データと関連する業務ロジックを表現したドメインオブジェクトの集合 |
参考文献
この記事は以下の情報を参考にして執筆しました。