오늘 포스트에서는 Facade라는 소프트웨어 디자인 패턴에 대해 알아볼 것입니다. 처음 채택했을 때는 조금 어색한 느낌이 들었지만, Rails 앱에서 사용할수록 그 유용성에 감사하기 시작했습니다. 더 중요한 것은 내 코드를 더 철저하게 테스트하고, 컨트롤러를 정리하고, 내 보기 내의 논리를 줄이고, 애플리케이션 코드의 전체 구조에 대해 더 명확하게 생각할 수 있다는 것입니다.
소프트웨어 개발 패턴이기 때문에 파사드는 프레임워크에 구애받지 않지만 여기에서 제공할 예제는 Ruby on Rails에 대한 것입니다. 그러나 이 기사를 읽고 사용 중인 프레임워크에 관계없이 시도해 볼 것을 권장합니다. 이 패턴에 익숙해지면 코드베이스의 많은 부분에서 사용할 수 있는 기회가 생기기 시작할 것입니다.
더 이상 고민하지 말고 바로 들어가 봅시다!
MVC 패턴의 문제
MVC(Model-View-Controller) 패턴은 1970년대로 거슬러 올라가는 소프트웨어 개발 패턴입니다. 소프트웨어 인터페이스를 설계하기 위해 전투 테스트를 거친 솔루션으로, 고유한 방식으로 서로 통신하는 세 가지 주요 그룹으로 프로그래밍 문제를 분리합니다.
MVC 패턴을 기반으로 하는 많은 대형 웹 프레임워크가 2000년대 초반에 등장했습니다. Spring(Java용), Django(Python용) 및 Ruby on Rails(Ruby용)는 모두 핵심에서 상호 연결된 요소의 삼위일체로 위조되었습니다. MVC 패턴은 그것을 사용하지 않은 소프트웨어로 인한 스파게티 코드와 비교할 때 소프트웨어 개발과 인터넷 모두의 진화에서 큰 성과이자 전환점이었습니다.
본질적으로 Model-View-Controller 패턴은 다음을 허용합니다. 사용자가 View에서 작업을 수행합니다. 보기는 잠재적으로 모델을 생성/읽기/업데이트 또는 삭제할 수 있는 컨트롤러에 대한 요청을 트리거합니다. 모델 트랜잭션은 컨트롤러에 다시 응답하고 컨트롤러는 사용자가 보기에 반영된 일부 변경 사항을 렌더링합니다.
이 프로그래밍 패턴에는 많은 장점이 있습니다. 일부를 나열하려면:
- 관심 사항을 분리하여 코드 유지 관리성을 향상시킵니다.
- 테스트 가능성을 높일 수 있습니다(모델, 뷰 및 컨트롤러를 개별적으로 테스트할 수 있음)
- SOLID의 단일 책임 원칙("클래스는 변경해야 하는 이유는 단 하나여야 함")을 적용하여 우수한 코딩 관행을 장려합니다.
당시로서는 경이적인 성과였던 개발자들은 곧 MVC 패턴도 다소 제한적이라는 사실을 깨달았습니다. HMVC(계층적 모델-뷰-컨트롤러), MVA(모델-뷰-어댑터), MVP(모델-뷰-프레젠터), MVVM(모델-뷰-뷰모델) 등과 같은 변종들이 등장하기 시작했습니다. MVC 패턴의 한계를 해결합니다.
MVC 패턴이 소개하는 문제 중 하나이자 오늘 기사의 주제는 다음과 같습니다. 누가 복잡한 보기 논리를 처리하는 책임자입니까? 뷰는 단순히 데이터를 표시하는 데 관심을 가져야 하고 컨트롤러는 모델에서 받은 메시지를 중계할 뿐이며 모델은 뷰 로직에 관심을 두어서는 안 됩니다.
이 일반적인 문제를 해결하기 위해 모든 Rails 애플리케이션은 helpers
로 초기화됩니다. 예배 규칙서. helper
디렉토리는 복잡한 보기 로직을 지원하는 메소드가 있는 모듈을 포함할 수 있습니다.
다음은 Rails 애플리케이션 내의 도우미 예입니다.
app/helpers/application_helper.rb
module ApplicationHelper
def display_ad_type(advertisement)
type = advertisement.ad_type
case type
when 'foo'
content_tag(:span, class: "foo ad-#{type}") { type }
when 'bar'
content_tag(:p, 'bar advertisement')
else
content_tag(:span, class: "badge ads-badge badge-pill ad-#{type}") { type }
end
end
end
이 예는 간단하지만 복잡성을 줄이기 위해 템플릿 자체에서 이러한 종류의 의사 결정을 추출하려는 사실을 보여줍니다.
도우미도 좋지만 몇 년 동안 받아들여진 복잡한 View 로직을 처리하기 위한 또 다른 패턴이 있습니다. 바로 Facade 패턴입니다.
파사드 패턴 소개
Ruby on Rails 애플리케이션에서 파사드는 일반적으로 app/facades
내에 배치됩니다. 디렉토리.
helper
와 유사하지만 , facades
모듈 내의 메소드 그룹이 아닙니다. Facade는 컨트롤러 내에서 인스턴스화되지만 정교한 View 비즈니스 로직을 처리하는 PORO(Plain Old Ruby Object)입니다. 따라서 다음과 같은 이점이 있습니다.
UsersHelper
용 단일 모듈을 사용하는 것보다 또는ArticlesHelper
또는BooksHelper
, 각 컨트롤러 작업에는 고유한 Facade가 있을 수 있습니다.Users::IndexFacade
,Articles::ShowFacade
,Books::EditFacade
.- 파사드는 모듈뿐만 아니라 단일 책임 원칙이 시행되도록 하기 위해 파사드를 중첩할 수 있도록 하여 좋은 코딩 방법을 권장합니다. 수백 레벨 깊이로 중첩된 파사드를 원하지 않을 수도 있지만 유지 관리 및 테스트 범위를 개선하기 위해 하나 또는 두 개의 중첩 레이어를 갖는 것이 좋습니다.
다음은 인위적인 예입니다.
module Books
class IndexFacade
attr_reader :books, :params, :user
def initialize(user:, params:)
@params = params
@user = user
@books = user.books
end
def filtered_books
@filtered_books ||= begin
scope = if query.present?
books.where('name ILIKE ?', "%#{query}%")
elsif isbn.present?
books.where(isbn: isbn)
else
books
end
scope.order(created_at: :desc).page(params[:page])
end
end
def recommended
# We have a nested facade here.
# The `Recommended Books` part of the view has a
# single responsibility so best to extract it
# to improve its encapsulation and testability.
@recommended ||= Books::RecommendedFacade.new(
books: books,
user: user
)
end
private
def query
@query ||= params[:query]
end
def isbn
@isbn ||= params[:isbn]
end
end
end
파사드 패턴을 사용하지 않는 경우
또한 어떤 파사드가 그렇지 않은지 잠시 생각해 봅시다.
-
Facade는 예를 들어
lib
에 있는 클래스에 배치해서는 안 됩니다. 보기에 표시해야 하는 코드의 디렉터리입니다. 파사드의 수명 주기는 컨트롤러 작업에서 생성되어야 하며 연결된 뷰에서 사용해야 합니다. -
파사드는 비즈니스 로직이 CRUD 작업을 수행하는 데 사용되지 않습니다(서비스 또는 인터랙터와 같은 다른 패턴이 있지만 다른 날의 주제입니다). 다시 말해 파사드는 생성과 관련해서는 안 됩니다. 업데이트 또는 삭제. 그들의 목표는 View 또는 Controller에서 복잡한 프리젠테이션 로직을 추출하고 모든 정보에 액세스할 수 있는 단일 인터페이스를 제공하는 것입니다.
-
마지막으로 Facades는 은색 총알이 아닙니다. 그들은 MVC 패턴을 우회하는 것을 허용하지 않지만 오히려 함께 플레이합니다. Model에서 변경 사항이 발생하면 View에 즉시 반영되지 않습니다. MVC의 경우 항상 그렇듯이 Facade가 View에 변경 사항을 표시하려면 컨트롤러 작업을 다시 렌더링해야 합니다.
컨트롤러 혜택
Facade의 주요하고 분명한 이점 중 하나는 컨트롤러 로직을 극적으로 줄일 수 있다는 것입니다.
컨트롤러 코드는 다음과 같이 축소됩니다.
class BooksController < ApplicationController
def index
@books = if params[:query].present?
current_user.books.where('name ILIKE ?', "%#{params[:query]}%")
elsif params[:isbn].present?
current_user.books.where(isbn: params[:isbn])
else
current_user.books
end
@books.order(created_at: :desc).page(params[:page])
@recommended = @books.where(some_complex_query: true)
end
end
이에:
class BooksController < ApplicationController
def index
@index_facade = Books::IndexFacade.new(user: current_user, params: params)
end
end
혜택 보기
보기의 경우 Facades를 사용할 때 두 가지 주요 이점이 있습니다.
- 조건부 검사, 인라인 쿼리 및 기타 논리를 템플릿 자체에서 깔끔하게 추출하여 코드를 훨씬 더 읽기 쉽게 만들 수 있습니다. 예를 들어 다음과 같은 형식으로 사용할 수 있습니다.
<%= f.label :location %>
<%= f.select :location, options_for_select(User::LOCATION_TYPES.map { |type| [type.underscore.humanize, type] }.sort.prepend(['All', 'all'])), multiple: (current_user.active_ips.size > 1 && current_user.settings.use_multiple_locations?) %>
다음과 같이 될 수 있습니다.
<%= f.label :location %>
<%= f.select :location, options_for_select(@form_facade.user_locations), multiple: @form_facade.multiple_locations? %>
<올 시작="2"> // Somewhere in the view, a query is performed.
<% current_user.books.where(isbn: params[:isbn]).each do |book| %>
// Do things
<% end %>
// Somewhere else in the view, the same query is performed again.
<% current_user.books.where(isbn: params[:isbn]).each do |book| %>
// Do things
<% end %>
다음이 될 것입니다:
// Somewhere in the view, a query is performed.
<% @index_facade.filtered_books.each do |book| %>
// Do things
<% end %>
// Somewhere else in the view.
// Second query is not performed due to instance variable caching.
<% @index_facade.filtered_books.each do |book| %>
// Do things
<% end %>
테스트 이점
Facades의 주요 이점은 전체 컨트롤러 테스트를 작성하지 않고도 비즈니스 로직의 단일 비트를 테스트할 수 있다는 것입니다. 데이터 프레젠테이션은 예상대로입니다.
단일 PORO를 테스트할 것이기 때문에 빠른 테스트 제품군을 유지하는 데 도움이 됩니다.
다음은 데모 목적으로 Minitest로 작성된 테스트의 간단한 예입니다.
require 'test_helper'
module Books
class IndexFacadeTest < ActiveSupport::TestCase
attr_reader :user, :params
setup do
@user = User.create(first_name: 'Bob', last_name: 'Dylan')
@params = {}
end
test "#filtered_books returns all user's books when params are empty"
index_facade = Books::IndexFacade.new(user: user, params: params)
expectation = user.books.order(created_at: :desc).page(params[:page])
# Without writing an entire controller test or
# integration test, we can check whether using the facade with
# empty parameters will return the correct results
# to the user.
assert_equal expectation, index_facade.filtered_books
end
test "#filtered_books returns books matching a query"
@params = { query: 'Lord of the Rings' }
index_facade = Books::IndexFacade.new(user: user, params: params)
expectation = user
.books
.where('name ILIKE ?', "%#{params[:query]}%")
.order(created_at: :desc)
.page(params[:page])
assert_equal expectation, index_facade.filtered_books
end
end
end
단위 테스트 외관은 테스트 스위트 성능을 상당히 향상시키며, 이러한 문제가 어느 정도 심각하게 해결되지 않는 한 모든 대기업은 결국 느린 테스트 스위트에 직면하게 될 것입니다.
하나의 파사드, 두 개의 파사드, 세 개의 파사드, 그 이상?
View가 일부 데이터를 출력하는 부분을 렌더링하는 시나리오가 발생할 수 있습니다. 이 경우 부모 파사드를 사용하거나 중첩 파사드를 사용할 수 있습니다. 이는 얼마나 많은 로직이 관련되어 있는지, 별도로 테스트할 것인지, 기능을 추출하는 것이 합리적인 것인지에 따라 크게 달라집니다.
얼마나 많은 파사드를 사용할지 또는 얼마나 많은 파사드가 서로 중첩되어야 하는지에 대한 황금률은 없습니다. 그것은 개발자의 재량에 달려 있습니다. 저는 일반적으로 컨트롤러 작업에 대해 단일 파사드를 사용하는 것을 선호하며 코드를 더 쉽게 따라갈 수 있도록 중첩을 단일 수준으로 제한합니다.
다음은 개발 중에 스스로에게 물어볼 수 있는 몇 가지 일반적인 질문입니다.
- 파사드가 뷰에 제시하려는 논리를 캡슐화합니까?
- 파사드 내의 방법이 이 맥락에서 의미가 있습니까?
- 지금 코드를 따라가기 더 쉽습니까, 아니면 더 어렵습니까?
확신이 서지 않으면 항상 코드를 가능한 한 쉽게 따라할 수 있도록 노력하십시오.
결론
결론적으로, 파사드는 컨트롤러와 뷰를 간결하게 유지하면서 코드 유지 관리, 성능 및 테스트 가능성을 향상시키는 환상적인 패턴입니다.
그러나 모든 프로그래밍 패러다임과 마찬가지로 만병통치약은 없습니다. 최근 몇 년 동안 등장한 수많은 패턴(HMVC, MVVM 등)조차도 소프트웨어 개발의 복잡성에 대한 만능 솔루션이 아닙니다.
닫힌 시스템의 엔트로피 상태가 항상 증가한다는 열역학 제2법칙과 유사하게, 모든 소프트웨어 프로젝트에서도 시간이 지남에 따라 복잡성이 증가하고 진화합니다. 장기적으로 목표는 가능한 한 읽고, 테스트하고, 유지하고, 따르기 쉬운 코드를 작성하는 것입니다. 파사드가 바로 이것을 제공합니다.
추신 Ruby Magic 게시물이 언론에 공개되는 즉시 읽고 싶다면 Ruby Magic 뉴스레터를 구독하고 게시물을 놓치지 마세요!