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

Railsアプリをサービスオブジェクトでリファクタリングする

サービスオブジェクトは、単一のアクションを実行するRubyオブジェクトです。ドメインまたはビジネスロジックのプロセスをカプセル化します。架空のライブラリアプリケーションで本のインスタンスを作成する必要があると想像してください。プレーンなRailsアプリでは、次のようにします。

class BookController < ApplicationController
  def create
    Book.new(*args)
  end
end

これは単純なことには問題ありません。ただし、アプリが大きくなるにつれて、アプリを囲む多くの定型文になってしまう可能性があります:

class BookController < ApplicationController
 def create
    default_args = { genre: find_genre(), author: find_author() }
    Book.new(attrs.merge(default_args))
 end

 private

 def find_genre
   // ...
 end

  def find_author
   // ...
 end
end

サービスオブジェクトを使用すると、この動作を別のクラスに抽象化できます。その後、コードは再び単純になります:

class BookController < ApplicationController
  def
    BookCreator.create_book
  end
end
サービスオブジェクトが必要な理由

Railsは、MVC(モデル、コントローラー、ビュー、ヘルパーなど)の組織構造をネイティブにサポートするように設計されています。この構造は、単純なアプリケーションに適しています。ただし、アプリケーションが大きくなるにつれて、モデルとコントローラー全体にドメイン/ビジネスロジックが散らばっているのが見える場合があります。このようなロジックは、コントローラーにもモデルにも属していないため、コードの再利用と保守が困難になります。 Railsサービスオブジェクトは、ビジネスロジックをコントローラーやモデルから分離するのに役立つパターンであり、モデルを単純なデータレイヤーにし、コントローラーをAPIへのエントリポイントにすることができます。

ビジネスロジックをカプセル化するサービスを導入すると、次のような多くのメリットが得られます。

  • LeanRailsコントローラー -コントローラーは、要求を理解し、要求パラメーター、セッション、およびCookieを、動作するサービスオブジェクトに渡される引数に変換することのみを担当します。次に、コントローラーはサービスの応答に従ってリダイレクトまたはレンダリングします。大規模なアプリケーションでも、サービスオブジェクトを使用するコントローラーアクションは通常、10行以内のコードです。

  • テスト可能なコントローラー -コントローラーは無駄がなく、サービスの共同作業者として機能するため、特定のアクションが発生したときにコントローラー内の特定のメソッドが呼び出されたかどうかしか確認できないため、テストが非常に簡単になります。

  • ビジネスプロセスを個別にテストする機能 -サービスは、環境から分離された小さなRubyオブジェクトであるため、テストが簡単で高速です。すべての共同作業者を簡単にスタブし、サービス内で特定の手順が実行されているかどうかのみを確認できます。

  • 再利用可能なサービス -サービスオブジェクトは、コントローラー、他のサービスオブジェクト、キューに入れられたジョブなどから呼び出すことができます。

  • フレームワークとビジネスドメインの分離 -Railsコントローラーはサービスのみを認識し、それらを使用してドメインオブジェクトと対話します。この結合の減少により、特にモノリスからマイクロサービスに移行する場合に、スケーラビリティが容易になります。最小限の変更で、サービスを簡単に抽出して新しいサービスに移動できます。

サービスオブジェクトの作成

まず、架空のライブラリ管理アプリケーション用のapp/servicesという新しいフォルダに新しいBookCreatorを作成しましょう。

$ mkdir app/services && touch app/services/book_creator.rb

次に、すべてのロジックを新しいRubyクラス内にダンプしましょう:

# app/services/book_creator.rb
class BookCreator
  def initialize(title:, description:, author_id:, genre_id:)
    @title = title
    @description = description
    @author_id = author_id
    @genre_id = genre_id
  end

  def create_book
    Boook.create!(
    title: @title
    description: @description
    author_id: @author_id
    genre_id: @genre_id
    )
    rescue ActiveRecord::RecordNotUnique => e
     # handle duplicate entry
    end
  end
end

次に、コントローラー内またはアプリケーション内の任意の場所でサービスオブジェクトを呼び出すことができます。

class BookController < ApplicationController
  def create
    BookCreator.new(title: params[:title], description: params[:description], author_id: params[:author_id], genre_id: params[:genre_id]).create_book
  end
end
サービスオブジェクトのシンタックスシュガー

BookCreator.new(arguments).createを簡略化できます BookCreatorをインスタンス化するクラスメソッドを追加してチェーンします createを呼び出します 私たちのための方法:

# app/services/book_creator.rb
class BookCreator
  def initialize(title:, description:, author_id:, genre_id:)
    @title = title
    @description = description
    @author_id = author_id
    @genre_id = genre_id
  end

  def call(*args)
    new(*args).create_book
  end

  private

  def create_book
    Boook.create!(
    title: @title
    description: @description
    author_id: @author_id
    genre_id: @genre_id
    )
    rescue ActiveRecord::RecordNotUnique => e
     # handle duplicate entry
    end
  end
end

コントローラでは、ブックの作成者を次のように呼び出すことができます。

class BookController < ApplicationController
  def create
    BookCreator.call(
    title: params[:title],
    description: params[:description],
    author_id: params[:author_id],
    genre_id: params[:genre_id])
  end
end

