頑張るときはいつも今

自称エンジニアのゴリラです。日々精進。

【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というモジュールを作成しています。

モジュールをUserProductクラスがincludeして、メソッドを利用しています。

UserProductのように、継承関係が結びつくものではない、ただログ出力という共通の役割(ロール)を持っている時に、

モジュールを利用することで、それぞれのクラスに共通の振る舞いを実装し、多重継承のような実装が可能になります。

モジュールの使う用途としては下記のような感じです。

  • 継承を使わずにクラスにインスタンスメソッドを追加する、もしくは上書きする(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のような機能を利用したRailsconcernsなんかもアプリの可読性が低くなってしまうこともあるため、

便利なのですが使う際には少し考える必要があるようです。

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つ目のメッセージは、おそらくメソッドのことかなと思うのですが、どうなんでしょうか??

インタフェースを理解する

下の図のように、オブジェクト同士のメッセージのやりとりが構成されているとします。

f:id:wa_football_1120:20200408183716j:plain

この時、パターンAとパターンBどちらがより良い設計でしょうか。

パターンAは、任意のオブジェクトが任意のオブジェクトに対してメッセージを送れるような構成になっています。

あるオブジェクトを変更するとアプリケーション全体に影響がかかる、あまりよろしくない設計です。

対してパターンBは、メッセージのパターンに何らかの制約があるように見受けられます。

パターンAのようなアプリケーションにならないために、

何をどのように外部に晒すのか?と言うことについて考える必要があります。

インタフェースを定義する

インタフェースには、

  • 外部からメッセージを受け取ることができるパブリックインタフェース

  • 内部のみで利用するプライベートインタフェース

があります。

これらのインタフェースについて考えていきます。

パブリックインタフェース

クラスが外部に公開するメソッド(パブリックインタフェース)は次の特性を備えます

  • クラスの主要な責任を明らかにする
  • 外部から実行されることが想定される
  • 気まぐれに変更されない
  • 他者がそこに依存しても安全
  • テストで完全に文書化されている

テストで完全に文書化されているってなるほどなと思います。

Railsでもどの程度メソッドをテスト対象に含めるべきかって、最初の方迷いがちだったので、

1種の判断材料になりますよね。

下にも書いてありますが、プライベートインタフェースだからテスト対象に入らないという訳ではないので、

ある程度の規模感と複雑性を持ってテストをすべきなのでしょうか。

プライベートインタフェース

クラス内のみで使われるメソッド(プライベートインタフェース)は次の特性を備えます

  • 実装の詳細に関わる
  • 他のオブジェクトから送られてくることを想定しない
  • どんな理由でも変更され得る
  • 他社がそこに依存することは危険
  • テスト項目に入らないこともある

ドメインオブジェクト

一応、書内では仮想的な呼び方になっています。

よく分からないですが、DDDとかだとまた違う解釈になっているのだと思います。

ドメインオブジェクトの特徴としては、

  • データ振る舞いの両方を兼ね備えた名詞
    • 例えば、ユーザ、記事とか
  • 永続化する

ドメインオブジェクトとして管理すべき物は、アプリケーション設計で比較的簡単に見つかりますが、

そこにこだわりすぎると、無理な振る舞いをしてしまいがちです。

オブジェクト間のメッセージに着目してここら辺の振る舞いを改善していくことが大事らしいです。

クラス図に執着するよりも、シーケンス図に目を向けるといような感じでしょうか。

「どのように」を伝えるのではなく「何を」を頼む

パブリックインタフェースが小さいと言うことは、他のところから依存されるメソッドが僅かしかないことを意味する

依存関係について学んだ際に、変更される可能性が低いもの依存すると言うセオリーがありました。

クラスも同様で、あるオブジェクトが依存するパブリックのインタフェースはなるべく変更されないように実装しなければいけません。

(と言うより、突然メソッド名変えましたって言われたら確かに誰でもキレそうだな〜)

下のシーケンスを考えてみます。

TripオブジェクトからMechanicオブジェクトに対して、bikeを引数にいくつかの処理を加えています。

この場合、bikeに対して処理が追加された場合に、同じような処理を呼び出している箇所全てを呼び出す必要があります。

f:id:wa_football_1120:20200408183828p:plain

この複数の処理を、Mechanicオブジェクトに責務を持たせるようにします。

f:id:wa_football_1120:20200408183853p:plain

このようにすることで、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で引用を書く方法分からん。)

プログラムの要素が依存する物といえば、多くの場合は以下のような物を指す事が多いと思います。

  • 参照している他の構成要素(引数や他のクラスなど)
  • 使用しているライブラリやフレームワーク

f:id:wa_football_1120:20200405102210j:plain

前回の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軸で考えます。

f:id:wa_football_1120:20200405102309j:plain

各領域について整理すると、

  • 抽象領域
    • 大量のクラスに依存されているクラスはここに落ち着く
    • 例えば抽象クラスとかインタフェース
  • 中立領域#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への通知プロトコルが変更されるかもしれない

つまり、システム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登録アプリを想定して、下記のようなクラス間の関係があることを前提とします。

f:id:wa_football_1120:20200322141416p:plain

この時に、下記のような処理を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に記述する必要があります。

f:id:wa_football_1120:20200322144236p:plain

Form Objectを使った場合の処理フロー

Controllerがよしなに対応してくれた処理を、Form::Todoに委譲させた処理フローが画像のような形になります。

f:id:wa_football_1120:20200322145554p:plain

処理フローが多くなっている気がしますが、コントローラーで行う処理としては、

  • Form::Todoのnew

  • Form::Todoのsave

  • saveの結果に応じたレスポンスを返す

