Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Ruby on Railsアプリケーションでモジュールとミックスインを使いこなす方法

モジュールとミックスインは、Rubyを魅力的な言語にしている代表的な機能です。これらを活用すれば、アプリケーションのどこからでも再利用できるコードを手軽に共有できるようになり、機能や関心事ごとにコードをグループ化することで、可読性と保守性の向上にも大きく貢献します。

この記事では、モジュールとミックスインの背後にある概念を解説し、モジュールの作成方法や他のクラスへの組み込み方、そしてRuby on Railsアプリケーションでこれらを活用するメリットについて学んでいきます。

モジュールとは

モジュールは、Rubyの最も優れた機能の一つです。主な利点として、次の2つが挙げられます。

  • 名前の衝突を防ぐための名前空間を作成できる
  • ミックスインとしてアプリケーション全体でコードを共有できる

構造的には、モジュールはRubyのクラスとよく似ています。実際、RubyにとってClassModuleでもあります。irbコンソールで以下のように確認できます。

> Class.is_a?(Module)
 => true

クラスと同じように、モジュールでもメソッドや定数をグループ化してコードを共有できます。ただし、モジュールと通常のRubyクラスにはいくつかの違いがあります。

  • 定義はclassではなくmoduleキーワードで始める
  • モジュールはインスタンス化できないため、そこからオブジェクトを生成できない
  • モジュールから継承することはできず、代わりにミックスインとして使う
  • モジュールは独立したコードであり、継承階層を持たない

モジュールは、サービスオブジェクト、concerns(関心事)、定数など、同じ責任を持つコードをひとまとめにしておくのに最適な場所です。

モジュールの基本的な構造は次のようになります。

# lib/modules/invoice_creator.rb
module InvoiceCreator
  TAX_FEE = 0.5
 
  def self.generate
    puts "Don't worry! I'll generate the invoice for you at #{TAX_FEE}%"
  end
 
  def invoice_total
    puts "I'll return the invoice total"
    1000
  end
end

この例からわかるように、モジュールには2種類のメソッドを定義できます。それはモジュールメソッドインスタンスメソッドです。

self.generateはモジュールメソッドです。これは、他のオブジェクトにinclude(またはextend)しなくても直接使用できることを意味します。サービスオブジェクトを作成する際によく使われるパターンです。モジュールメソッドは次のように呼び出します。

2.5.3 :006 > InvoiceCreator.generate
Don't worry! I'll generate the invoice for you
=> nil

一方、invoice_totalはインスタンスメソッドです。こちらを使用するには、次のようにクラスにモジュールをincludeする必要があります。

# app/models/invoice.rb
class Invoice < ApplicationRecord
  include InvoiceCreator # Invoiceクラスにモジュールを取り込む
 
  def calculate_tax
    total = invoice_total # モジュールから取り込んだメソッド
    tax = total * InvoiceCreator::TAX_FEE
    puts "This is the invoice tax: #{tax}"
  end
end

これにより、InvoiceCreatorのすべてのインスタンスメソッドがInvoiceのインスタンスで利用可能になり、calculate_taxメソッドを簡単に呼び出せるようになります。

2.5.3 :008 > Invoice.new.calculate_tax
"I'll return the invoice total"
"This is the invoice tax: 500.0"
 => nil
2.5.3 :009 >

さらに、calculate_taxの中ではモジュール内で定義された定数を使用しています。前述の通り、モジュールは定数を管理するのに最適な場所なのです。

ここで、サプライヤー向けと顧客向けにまったく異なる請求書を生成するために、2種類のInvoiceCreatorが必要になったシナリオを想像してみてください。同名のクラスが衝突してしまうため、これを避けるためにモジュールのもう一つの大きな利点である「名前空間」を活用します。次のセクションで見ていきましょう。

名前空間の活用

名前空間とは、特定の機能に対してローカルなコンテキストを作りたいときにコードを整理するための仕組みです。先ほどのシナリオ、つまり顧客向けの請求書生成とサプライヤー向けの請求書生成に、それぞれ別のコンテキストが必要になる場合にこそ力を発揮します。

コードに戻りましょう。2つの異なるモジュールを作成します。

# lib/modules/customer/invoice_creator.rb
module Customer
  module InvoiceCreator
    def self.generate
      puts "Don't worry! I'll generate the customer invoice for you"
    end
  end
end
 
# lib/modules/supplier/invoice_creator.rb
module Supplier
  module InvoiceCreator
    def self.generate
      puts "Don't worry! I'll generate the supplier invoice for you"
    end
  end
end

あとは、必要な場所でCustomer::InvoiceCreatorまたはSupplier::InvoiceCreatorを使用するだけです。

2.5.3 :014 > Customer::InvoiceCreator.generate
Don't worry! I'll generate the customer invoice for you
 => nil
2.5.3 :015 > Supplier::InvoiceCreator.generate
Don't worry! I'll generate the supplier invoice for you
 => nil
2.5.3 :016 >

このようにすることで、それぞれのコードが独自のモジュール内にカプセル化され、「関心の分離」の原則が守られます。加えて、名前空間を適切に使うことは、コードベースを整理された状態に保つための優れた手法でもあります。

続いて、Rubyモジュールのもう一つの利点である「ミックスイン」の使い方を見ていきましょう。

ミックスインの魔法

