【Ruby】オブジェクト思考設計実践ガイドを読み始めた#7章
モジュールでロールの振る舞いを共有
継承を使って、共通の性質や振る舞いを共有することができます。
ただ、Rubyは単一継承であるため、複数の親クラスの性質を継承することはできません。
そんな時は、Rubyのモジュール
を使って、共通の役割(ロール
)を定義することで、
多重継承に似た動作を実現していきます。
モジュールって何?
具体例から入ると下のようなコードです。
module Loggable def log_warn(text) puts "[Warn] #{text}" end end class User include Loggable def name log_warn("User Class") 'Alice' end end class Product include Loggable def name log_warn("Product Class") 'movie' end end user = User.new puts user.name product = Product.new puts product.name
module
という宣言を利用してLoggable
というモジュールを作成しています。
モジュールをUser
とProduct
クラスがinclude
して、メソッドを利用しています。
User
とProduct
のように、継承関係が結びつくものではない、ただログ出力という共通の役割(ロール)
を持っている時に、
モジュールを利用することで、それぞれのクラスに共通の振る舞いを実装し、多重継承のような実装が可能になります。
モジュールの使う用途としては下記のような感じです。
継承を使わずにクラスにインスタンスメソッドを追加する、もしくは上書きする(mix in)
複数のクラスに対して共通のクラスメソッドを追加する(mix in)
クラス名や定数名の衝突を防ぐために、名前空間を作る
シングルトンオブジェクトのように扱って、設定値などを保持する
頻繁に利用するのは共通の振る舞いを実装するmix in
の使い方になると思います。
mix in
にも、include
を利用する方法とextend
を利用する方法、2通りがあるので少し説明を残していきます。
mix inの方法1 include
include
を利用した方法は上のコードのように、モジュールで定義したメソッドをインスタンスメソッドのような形で利用する方法です。
下記のようにクラスメソッドとして、動作させようとした場合にはエラーが発生します。
module Loggable def log_warn(text) puts "[Warn] #{text}" end end class User include Loggable def name log_warn("User Class") 'Alice' end end class Product include Loggable def name log_warn("Product Class") 'movie' end end user = User.log_warn("hoge") # 出力 Traceback (most recent call last): chap7/code.rb:26:in `<main>': undefined method `log_warn' for User:Class (NoMethodError)
モジュールをmix inする方法2 extend
include
を利用した場合は、インスタンスメソッドとして動作します。
ただ、共通のクラスメソッドとして動作させたいケースもあると思います。
そんな時に使うのがextend
になります。
module Loggable def log_warn(text) puts "[Warn] #{text}" end end class User extend Loggable def name log_warn("User Class") 'Alice' end end # クラスメソッドとして動作 User.log_warn("hoge") user = User.new # インスタンスメソッドとしては動作しなくなる user.log_warn # 出力 [Warn] hoge Traceback (most recent call last): chap7/code.rb:22:in `<main>': undefined method `log_warn' for #<User:0x00007ff612970d50> (NoMethodError)
継承とmixinの違い
module
であっても、テンプレートメソッドパターン(下記のようなコード)にすることで、
継承のような振る舞いにさせることができます。
module Loggable def put_log(text) puts "#{log_level} #{text}" end def log_level "Warn" end end class User include Loggable # moduleで定義したメソッドを上書き def log_level "Info" end end
モジュールで宣言したメソッドをinclude先で、上書きして特化したメソッドのように振舞わせることができるようになります。
継承が縦に依存関係を構成するとしたら、モジュールの場合は横に依存関係を広げていくようなイメージになります。
モジュールを実装する上でのアンチパターン
継承を実装する上でのアンチパターンは、
一部のサブクラスでしか使わないメソッドをスーパークラス
に定義することです。
モジュールでも同様に、モジュールをincludeしたクラスの一部だけで使うようなメソッド
を定義することはアンチパターンになります。
また、実装を強制するようなNot implemented error
のように例外を吐かせるコードも避けた方がいいとされています。(これはケースバイケースになるとは思いますが。。。実装されているメソッド自体が少ないかつ実装を強制させるパターンのみの場合は、やってもいいような気がする)
アンチパターンを避けるテクニックとしてテンプレートメソッドパターンを使うことが挙げられます。
まとめ
moduleを使うことで、多重継承のような形で共通の振る舞いを縦に関係がないクラス間で共有することができます。
一方で、moduleを乱用しすぎることでクラス間の関係が、横に、横に、広がっていってしまうことは意識しておかなければなりません。
moduleのような機能を利用したRailsのconcerns
なんかもアプリの可読性が低くなってしまうこともあるため、
便利なのですが使う際には少し考える必要があるようです。
https://techracho.bpsinc.jp/hachi8833/2019_09_24/80832
https://blog.willnet.in/entry/2019/12/02/093000
moduleを使うタイミングは少し分かったような、まだ分かっていないような。。
【Ruby】オブジェクト思考設計実践ガイドを読み始めた#5
記事に書くこと
前回に引き続き、オブジェクト思考設計ガイドを読み進めています。 5章で学んだことをまとめます。
先にまとめ
ダックタイピング
は同様のメソッドを持っていればどんなオブジェクトでも同じように扱える- クラスの種類ごとに処理を分岐しているケースでは
ダックタイピング
を使って依存度を下げる
ダックタイピングって?
なんだかpythonを勉強した時にも読んだ記憶がありますが、
いまいち使いどころが分からないままだった奴です。
"If it walks like a duck and quacks like a duck, it must be a duck" (もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない)
どの本にも出てくる言葉ですよね。
ダックタイピング自体は、オブジェクトのクラスがなんであろうと、同様のメソッドを持っていれば、
同じものとして扱うようなプログラミングスタイルだと思っています。
コードから考えた方が、理解しやすいと思います。
まずは、ダックタイピングではない下のコードから。
class EmploymentWorker def work(employees) employees.each do |employee| case employee when Staff then employee.do_clean when Managager then employee.do_check end end end class Staff def do_clean # ~~~~~~~ end end class Managager def do_check #~~~~~~~~~~ end end
EmploymentWorker#work
では、Staff
もしくはManager
が格納された配列を受け取っているようです。
さらに、引数で渡された配列を順にwhen ~ case
で処理を分岐させています。
上の規模感的には問題無いように見えますが、拡張性とかを含めると下記の問題があります。
case ~ when
に依存しすぎている- 操作対象のクラスが増えるたびに、
when
を増やして行かないといけない
- 操作対象のクラスが増えるたびに、
- メッセージの送り先にも依存しすぎている
- クラスごとに、呼び出し先のメソッドを
EmploymentWorker
を知っている(依存する)必要がある - 依存先のオブジェクトに変更があった際には
EmploymentWorker
も変更する必要が出てくる
- クラスごとに、呼び出し先のメソッドを
とはいえ、EmplomentWorker
は複数のクラスに対して操作するできるような実装をしなければいけません。
どんなオブジェクトであっても、同様のメソッドを持っていれば、同じものとして扱えるようにリファクタしていきたいと思います。
EmploymentWorker
からすれば、どのクラスであってもwork
してくれればいいという書き方にリファクタした物が下記のコードです。
class EmploymentWorker def work(employees) employees.each do |employee| employee.work end end end class Staff def work do_clean end private def do_clean #~~~~~~~ end end class Managager def work do_check end private def do_check #~~~~~~~~~~ end end
EmploymentWorker
の依存度が一気に減った気がします。
ダックタイピングを使うことで、依存先の抽象度を高くすることで、
後からでも拡張しやすい構造にリファクタすることできました。
修正前 | 修正後 |
---|---|
扱えるオブジェクトが増えるたびに、case~when を増やす必要があった |
work メソッドさえ実装していればどんなオブジェクトも扱える |
扱うオブジェクトのメソッドまでEmploymentWorker は知っている必要があった |
具体的なメソッドは呼び出し先に移譲することで、work さえ知っていればよくなった |
ダックタイプをどうやって見つけるのか?
ダックタイプを使うことで依存度を減らせることが分かりました。
ただ、実際にダックタイプをどのように見つけていくのか?
書籍では、後術の3通りパターンにはダックタイプに置き換えることができるように書かれていました。
パターン1 クラスで分岐するcase文
上でダックタイプの例で書いたコードがこのパターンです。
class EmploymentWorker def work(employees) employees.each do |employee| case employee when Staff then employee.do_clean when Managager then employee.do_check end end end class Staff def do_clean # ~~~~~~~ end end class Managager def do_check #~~~~~~~~~~ end end
EmploymentWorker
からすれば、どのオブジェクトであっても、work
してくれればいいという形で置き換えることができます。
パターン2 kind_of?とis_a?を使うパターン
オブジェクトのクラスを確認する方法で、パターン1ではwhen case
を使っていました。
when case
ではなくても、kind_of?
やis_a?
でもクラスを確認することができます。
これらを利用して処理を分岐しているパターンにおいてもダックタイピングで置き換えることができます。
class EmploymentWorker def work(employees) employees.each do |employee| if employee.kind_of?(Staff) #~~~~ elsif employee.kind_of?(Managager) #~~~ else #~~~ end end end end
パターン3 respond_to?
respond_to?
は、あるオブジェクトがメソッドを持っているか(実装しているか)を判定するメソッドです。
先ほどのパターンまでは、クラスに依存していましたがメソッドに依存して分岐しているケースでも、
ダックタイプに置き換えることができます。
employees.each do |employee| if employee.respond_to?(:do_clean) #~~~~ elsif employee.respond_to?(:do_check) #~~~ else #~~~ end end
【Ruby】オブジェクト思考設計実践ガイドを読み始めた # 第4章
前回と同様に、オブジェクト指向設計実践ガイドの続きです。
概念的な物がとても難しい。。。_
第4章 柔軟なインタフェースを作る
序文では下記のようなことが書かれていました。
- オブジェクト指向はただのクラスの集まりではない
- オブジェクトの責任、オブジェクトの依存関係ではなく、メッセージにも着目しなければならない
2つ目のメッセージ
は、おそらくメソッドのことかなと思うのですが、どうなんでしょうか??
インタフェースを理解する
下の図のように、オブジェクト同士のメッセージのやりとりが構成されているとします。
この時、パターンAとパターンBどちらがより良い設計でしょうか。
パターンAは、任意のオブジェクトが任意のオブジェクトに対してメッセージを送れるような構成になっています。
あるオブジェクトを変更するとアプリケーション全体に影響がかかる、あまりよろしくない設計です。
対してパターンBは、メッセージのパターンに何らかの制約があるように見受けられます。
パターンAのようなアプリケーションにならないために、
何をどのように外部に晒すのか?
と言うことについて考える必要があります。
インタフェースを定義する
インタフェースには、
外部からメッセージを受け取ることができる
パブリックインタフェース
内部のみで利用する
プライベートインタフェース
があります。
これらのインタフェースについて考えていきます。
パブリックインタフェース
クラスが外部に公開するメソッド(パブリックインタフェース
)は次の特性を備えます
- クラスの主要な責任を明らかにする
- 外部から実行されることが想定される
- 気まぐれに変更されない
- 他者がそこに依存しても安全
- テストで完全に文書化されている
テストで完全に文書化
されているってなるほどなと思います。
Railsでもどの程度メソッドをテスト対象に含めるべきかって、最初の方迷いがちだったので、
1種の判断材料になりますよね。
下にも書いてありますが、プライベートインタフェース
だからテスト対象に入らないという訳ではないので、
ある程度の規模感と複雑性を持ってテストをすべきなのでしょうか。
プライベートインタフェース
クラス内のみで使われるメソッド(プライベートインタフェース
)は次の特性を備えます
- 実装の詳細に関わる
- 他のオブジェクトから送られてくることを想定しない
- どんな理由でも変更され得る
- 他社がそこに依存することは危険
- テスト項目に入らないこともある
ドメインオブジェクト
一応、書内では仮想的な呼び方になっています。
よく分からないですが、DDDとかだとまた違う解釈になっているのだと思います。
ドメインオブジェクトの特徴としては、
データ
と振る舞い
の両方を兼ね備えた名詞- 例えば、ユーザ、記事とか
- 永続化する
ドメインオブジェクトとして管理すべき物は、アプリケーション設計で比較的簡単に見つかりますが、
そこにこだわりすぎると、無理な振る舞いをしてしまいがちです。
オブジェクト間のメッセージに着目してここら辺の振る舞いを改善していくことが大事らしいです。
クラス図に執着するよりも、シーケンス図に目を向けるといような感じでしょうか。
「どのように」を伝えるのではなく「何を」を頼む
パブリックインタフェースが小さいと言うことは、他のところから依存されるメソッドが僅かしかないことを意味する
依存関係について学んだ際に、変更される可能性が低い
もの依存すると言うセオリーがありました。
クラスも同様で、あるオブジェクトが依存するパブリックのインタフェースはなるべく変更されないように実装しなければいけません。
(と言うより、突然メソッド名変えましたって言われたら確かに誰でもキレそうだな〜)
下のシーケンスを考えてみます。
TripオブジェクトからMechanicオブジェクトに対して、bike
を引数にいくつかの処理を加えています。
この場合、bike
に対して処理が追加された場合に、同じような処理を呼び出している箇所全てを呼び出す必要があります。
この複数の処理を、Mechanicオブジェクトに責務を持たせるようにします。
このようにすることで、bike
に対して必要な処理が増えたとしても、Mechanicのみを修正すればいいので、
メンテナンス性とコードの柔軟性が向上させることができるようになるようです。
コンテキストを最小限にする
ここで言う、コンテキストはメソッドの引数とかで使われるcontext
とは別物だと思います。
メッセージを送るオブジェクトが求めているものというかもっと概念的な物な気がする(理解できるようになりたい)
パブリックインタフェースを構築する際には、このコンテキストを最小限にする必要があります。
メッセージの送り手が、送り先のクラスがどのように振舞いを実装しているか
知らなくても使えるようにしてくれということです。
(確かに、なんか使えそうなメソッドがあった際に、中身の実装を細かく分析するのは辛そうです。)
使うメソッドがどのように振舞うのか
ではなく、何をする
だけを考えるだけで使うことができれば、
それは再利用ができているメソッドになります。
デメテルの法則
いわゆる、異なるオブジェクト間のメッセージチェーンは繋げすぎないという法則です。
例えば、userとpostとcommentとcomment_attachmentfileがあるとして、
こんな感じの繋げ方は正直NGです。
user.posts.first.comments.attachmentfiles.url.underscore!
userとは遠くにあるオブジェクトをみているということになります。
なぜいけないのか?
userからattachmentfilesにたどり着くまでに、
postsとcommentsを経由しています。
この中間にいるオブジェクトの変更さえも動作に影響する可能性が出てくるためです。
ただし中間のオブジェクトが全て同じ方をもつ場合には、デメテルの法則違反にはならないことは覚えておく必要があります。
違反しない例
hash.keys.sort.join
まとめ
すんなり理解できたとは言えないですが、
設計のときは下のことも意識する必要がありそうです。
- メッセージに着目する
- メッセージの送り先が送り手の振る舞いに依存していないか
- ドメインオブジェクトの中間に存在するオブジェクトが存在しないか
- パブリックインタフェースは変更されにくいように設計する
- 明示的なインタフェースを作る
どのように振舞うか
ではなく何をする
になっている
【Ruby】オブジェクト思考設計実践ガイドを読み始めた(第3章)
前回に引き続き、 オブジェクト指向設計実践ガイドの読書記録です。
第3章 依存関係を整理する
先にまとめると
依存関係はなるべく少なくする
- 必要な場合は、メソッドに閉じ込めて、変更への影響範囲を最小限にする
依存する方向は、
変更が少なそうな物
に対して- 要件が明確
- 抽象クラス、インタフェース
変更が多い
かつ依存されている数が多い
クラスは存在させないようにする
依存関係って何?
そもそも依存関係って何でしょうか?
「オブジェクト指向 依存関係」と言う、ど素人感が半端ない、ワードで検索してみたところ
こちらのQiita記事を発見しました。
それが下のような物。(改行を含む場合にmdで引用を書く方法分からん。)
プログラムの要素が依存する物といえば、多くの場合は以下のような物を指す事が多いと思います。
- 参照している他の構成要素(引数や他のクラスなど)
- 使用しているライブラリやフレームワーク
前回のGearクラスの例だと、Gearクラスからオブジェクトを生成するためには、
Wheelクラスから生成されたオブジェクトが必要でした。
これが依存関係になります。
Gearの機能を全て使うためには、Wheelが必要になる
と言うことになります。
他にも実際のプログラムは多くのものに依存しています。
依存関係を作ることは、アプリケーションを作成する上で、必ず必要なものですが、
慎重に管理しないとかなり可読性、拡張性の低い物になってしまいます。
依存関係を理解する
前項で、依存関係を自分で調べた内容を記載していますが、
書籍にはこのように書かれていました。
一方のオブジェクトに変更を加えた時、他方のオブジェクトも変更せざるを得ないおそれがあるのであれば、片方に依存しているオブジェクトがあります。
書籍の通りに下のようなプログラムを考えてみましょう。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @wheel = Wheel.new(rim, tire) end def ratio chainring / cog.to_f end def gear_inches ratio * wheel.diameter end end class Wheel attr_reader :rim :tire def initialize(rim, tire) @rim = rim @tire = tire end def diameter rim + (tire * 2) end def circumference diameter * Math::PI end end @wheel = Wheel.new(10, 1.5) @gear = Gear.new(52, 11, @diameter)
このコードにおける依存関係は下の4つです。
- GearクラスはWheelと言う名前のクラスが存在することを知っている(前提に作られている)
- GearはWheelのインスタンスが
diameter
に応答することを知っている - GearはWheel.newにrimとtireが必要であることを知っている
- さらにWheel.newに必要な引数の順番についても知っている
先ほどの画像の通り、GearはWheelに依存しています。
Wheelが変更されたら、Gearも変更しなくてはならないかもしれないコードです。
依存関係自体は別に悪
と言うものではありません。
不必要な依存関係を取り除いていくことが、今回のテーマになります。
オブジェクト間の結合(CBO)
先ほどの例だと、GearがWheelのクラス・メソッドの構造を知れば知るほど、
両者の結合度は高いコードとなります。
Wheelの機能を少し変えたいだけなのに、Gearも変えないといけない
状況が発生してくると言うことです。
単体テストの視点で言うと、Gearの機能をテストするだけなのに、同時にWheelの機能のテストにもなっている状況です。
(Whellの機能を変えた場合、Gear, Whellのテストコード, Gearのテストコードを変更しないといけないと言うことになりますね。)
また、Railsなんかでは離れた関係のオブジェクトをメソッドチェーン
でつなげていくことが
あると思います。
メソッドチェーン
は明らかな依存でありかつ、チェーンの途中経路のオブジェクトどれに変更があっても、
影響が及ぶ可能性があるので、かなり危険なものとなります。
疎結合なコード
先ほどまで書いた内容を避けるための方法としては、
疎結合なコード
を書くことです。(いや、急に言われても無理じゃね?と言う意見は置いといて)
依存関係を減らすためのテクニックは存在するんです。
依存オブジェクトを注入
先ほどのGearクラスが下のような場合だったと考えます。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @rim = rim @tire = tire end def gear_inches ratio * Wheel.new(rim, tire).diameter end end
gear_inches
メソッド内で、Wheelに関する処理がハードコーディングされています。
こういったハードコーディングは、GearとWheelがガチガチに結合している物になります。
後から、別なオブジェクトのdiameter
を経由してgear_inches
を求める必要がある場合、
拡張は不可能になってしまいます。
diameter
に反応できるオブジェクトを挿入することで、
ダックタイプで反応できるようにします。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, diameterable_object) @chainring = chainring @cog = cog @diameterable = diameter_object end def gear_inches ratio * diameterable.diameter end end # 外部からはdiameterメソッドを実装したオブジェクトを挿入する gear = Gear.new(10, 0.4, Wheel.new(10, 20)) puts gear.gear_inches
diameter
を知っているオブジェクト生成を外部に切り出すことで、
結合を切り離すことができます。
これは依存オブジェクトの挿入
というテクニックになります。
依存を隔離する
依存関係を取り除くことは技術的には可能かも知れませんが、
現実的に考えるとかなり難しい物になります。(特にRails使う場合なんかは、エンハンスで改善していくケースが多くなりますよね)
そういった場合は、完璧を目指さず、改善を目的とします。
依存している部分をクラス内のメソッドに閉じ込めてしまうことで、
依存している部分を最小限にすることができます。
先ほどのコードとほぼ同様ですが、下のコードの場合は考えます。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog wheel = Wheel.new(rim, tire) end def gear_inches ratio * wheel.diameter end end
初期化メソッドで、wheel
オブジェクトを生成しています。
特に問題が内容に見えますが、オブジェクトの生成をメソッド経由にしてみます。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @rim = rim @tire = tire end def gear_inches ratio * wheel.diameter end private def wheel @whell ||= Wheel.new(@rim, @tire) end end
特に変化がないように見えますが、Wheel
に依存していることが、
少しわかりやすくなったように見えます。
次は、外部メッセージ
を隔離してみましょう。
外部メッセージ
とは、self以外に送られるメッセージ
を示します。
gear_inches
メソッドにおいて、wheel
の直径を出していますが、
こいつをメソッドに切り出してあげます。
def gear_inches ratio * diameter end private def wheel @whell ||= Wheel.new(@rim, @tire) end def diameter wheel.diameter end
初っ端からこういった実装が必要になるわけではないのですが、
クラスの実装をメソッドに閉じ込めていますので、依存するメソッドも切り出してあげたほうがいいと言う感じになります。
引数の順番への依存を取り除く
こういったメソッドて結構書きますよね。
def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog wheel = Wheel.new(rim, tire) end
外部から呼び出される時に、引数の数と順番を知っている状態で呼び出すみたいなコードです。
メソッドで引数が追加されたり、削除された場合に、呼び出しもとを全て変更する必要が出てきます。
引数の受け取り方をHash
に変えることでこういったコードを改善することができます。
class Gear attr_reader :chainring, :cog, :wheel def initialize(args) @chainring = args[:chainring] @cog = args[:cog] @wheel = args[:wheel] end end gear = Gear.new( chainring: 52, cog: 11, wheel: Wheel.new(26, 11) )
コードの冗長性は増していますが、
引数の順番(index)よりも、キー名に依存させることで変更しやすいコードになりました。
依存方向の管理
先ほどコードで言う、GearがWheelに依存すべきか、
WheelがGearに依存すべきかを考えていく項目です。
依存方向を選択する場合には、変更されない物に依存する
ほうがいいです。
変更されない物に依存する
と言う言葉には、下の意味を包含しています。
- あるクラスは、他のクラスよりも要件がわかりやすい
- 要件が分かりやすいのであれば、そこまで変更される可能性は低そうですよね
- 具象クラスよりも抽象クラス
- 具象クラスは、独自メソッドも持っているので変更される可能性が高いです
- 多くの物に依存されたクラスを変更すると、アプリ全体に影響が生じる
- これは、大量に依存されたクラスはなるべく避けることが望ましいと言うことになります。
依存関係の分類
クラスを要件の変わりやすさ
とクラスに依存している物の数
の2軸で考えます。
各領域について整理すると、
- 抽象領域
- 大量のクラスに依存されているクラスはここに落ち着く
- 例えば抽象クラスとかインタフェース
- 中立領域#1
- 設計時にほとんど考慮する必要はない
- 中立領域2
- コード(要件)がとても変わりやすい
- ただ依存されている数は非常に少ないので、影響範囲は少ない
- 危険領域
- 変更のそれぞれが全てのクラスに影響する
- メンテすら難しい状態になる
通常は、抽象領域
と中立領域
に落ち着くべきです。
危険領域
に位置するクラスが存在した場合、アプリケーションのメンテはかなり
難しい物になります。
所感
概念的な物が多いので、かなり難しいです。
少しづつ業務とか個人開発に適用できるようにしていきます。
【Ruby】オブジェクト思考設計実践ガイドを読み始めた
オブジェクト思考設計ガイドを読み始めた
記事に書くこと
こちらの本を2章を読んだことの忘備録になります。
読み始めた背景
設計て大事ですよね。
なるべく早くアプリケーション開発するのは大事ですが、
設計がボロボロだった時に拡張ができないとか問題が発生します。
RubyとかRailsを少しは書けるようになってきましたが、
実装以外で迷うところはやはり設計です。
どこにコードを書くのか?、保守性が高いコードを書くためにはどのようなModelにするべきか、かなり迷います。
そんなこんなで、設計力を高めていきたいなと思いこちらの本を購入しました。
第2章 単一責任のクラスを設計する
先にまとめておくと
- クラスは今すぐに求められる動作を使い、後にも簡単に変更できるようにモデル化する
- そのためにメソッドを正しくグループ分けしてクラスにまとめることが必要
- 単一責任のクラスを作る
- クラスを変更する理由は2つ以上あってはならない
- クラスを1文で説明して、
and
とかor
がついたら複数の機能を持っている
- データを外部から隠蔽する
- データとしてではなく、アクセサメソッドを介して、外部から参照できるようにする
変更が簡単なコード
変更が簡単なコードって難しいですよね。
そもそも変更が簡単なコードって何だよと私もすごい思います。
書籍では変更が簡単
について以下のように定義されていました。
- 変更が副作用をもたらさない
- 要件の変更範囲とコードの変更規模は比例する
- 少しだけ変更するだけだったら、コードを変更する箇所も少しだよね的な
- 既存のコードは簡単に利用できる
- 何か追加の機能を開発する時に既存のコードを流用できるとか
- Railsだと
scope
とかが該当するのではないかと
- 最も簡単な変更方法はコードの追加である。ただし追加するコード自体は変更が容易なものとする
- 機能改修が多く、私もコードを追加することが多いのでとても染みた
じゃあ、上の定義を実現するためにどんなコードを書かないといけないのか?
下のTRUE(頭文字をとって)
がコードがあるべき姿になります。
- TransParent(見通しがいい)
- 変更がもたらす影響が明白であること
- コードが依存している場所、依存されている場所は明白であること
- Reasonable(合理的)
- どんな変更であっても、かかるコストは変更がもたらす利益に等しい
- Usable(利用性が高い)
- 新しい環境、予期していなかった環境でも利用できる
- Exemplary(模範的)
- コードに変更を加える人が、上記の品質を自然と保つようなコードになっている
- ある意味コードが、コーティング規約になっているみたいな感じ
TRUE
なコードを書くための最初の1歩が単一責任
を実現することになります。
単一責任って何?
オブジェクト思考とかで、よく言われる単一責任の原則
とかってありますよね。
単一責任
とは、
1つのクラスに1つの役割(機能)
と言うものになります。
1つのクラスに1つの役割って解釈がとても難しいですよね。
システムAとシステムBがあり、そこをつなげるクラスを作成していたとします。
作ったクラスが下のような、機能を持っている時これは単一責任でしょうか?
システムAから何かしらの命令を受けてそれをシステムBが処理できる形に変換する
変換した命令をシステムBへ通知する
システムAとシステムBの橋渡し(機能)と考えた場合って、単一責任
ですよね。
ただ、オブジェクト思考的には、これは単一責任とは言えません。
作成したクラスは、システムAから命令を受けて変換する機能
とシステムBに対して通知する機能
2つの機能が混在しているためです。
じゃあ、どうやってクラスが単一責任かチェックするのか。
作成したクラスを変更する理由が2つ以上存在してはならない
と言う意味に置き換えるてクラスの妥当性をチェックしていきます。
この意味で考えると、上のクラスが変更する理由としては、下のようなものです。
つまり、システムAから命令を受けて変換する機能
とシステムBに対して通知する機能
は
それぞれ別クラス(もしくはインタフェースを作成するなど)に分離する必要があります。
役割=クラスを変更する理由
と考えることで、単一責任になっているかを考えていくことができます。
なぜ単一責任が必要になるのか?
単一責任は、再利用が簡単なコードを書くための原則です。
2つ以上の責任を持っている場合、簡単に再利用ができなくなります。
(ある一方の機能のみ欲しいんだけど。。とりあえず別クラスに複製してみるかみたいな感じになりそうですよね)
必要な機能(振る舞い)のみで構成されているクラスは、クラス全体を複製することができるのです。
既存のコードが単一責任であるかどうかについて見極めるもう1つの方法としては、
コードの役割を1文で説明できるか
と言うことを意識してもいいようです。
説明に、それと
が含まれている場合は、2つ以上の責任を持っています。
または
がつく場合はそこまで関連していない2つ以上の責任を負っていると言うことになります。
データではなく振る舞いに依存させる
データ=クラスがもつ変数
、振る舞い=メソッド
です。
単一責任のクラスを作れば、どんな些細な振る舞いの変更も、
ただ1箇所のみに存在するようになります。
これがDRY
の概念とも被ります。
DRY
なコードは振る舞いに変更があったとしても、変更する箇所はただ1箇所になります。
メソッドもデータにアクセスする必要があります。
データにアクセスするための方法は、
があります。
業務要件として、データに直接アクセスする必要がある場合もありますが、
大抵の場合は、データをオブジェクトとして扱い、外部に対しては隠蔽
し、
外部からはアクセサメソッド経由でアクセス
する方が望ましいものとなります。
class Gear # Rubyのattr_readerでインスタンスパラメータをカプセル化(読み取り専用) attr_reader :chainring, :cog def initialize(chainring, cog) @chainring = chainring @cog = cog end def ratio chainring / cog.to_f end end # attr_readerは下のメソッドを定義したと同じ def cog @cog end # 外部からはデータを直接書き換えることはできない gear = Gear.new(10, 10) gear.cog = 20 # NG
Structを使ったデータの隠蔽
下のようなクラスとメソッドがあるとします。
Gearは外部から2次元配列をもらってインスタンス化されます。
class Gear attr_reader :data def initialize(data) @data = data end def diameters # 配列のindexを通してデータを処理 data.collect {|cell| cell[0] + (cell[1] * 2)} end # ここからも先のメソッドも配列のindexを通してデータを処理する # .... end data = [ [10, 20], [20, 30], [30, 40] ] gear = Gear.new(data)
dataはattr_reader
で確かに隠蔽されていますが、
data自体が複雑な構成になっていて、diametersメソッドは、
dataの構造に依存した処理になっています。
配列の構造が変わった瞬間に壊れる可能性があるコードです。
配列の構造を知っている場所をただ1箇所に抑え込む必要があります。
そんな時に構造体Struct
を使ってみます。
class Gear attr_reader :data def initialize(data) @wheels = wheelify(data) end def diameters @wheels.collect {|wheel| wheel.rim + (wheel.tire * 2)} end Wheel = Struct.new(:rim, :tire) def wheelify(data) data.collect {|cell| Wheel.new(data[0], data[1])} end end
配列の構造を知っている場所が、wheelify
だけになりました。
他のメソッドからWheelオブジェクトを介して、振る舞いが実装されています。
配列の構造が変わったとしても、wheelify
のみ変更すればいいコードになっています。
メソッドも単一責任にする
クラスと同様にメソッドも単一責任にすることで、
再利用しやすいものになります。
先ほどのコードのdiameters
メソッドに着眼します。
def diameters @wheels.collect {|wheel| wheel.rim + (wheel.tire * 2)} end
diameters
メソッドは、配列で持っているタイヤを順番に直径計算して、
直径の配列を返すメソッドになります。
直径の配列を返す
と直径を計算する
複数の機能を持っています。
直径計算を別なメソッドに切り出して、diameters
は下のようにリファクタリングされます。
def diameters @wheels.collect {|wheel| diamter(wheel.rim, wheel.tire)} end def diameter(rim, tire) rim + (tire * 2) end
クラス内の余計な責任を隔離する
先ほどStruct
を使って、Wheelを実現していました。
ただ、このモデル設計は違和感があります。
Gear
クラスを一文で説明した際に、
外部からデータをもらってWheelを生成する。それと、wheelの直径を計算して・・・
Gearの目的とは離れた役割が出てきました。
wheel
は外部にクラスとして分離していきます。
最終的には下のような構成にすることで、単一責任を実現しています。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, wheel = nil) @chainring = chainring @cog = cog @wheels = wheel end def ratio chainring / cog.to_f end def gear_inches ratio * wheel.diameter end end class Wheel attr_reader :rim :tire def initialize(rim, tire) @rim = rim @tire = tire end def diameter rim + (tire * 2) end def circumference diameter * Math::PI end end @wheel = Wheel.new(10, 1.5) @gear = Gear.new(52, 11, @diameter)
所感
今回のケースは、メッセージを取り合うクラスが2つしか存在しないので、
とてもシンプルな例だったと思います。
実際のアプリはもっと多くのクラスと依存関係をもちメッセージパッシングするので、
いつの間にか複数の役割を持っていたりします。
3章はその依存関係について記載されているみたいです。
(ここら辺の読書記録は2日に1回ペースであげたいけどなかなかできていない)
【Rails】 Formオブジェクトを使って、has_manyを一括登録する
だいぶサボりすぎてしまいました。
NuxtのAsyncDataに苦戦しまくっていて、記事を書くやる気がなくなっていました。
記事に書くこと
RailsのFormObjectを使って、has_many関係があるデータを一括で登録する方法を残します。
Todo登録アプリを想定して、下記のようなクラス間の関係があることを前提とします。
この時に、下記のような処理をFormObjectを利用して処理を実現したいと思います。
todoのcreateアクション(POST /todos/)のリクエストパラメータには、menuを生成するパラメータが存在する
todoを保存する際に、menuについても同時に保存する
なぜ記事を書くのか
FormObjectを使わなくても、Modelのrelationに
accepts_nested_attributes_for
を追加してあげればTodoと同時に関連するMenuも保存することができます。
最初はこちらの方法を利用していくつもりだったのですが、
どうやらRails界隈では評判がよろしく内容で(特にRailsの生みの親が消したいと言っている)
今後リファクタ要件になっていく可能性があります。
そのため、代替手段としてFormObjectを使って、今回はコードを書いていくことにしました。
# File app/models/todos.rb class Todo < ApplicationRecord # relation belongs_to :user has_many :menus accepts_nested_attributes_for :menus end # File app/models/menu.rb class Menu < ApplicationRecord # relation belongs_to :todo end
FormObjectって何?
単体のモデルに依存しない場合(複数モデルにまたがる処理など)や
フォーム専用の特別な処理をモデルに書きたくない場合に用いる1種のデザインパターンのようなものになります。
ActiveModel::Model
をincludeするので、バリデーション処理を記述することも可能になります。
使うと何が嬉しいのか
1つのフォームで、複数のModelを更新する場合、FormObjectを1つのModelとして扱うことができる
- 処理が分散しない
Fat ControllerやFat Modelになりにくくなる
DBに依存しないインスタンスでも、ActiveRecorと同じインタフェースを介してアクセスができる
実際に使ってみる
Form Objectを使わない場合の処理フロー
まずは比較のため、FormObjectを使わない場合の処理フローを考えていきます。 (設計くそ雑魚なのでそこら辺は置いといてください。。。)
TodoとMenuそれぞれを生成するため、コントローラ内の処理がとても多い印象です。
また、実際にはsave!
実行時にvalidate処理も必要になるのでそれらの処理をModelに記述する必要があります。
Form Objectを使った場合の処理フロー
Controllerがよしなに対応してくれた処理を、Form::Todo
に委譲させた処理フローが画像のような形になります。
処理フローが多くなっている気がしますが、コントローラーで行う処理としては、
Form::Todo
のnewForm::Todo
のsavesaveの結果に応じたレスポンスを返す
だけになっています。
FormObjectで実装する
設計方針
設計にあたって、TodoのFormObjectとMenuのFormObjectを作成します。
Form::TodoはForm::Menuとhas_manyの関係になっています。
Gem(Virtus)の導入
FormObjectの導入にあたって便利なVirtusを導入します。
DBに紐づかない属性をattribute
を使って便利に宣言できます。
Form::Todoの作成
attirbuteを使って、Formから渡されるパラメータを宣言します。
また、attribute :menus, Array[Form::Menu]
を記述して、複数のForm::Menu
を保持するようにしています。
ネストしたForm::Menu
についてはvalid
の対象外となってしまうため、
プライベートメソッドのcheck_menus_validation
でバリデーションをかけています。
class Form::Todo include Virtus.model include ActiveModel::Model attribute :name, String attribute :planning_date, DateTime attribute :clear_date, DateTime attribute :menus, Array[Form::Menu] validates :name, presence: true validates :planning_date, presence: true, on: :create validate :check_menus_validatation attr_accessor :user def initialize(current_user, params) super(params) self.user = current_user end def save if valid? persist! true else false end end private def persist! todo = Todo.new(user_id: user.id, name: name, planning_date: planning_date) todo_menus = menus.map do |menu| Menu.new(name: menu.name, set_count: menu.set_count, weight: menu.weight) end todo.menus = todo_menus todo.save! end def check_menus_validatation menus.each do |menu_form| errors.add(:base, menu_form.errors.full_messages) unless menu_form.valid? end throw(:abort) if errors.any? end end
Form::Menuの実装
Form::Menu
については、今のところ単体でNewされることは想定していないので、
フォームから渡されるパラメータとバリデーションのみ宣言しています。
class Form::Menu include Virtus.model include ActiveModel::Model attribute :name, String attribute :set_count, Integer attribute :weight, Integer validates :name, presence: true validates :set_count, presence: true, numericality: { greater_than: 0 } validates :weight, presence: true, numericality: { greater_than: 0 } end
コントローラの実装
上記の処理フローと同じように、Form::Todo
オブジェクト生成後にsaveをかけているだけです。
保存されたTodoの取得が少しナンセンスな気がしますが、N+1対策を含めてTodoオブジェクトかた直接検索をかけています。
通常のモデル単体と同じような記述量でTodoとMenuの保存ができるようになりました。
def create todo = Form::Todo.new(current_api_user, create_todo_params) if todo.save @todo = Todo.with_menus.where(user: current_api_user).first render :json => @todo, :serializer => TodoSerializer, status: :created else response_unprocessable(class_name = self, details = todo.errors.full_messages) end end private def create_todo_params params.require(:todos).permit(:name, :planning_date, menus: [:name, :set_count, :weight]) end
最後に
かなり一部分のみを切り取った記事になりましたが、
FormObjectを利用した複数モデルの登録について学ぶことができました。
今回、FormObjectのGemではVirtus
を利用しましたが、
Githubを見たところ数年近く更新されていないようでした。 (実装してから気づいてしまった)
他のGemを見たところ、
ActiveType
が良さげだなと思いましたので、こちらを利用した方法でリファクタをかけて行きたいと思います。
【忘備録】なんとなく使っているDBのindexについて復習した
世間的にはコロナが大流行していますね。
色々と話題がありますが、
東洋経済のDA(Data Analyst)さんがコロナの感染状況をきれいに可視化しているサービスがありました。
北海道が一番感染者数は多いのですが、地理的には近い東北はいまだに感染者数が0人。
こういう世間的なニーズをすぐにサービスに落とし込めるのはやっぱりすごいです。
今回記事に書くこと
DBのindex
についてです。
開発時にはRoR
を利用しているので、indexに関わるマイグレーションも基本的にはOR
経由で
DBに設定していきます。
今まで、なんとなく参照頻度が高くてデータ数が多いものについてはindex
を貼っていましたが、
コードレビューの際に少し議論になったので復習をしました。
参考にした書籍はこちらです。
DBのindexって何?
Cなどをいじっている人には馴染み深いものですが、
プログラム的に例えると(x, α)
形式の配列になります。(x=キー値, α=データへのポインタ)
index
というテーブルとは独立したオブジェクトがデータの並び順を担保することで、
探したいデータにたどり着くまでのパフォーマンスを向上させることができます。
indexを使うと何が嬉しいのか
データに見つけ出すパフォーマンス改善はもちろんですが、
下のようなメリットが挙げられます
メリット1 アプリケーション透過的
改善するための影響範囲がDBだけに留まるという意味になります。
indexを使う際には、DB側にindexを貼るだけなのでアプリケーション側で必要な操作というものがありません。
もう一つのパフォーマンス改善の手段である正規化
はテーブル設計が変更となるので、
アプリケーション側の改修(SQLクエリやリレーションなど)が必要になってきます。
ちなみにアプリケーションから見た際に、indexの存在を気にしないで実装ができることを
アプリケーション透過的
と言うそうです。
メリット2 データ透過的
indexはテーブルとは完全に独立したオブジェクトとして保存されます。
そのためindexを追加しても、テーブルやデータを変更する必要はないと言うことになります。
データから見ても、indexの存在は気にしないで設計・実装ができるのでデータ透過的
となります。
B-treeインデックスについて
indexとは言っても複数種類があるようですが、
基本的にはB-tree
が利用されているようです。
ケースバイケースとなるようですが、B-tree
は下の5つの評価点のバランスがよく秀才型となります。
- 均一性
- 各キー値の間で検索速度のばらつきが少ない
- 持続性
- データ量の増加に対してパフォーマンス低下が少ない
- 処理汎用性
- CRUDのいずれの処理もそこそこに速い
- 非等値性
=
に限らず不等号を利用した処理もそこそこに速い
B-treeの構造
名前の通りですが、木構造でデータを保持します。
下の図で例えば5
のデータ見るける場合は、
ルートノード
→6
→5
の順序でデータを見つけにいきます。
B-treeの優れているところ
均一性
B-treeは平衡木になります。
平衡木とは、どのリーフもルートからの距離が均一になる木のことを挿します。
上の図だとどのリーフも深さが3
ですよね。
同じ深さの場合、どのデータに対してもたどり着く計算量が同じと言うことになります。
そのため、均一性
のパフォーマンスが優れていると訳です。
ただし、長期的に運用していくことでどうしてもB-treeのバランスが悪くなることがあるようなので、
定期的に、indexを再編成することが必要になります。
持続性
長期的に運用していくとB-treeでもバランスが悪くなりますが、
それでも性能劣化(計算量が増える)は非常に緩やかになります。
下の図の通りではありますが、計算量としてはO(log n)
になります。(nはデータ量)
処理汎用性
B-treeはCRUDのどの操作においても、計算量はO(log n)
になります。
非等値性
特定のノードよりも左みたいな形で探索範囲を絞ることができるので、
不等号(<, >, <=, >=)やBETWEEN
による範囲検索の条件に対しても、パフォーマンスを発揮します。
ただし、否定条件についてはB-treeによる範囲の絞り込みができなくなるので、
パフォーマンスは発揮されません。
B-treeインデックスをどのように設計していくか
B-treeはアプリケーションのパフォーマンスを向上させる便利な機能です。
とはいえ、全部に付けておけばいいと言うものではありません。
前述の通り、テーブルとは異なるオブジェクトとして保存されるためHDのデータ量も喰うことになります。
本には、3つほどの指針が掲示されていました。
- 大規模なテーブルに対して作成する
- カーディナリティの高い列に作成する
- SQLでWHEREの選択条件もしくは結合条件に利用される列に作成する
上2つの指針については、少し詳しく説明します。
最後の奴は頻繁に参照とかで使われる物を選びましょうと言う感じです。
指針1 大規模なテーブルに対して作成する
先ほどの計算量のグラフと同じではありますが、
あるデータ量までは、フルスキャンの方が計算量が少ない領域があります。
データ量が少ない領域なのでそこまで大差は出ないのですが、無駄なindexを張らないためにもこの考え方は重要です。
大体1万件以下の場合は、indexの性能がそこまで発揮されるものでは無いようです。
指針2 カーディナリティの高い列に作成する
カーディナリティ
は特定の列の値がどれぐらいの種類の多さをもつかと言うことを表す概念です。
下の図のように、カーディナリティが高い列(=種類が多い)列を選ぶことが基本とされています。
特定のキー値を指定した時に、全体のレコード数の5%程度に絞り込めるだけのカーディナリティがあること目安とされています。
また、複合インデックスを貼る場合には、先頭に近いキーのカーディナリティが高いほど
パフォーマンスを発揮しやすいとされています。
B-treeのindexによる効果が期待できないSQL
CRUD全てにおいてある程度のパフォーマンスが期待できると書きましたが、
SQLの種類によってはパフォーマンスが期待できないことがあります。
index列で演算を行う
indexで作成された列はSQLにおいてはそのまま
で用いるのが原則です。
そもそも、演算された値はindexに保管されている訳では無いのでパフォーマンスは発揮されません。
-- こう言うのはパフォーマンスを発揮しない SELECT * FROM shohin WHERE col_1 * 1.1 > 100; --SQL組み込み関数も同様 SELECT * FROM shoshin WHERE SUBSTR(col_1, 1, 1) = 'a';
IS NULL述語
NULL許容の列から値がnull
の行のみを取得したい場合があるかと思います。
その時に下記のようなSQLを発行すると思いますが、これはindexのパフォーマンスは発揮されないSQLになります。
B-treeはnullについてはデータの値みなさず、indexに保持しないようにしているからです。
-- NOT NULLも同様 SELECT * from shohin WHERE col_1 IS NULL;
否定形
前述ではありますが、<>
を用いた否定形も
検索範囲が広すぎるためindexのパフォーマンスは発揮されません。
OR
これとても驚いたのですが、OR
を使った場合にindexは利用されないようです。
これの回避策として、IN
を利用することでindexを利用できる形式になるようです。
-- これはindexが使われない SELECT * FROM shohin WHERE col_1 = 10 OR col_1 = 11; -- INを使った場合はindexが使われる SELECT * FROM shohin WHERE col_1 IN (10, 11)
後方一致と中間一致のLIKE
LIKEを利用した場合、後方一致
と中間一致
については、
indexが利用されません。
ただし前方一致
についてはindexが利用されます。(確かに範囲検索が前方一致の場合はできそう)
最後に
概要レベルではありますが、DBのindexについて理解が深まった気がします。
RoRなんかを使っていると、形式的にindexを貼ったりしがちなので
indexについてもリファクタ対象として改修していきたいと思います。