だけになっています。

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人。

こういう世間的なニーズをすぐにサービスに落とし込めるのはやっぱりすごいです。

toyokeizai.net

今回記事に書くこと

DBのindexについてです。

開発時にはRoRを利用しているので、indexに関わるマイグレーションも基本的にはOR経由で

DBに設定していきます。

今まで、なんとなく参照頻度が高くてデータ数が多いものについてはindexを貼っていましたが、

コードレビューの際に少し議論になったので復習をしました。

参考にした書籍はこちらです。

達人に学ぶDB設計 徹底指南書

DBのindexって何?

Cなどをいじっている人には馴染み深いものですが、

プログラム的に例えると(x, α)形式の配列になります。(x=キー値, α=データへのポインタ)

indexというテーブルとは独立したオブジェクトがデータの並び順を担保することで、

探したいデータにたどり着くまでのパフォーマンスを向上させることができます。

f:id:wa_football_1120:20200229120130j:plain

indexを使うと何が嬉しいのか

データに見つけ出すパフォーマンス改善はもちろんですが、

下のようなメリットが挙げられます

メリット1 アプリケーション透過的

改善するための影響範囲がDBだけに留まるという意味になります。

indexを使う際には、DB側にindexを貼るだけなのでアプリケーション側で必要な操作というものがありません。

もう一つのパフォーマンス改善の手段である正規化はテーブル設計が変更となるので、

アプリケーション側の改修(SQLクエリやリレーションなど)が必要になってきます。

ちなみにアプリケーションから見た際に、indexの存在を気にしないで実装ができることを

アプリケーション透過的と言うそうです。

メリット2 データ透過的

indexはテーブルとは完全に独立したオブジェクトとして保存されます。

そのためindexを追加しても、テーブルやデータを変更する必要はないと言うことになります。

データから見ても、indexの存在は気にしないで設計・実装ができるのでデータ透過的となります。

B-treeインデックスについて

indexとは言っても複数種類があるようですが、

基本的にはB-treeが利用されているようです。

ケースバイケースとなるようですが、B-treeは下の5つの評価点のバランスがよく秀才型となります。

  • 均一性
    • 各キー値の間で検索速度のばらつきが少ない
  • 持続性
    • データ量の増加に対してパフォーマンス低下が少ない
  • 処理汎用性
    • CRUDのいずれの処理もそこそこに速い
  • 非等値性
    • =に限らず不等号を利用した処理もそこそこに速い

B-treeの構造

名前の通りですが、木構造でデータを保持します。

下の図で例えば5のデータ見るける場合は、

ルートノード65の順序でデータを見つけにいきます。

f:id:wa_football_1120:20200229122149j:plain

B-treeの優れているところ

均一性

B-treeは平衡木になります。

平衡木とは、どのリーフもルートからの距離が均一になる木のことを挿します。

上の図だとどのリーフも深さが3ですよね。

同じ深さの場合、どのデータに対してもたどり着く計算量が同じと言うことになります。

そのため、均一性のパフォーマンスが優れていると訳です。

ただし、長期的に運用していくことでどうしてもB-treeのバランスが悪くなることがあるようなので、

定期的に、indexを再編成することが必要になります。

持続性

長期的に運用していくとB-treeでもバランスが悪くなりますが、

それでも性能劣化(計算量が増える)は非常に緩やかになります。

下の図の通りではありますが、計算量としてはO(log n)になります。(nはデータ量)

f:id:wa_football_1120:20200229161151j:plain

処理汎用性

B-treeはCRUDのどの操作においても、計算量はO(log n)になります。

非等値性

特定のノードよりも左みたいな形で探索範囲を絞ることができるので、

不等号(<, >, <=, >=)やBETWEENによる範囲検索の条件に対しても、パフォーマンスを発揮します。

ただし、否定条件についてはB-treeによる範囲の絞り込みができなくなるので、

パフォーマンスは発揮されません。

B-treeインデックスをどのように設計していくか

B-treeはアプリケーションのパフォーマンスを向上させる便利な機能です。

とはいえ、全部に付けておけばいいと言うものではありません。

前述の通り、テーブルとは異なるオブジェクトとして保存されるためHDのデータ量も喰うことになります。

本には、3つほどの指針が掲示されていました。

  • 大規模なテーブルに対して作成する
  • カーディナリティの高い列に作成する
  • SQLでWHEREの選択条件もしくは結合条件に利用される列に作成する

上2つの指針については、少し詳しく説明します。

最後の奴は頻繁に参照とかで使われる物を選びましょうと言う感じです。

指針1 大規模なテーブルに対して作成する

先ほどの計算量のグラフと同じではありますが、

あるデータ量までは、フルスキャンの方が計算量が少ない領域があります。

データ量が少ない領域なのでそこまで大差は出ないのですが、無駄なindexを張らないためにもこの考え方は重要です。

大体1万件以下の場合は、indexの性能がそこまで発揮されるものでは無いようです。

f:id:wa_football_1120:20200229162155j:plain

指針2 カーディナリティの高い列に作成する

カーディナリティは特定の列の値がどれぐらいの種類の多さをもつかと言うことを表す概念です。

下の図のように、カーディナリティが高い列(=種類が多い)列を選ぶことが基本とされています。

特定のキー値を指定した時に、全体のレコード数の5%程度に絞り込めるだけのカーディナリティがあること目安とされています。

f:id:wa_football_1120:20200229162657j:plain

また、複合インデックスを貼る場合には、先頭に近いキーのカーディナリティが高いほど

パフォーマンスを発揮しやすいとされています。

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についてもリファクタ対象として改修していきたいと思います。