コードをDRY(Do n't Repeat Yourself)のままにして、この動作を他のサービスオブジェクトで再利用するために、callを抽象化できます。 ベースのApplicationServiceへのメソッド すべてのサービスオブジェクトが継承するクラス:

class ApplicationService
  self.call(*args)
      new(*args).call
  end
end

このコードを使用して、BookCreatorをリファクタリングできます。 ApplicationServiceから継承します :

# app/services/book_creator.rb
class BookCreator < ApplicationService
  def initialize(title:, description:, author_id:, genre_id:)
    @title = title
    @description = description
    @author_id = author_id
    @genre_id = genre_id
  end

  def call
    create_book
  end

  private

  def create_book
    # ...
  end
end

BusinessProcessGemを使用したサービスオブジェクトの作成

BusinessProcess gemを使用すると、基本アプリケーションサービスクラスを作成したり、initializeを定義したりする必要はありません。 gemにはこれらすべての構成が組み込まれているためです。サービスオブジェクトは、BusinessProcess::Baseから継承する必要があります 。

gemファイルに次を追加します:

gem 'business_process'

次に、bundleを実行します 端末のコマンド

class BookCreator < BusinessProcess::Base
  # Specify requirements
  needs :title
  needs :description
   needs :author_id
    needs :genre_id

  # Specify process (action)
  def call
    create_book
  end

   private

  def create_book
    # ...
  end
end
優れたサービスオブジェクトを作成するためのガイド

1つのパブリックメソッド

サービスオブジェクトは、1つのビジネスアクションを実行し、それを適切に実行することになっているため、それを実行するための単一のパブリックメソッドのみを公開する必要があります。他のメソッドはプライベートで、パブリックメソッドによって呼び出される必要があります。名前がすべてのサービスオブジェクトで一貫している限り、パブリックメソッドに任意の名前を付けることができます。この例では、callという名前を付けています。 。他の従来の名前はperformです およびexecute

実行する役割に応じてサービスオブジェクトに名前を付ける

サービスオブジェクトの名前は、それが何をするかを示す必要があります。 「or」と「er」で終わる単語でサービスオブジェクトに名前を付ける一般的な方法があります。たとえば、サービスオブジェクトのジョブが本を作成することである場合は、BookCreatorという名前を付け、本を読むことがジョブである場合は、BookReaderという名前を付けます。

サービスオブジェクトを直接インスタンス化しないでください

構文糖衣パターンのような抽象化やBusinessProcessのような宝石を使用して、サービスオブジェクトの呼び出しの表記を短くします。このアプローチを使用すると、BookCreator.new(*args).callを簡素化できます。 またはBookCreator.new.call(*args) BookCreator.call(*args),に 短くて読みやすいです。

名前空間でサービスオブジェクトをグループ化する

特に大規模なアプリケーションでサービスオブジェクトを導入するということは、1つのサービスオブジェクトから数十のサービスオブジェクトに成長することを意味します。コード編成を改善するには、一般的なサービスオブジェクトを名前空間にグループ化することをお勧めします。たとえば、図書館のアプリケーションでは、すべての本関連のサービスをグループ化し、すべての著者関連のサービスを別の名前空間にグループ化します。フォルダ構造は次のようになります:

services
├── application_service.rb
└── book
├── book_creator.rb
└── book_reader.rb

サービスオブジェクトは次のようになります:

# services/book/book_creator.rb
module Book
  class BookCreator < ApplicationService
  ...
  end
end
# services/twitter_manager/book_reader.rb
module Book
  class BookReader < ApplicationService
  ...
  end
end

これで、呼び出しはBook::BookCreator.call(*args) and Book::BookReader.call(*args)になります。 。

サービスオブジェクトごとに1つの責任

複数のことを行うサービスオブジェクトを持つことは、サービスオブジェクトの「ビジネスアクション」の考え方に反します。従来、複数のアクションを実行する汎用サービスオブジェクトを使用することはお勧めしません。サービスオブジェクト間でコードを共有する場合は、ベースモジュールまたはヘルパーモジュールを作成し、ミックスインを使用してサービスオブジェクトに含めます。

例外をレスキューし、カスタム例外を発生させます

サービスオブジェクトの目的は、サードパーティのサービスやライブラリ間の相互作用やRails ActiveRecordによるデータベース操作など、実装の詳細をその中にカプセル化することです。 ActiveRecordとの対話中にActiveRecord::RecordNotUniqueなどのエラーが発生した場合、サービスは例外を適切にレスキューする必要があります。エラーがコールスタックに伝播することを許可しないでください。レスキューブロック内で処理できない場合は、そのサービスオブジェクトに固有のカスタム定義の例外を発生させます。

結論

サービスオブジェクトパターンは、アプリケーションに新しい機能を追加するときに、アプリケーションの全体的な設計を大幅に改善できます。これにより、コードベースがより表現力豊かになり、保守が容易になり、テストの負担が軽減されます。


  1. Rails5でのAngularの使用

    あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた

  2. これらのアプリを使用してカメラでオブジェクトを識別

    カメラ付きのスマートフォンは、写真をクリックするための従来のコンポーネントではなくなりました。 AR ステッカーを使って画像を面白くするなど、もっと魅力的なことを行うために使用されます .カメラを使用して顔を認識し、ジェスチャーで写真をクリックすることができます。同様に、スマートフォンのカメラでオブジェクトを識別できるようになりました。これは、カメラでクリックした被写体に関する情報を見つけるのに役立つ便利なテクニックの 1 つです。カメラでオブジェクトを識別するアプリについて説明しましょう: Google レンズ Google レンズは、この技術をサポートするすべての Android デバイ