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