ご存知の通り、Rubyは単一継承の仕組みを採用しており、1つのクラスが継承できるのは1つのクラスだけです。しかし、開発を進めていると複数のクラスから継承したい場面に遭遇することは少なくありません。Rubyでは、このニーズに「継承より合成(composition over inheritance)」のパターンで対応できます。

それを実現するのがミックスインです。あるRubyクラスにコードをミックスインすると、継承を使わずにそのクラスへ新しい振る舞いを追加できます。これは非常に強力な仕組みです。つまり、Rubyにおけるミックスインとは、必要なクラスにincludeされるモジュールのことです。これにより、コードをクリーンに保ちながら、責任を適切に分離できます。

例えば、InvoiceCreatorモジュールが、InvoiceCalculatorInvoiceRendererInvoiceSenderといった複数のモジュールが提供する機能を必要とするサービスだとしましょう。これらは請求書作成プロセスを完了するために不可欠なものです。

これは、モジュール群をミックスインとして連ねてincludeすることで実現でき、各メソッドを直接呼び出せるようになります。以下の例をご覧ください。

# lib/modules/invoice_calculator.rb
module InvoiceCalculator
  def calculate_items_total
    puts "imagine some math here"
  end
end
 
# lib/modules/invoice_renderer.rb
module InvoiceRenderer
  def generate_invoice_pdf
    puts "imagine that we are using some PDF generation magic here"
  end
end
 
# lib/modules/invoice_sender.rb
module InvoiceSender
  def send_invoice
    puts "imagine your favorite mail service being used here"
  end
end

InvoiceCreatorは他の3つのモジュールすべてから継承することができないため、代わりにincludeします。こうすることで、InvoiceCreatorは他のInvoiceモジュールのすべてのメソッドを取り込み、InvoiceCreatorモジュールがincludeされている任意のクラスやモジュールからこれらのメソッドを呼び出せるようになります。ただし注意点もあります。複数のモジュールに同名のメソッドが存在する場合、それらは互いに上書きされてしまうのです。

# lib/modules/customer/invoice_creator.rb
module Customer
  module InvoiceCreator
    include InvoiceCalculator
    include InvoiceRenderer
    include InvoiceSender
 
    def generate_invoice
      calculate_items_total # InvoiceCalculatorから
      generate_invoice_pdf # InvoiceRendererから
      send_invoice # InvoiceSenderから
    end
  end
end

これで、このモジュールをincludeした任意の場所でサービスのメソッドを呼び出せるようになりました。

# app/models/invoice.rb
class Invoice < ApplicationRecord
  include Customer::InvoiceCreator
 
  def send_invoice_to_customer
    puts "Don't worry! I'll generate the customer invoice for you"
    generate_invoice
  end
end

includeしたInvoiceモジュールのメソッドをInvoiceCreatorモジュール経由で呼び出すと、次のような結果になります。

2.5.3 :051 > Invoice.new.generate_invoice
Don't worry! I'll generate the supplier invoice for you
imagine some math here
imagine that we are using some PDF generation magic here
imagine your favorite mail service being used here
 => nil

お気づきかもしれませんが、これこそが「継承より合成」の原則の実践です。この原則では、可能な限り継承ではなく合成を選ぶべきだとされています。合成では、特定の機能を他のクラスへ提供することに特化した部品(ここではモジュール)を作成します。まさに私たちが行っていることです。

なお、RailsではActiveSupport::Concernという仕組みも用意されており、includedブロックや依存関係の管理などをより簡潔に記述できるため、実際のRailsアプリケーションでconcernsを作成する際にはこちらの活用も検討するとよいでしょう。

まとめ

Ruby開発者として、私たちはコードの設計、記述、保守、リファクタリングのあらゆる場面で、Rubyの便利な機能や糖衣構文を活用するのが大好きです。この記事を通じて、モジュールがコードの可読性向上、単一責任の保持、クリーンで保守しやすいコードベースの維持にどれほど役立つリソースであるかを実感していただけたなら幸いです。また、多重継承が必要になりそうな場面で、ミックスインの魔法を使ってモジュールをコードに組み込めることも理解していただけたでしょう。

今後もRubyとRailsの素晴らしい機能について発信していきますので、お楽しみに!

  1. RubyのStructとOpenStructの使い方を徹底解説!値オブジェクトを簡単に作る方法

    Rubyには、関連する属性をまとめて保持する「値オブジェクト」を手軽に作れるStructという組み込みクラスが用意されています。本記事では、Structの基本的な使い方から注意点、そしてOpenStructとの違いまで詳しく解説します。 そもそもStructとは何か? StructはRubyに組み込まれたクラスで、新しいクラスを動的に生成し、値オブジェクト(Value Object)を作成するために使われます。値オブジェクトとは、関連性のある複数の属性をひとまとめにして扱うためのオブジェクトです。 具体例を挙げてみましょう。 2つの座標(xとy)を持つPointというデータを表したい場合、この

  2. RailsアプリでTwitterログインを実装する方法:OmniAuth-Twitter完全ガイド

    このチュートリアルでは、RailsアプリケーションのユーザーがTwitterアカウントを使ってログインできるようにする方法を解説します。OAuthなどのツールのおかげで、この実装はとても簡単に行えるようになりました。 今回は、OmniAuth用のTwitterストラテジーを含む「OmniAuth-Twitter」を使用します。 それでは始めましょう! はじめに:プロジェクトの準備 まずはRailsアプリケーションを生成します。ターミナルで以下のコマンドを実行してください。 rails new Tuts-Social -T 次にGemfileを開き、bootstrap gemを追加します。