頑張るときはいつも今

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

【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を使うタイミングは少し分かったような、まだ分かっていないような。。