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에 대한 단순한 데이터 계층 및 컨트롤러 진입점이 될 수 있도록 합니다.

다음을 포함하여 비즈니스 로직을 캡슐화하는 서비스를 도입하면 많은 이점을 얻을 수 있습니다.

  • Lean Rails 컨트롤러 - 컨트롤러는 요청을 이해하고 요청 매개변수, 세션 및 쿠키를 서비스 개체에 전달되어 작동하도록 인수로 전환하는 작업만 담당합니다. 그런 다음 컨트롤러는 서비스 응답에 따라 리디렉션하거나 렌더링합니다. 대규모 응용 프로그램에서도 서비스 개체를 사용하는 컨트롤러 작업은 일반적으로 10줄 이하의 코드입니다.

  • 테스트 가능한 컨트롤러 - 컨트롤러가 린(lean)이고 서비스의 협력자 역할을 하기 때문에 특정 액션이 발생했을 때 컨트롤러 내의 특정 메소드가 호출되는지 여부만 확인할 수 있으므로 테스트하기가 정말 쉽습니다.

  • 독립적으로 비즈니스 프로세스를 테스트하는 기능 - 서비스는 환경에서 분리된 작은 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(Don'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

BusinessProcess Gem을 사용하여 서비스 개체 만들기

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개

서비스 개체는 하나의 비즈니스 작업을 수행하고 잘 수행해야 하므로 이를 수행하기 위한 단일 공용 메서드만 노출해야 합니다. 다른 메소드는 private이어야 하고 public 메소드에 의해 호출되어야 합니다. 모든 서비스 개체에서 이름이 일관되는 ​​한 원하는 대로 공용 메서드의 이름을 지정할 수 있습니다. 이 예에서는 이름을 call로 지정했습니다. . 다른 일반적인 이름은 perform입니다. 및 execute .

수행하는 역할에 따라 서비스 개체 이름 지정

서비스 개체의 이름은 그것이 하는 일을 나타내야 합니다. "or"와 "er"로 끝나는 단어로 서비스 객체를 명명하는 일반적인 방법이 있습니다. 예를 들어, 서비스 개체의 작업이 책을 만드는 것이라면 이름을 BookCreator로 지정하고 책을 읽는 것이라면 BookReader로 이름을 지정하세요.

서비스 개체를 직접 인스턴스화하지 않음

문법적인 설탕 패턴과 같은 추상화나 BusinessProcess와 같은 보석을 사용하여 서비스 객체를 호출하는 표기법을 줄이십시오. 이 접근 방식을 사용하면 BookCreator.new(*args).call을 단순화할 수 있습니다. 또는 BookCreator.new.call(*args) BookCreator.call(*args),으로 더 짧고 읽기 쉽습니다.

네임스페이스의 그룹 서비스 개체

특히 대규모 응용 프로그램에서 서비스 개체를 도입한다는 것은 하나의 서비스 개체에서 수십 개의 서비스 개체로 성장할 것임을 의미합니다. 코드 구성을 개선하려면 공통 서비스 개체를 네임스페이스로 그룹화하는 것이 좋습니다. 예를 들어 도서관 애플리케이션에서 우리는 모든 책 관련 서비스를 함께 그룹화하고 모든 저자 관련 서비스를 별도의 네임스페이스에 그룹화합니다. 이제 폴더 구조는 다음과 같습니다.

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)이 됩니다. .

서비스 개체당 하나의 책임

하나 이상의 일을 하는 서비스 객체를 갖는 것은 서비스 객체의 "비즈니스 행동" 사고방식에 어긋납니다. 일반적으로 여러 작업을 수행하는 일반 서비스 개체를 사용하지 않는 것이 좋습니다. 서비스 개체 간에 코드를 공유하려면 기본 또는 도우미 모듈을 만들고 믹스인을 사용하여 서비스 개체에 포함하세요.

예외를 구조하고 사용자 정의 예외를 발생

서비스 객체의 목적은 타사 서비스 또는 라이브러리 간의 상호 작용 또는 Rails ActiveRecord를 사용한 데이터베이스 조작과 같은 구현 세부 정보를 내부에 캡슐화하는 것입니다. ActiveRecord와 상호 작용하는 동안 ActiveRecord::RecordNotUnique와 같은 오류가 발생하면 서비스에서 예외를 적절히 복구해야 합니다. 호출 스택을 전파하는 데 오류가 허용되어서는 안 됩니다. 구조 블록 내에서 처리할 수 없는 경우 해당 서비스 개체와 관련된 사용자 정의 예외를 발생시킵니다.

결론

서비스 객체 패턴은 애플리케이션에 새로운 기능을 추가할 때 애플리케이션의 전체 디자인을 크게 향상시킬 수 있습니다. 코드베이스를 보다 표현력 있고 유지 관리하기 쉽고 테스트하기가 덜 고통스럽게 만들 것입니다.