평범한 오래된 Ruby 객체로 세련된 Rails 구성 요소 구축
게시 됨: 2022-03-11귀하의 웹사이트는 인기를 얻고 있으며 빠르게 성장하고 있습니다. Ruby/Rails는 선택한 프로그래밍 언어입니다. 귀하의 팀은 더 크고 Rails 앱의 디자인 스타일로 "뚱뚱한 모델, 마른 컨트롤러"를 포기했습니다. 그러나 여전히 Rails 사용을 포기하고 싶지는 않습니다.
문제 없어요. 오늘 우리는 OOP의 모범 사례를 사용하여 코드를 더 깨끗하고, 더 격리되고, 더 분리되도록 만드는 방법에 대해 논의할 것입니다.
앱이 리팩토링할 가치가 있습니까?
앱이 리팩토링에 적합한지 여부를 결정하는 방법을 살펴보는 것으로 시작하겠습니다.
다음은 내 코드에 리팩토링이 필요한지 여부를 결정하기 위해 일반적으로 스스로에게 묻는 메트릭 및 질문 목록입니다.
- 느린 단위 테스트. PORO 단위 테스트는 일반적으로 잘 격리된 코드로 빠르게 실행되므로 느리게 실행되는 테스트는 종종 잘못된 디자인과 과도하게 결합된 책임의 지표가 될 수 있습니다.
- FAT 모델 또는 컨트롤러. 200줄 이상의 코드(LOC)가 있는 모델 또는 컨트롤러는 일반적으로 리팩토링에 적합한 후보입니다.
- 지나치게 큰 코드 기반. 30,000개 이상의 LOC가 있는 ERB/HTML/HAML 또는 50,000개 이상의 LOC가 있는 Ruby 소스 코드(GEM 제외)가 있는 경우 리팩토링해야 할 가능성이 큽니다.
다음과 같이 사용하여 Ruby 소스 코드가 몇 줄인지 알아보세요.
find app -iname "*.rb" -type f -exec cat {} \;| wc -l
이 명령은 /app 폴더에서 확장자가 .rb인 모든 파일(루비 파일)을 검색하고 줄 수를 출력합니다. 주석 줄이 이 합계에 포함되기 때문에 이 수치는 대략적인 수치일 뿐입니다.
더 정확하고 유익한 또 다른 옵션은 코드 줄, 클래스 수, 메서드 수, 메서드 대 클래스 비율, 메서드당 코드 줄 비율에 대한 빠른 요약을 출력하는 Rails rake 작업 stats
를 사용하는 것입니다.
bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
- 내 코드베이스에서 반복 패턴을 추출할 수 있습니까?
행동의 디커플링
실제 사례부터 시작하겠습니다.
조깅하는 사람들의 시간을 추적하는 애플리케이션을 작성한다고 가정해 봅시다. 기본 페이지에서 사용자는 입력한 시간을 볼 수 있습니다.
각 시간 항목에는 날짜, 거리, 기간 및 추가 관련 "상태" 정보(예: 날씨, 지형 유형 등) 및 필요할 때 계산할 수 있는 평균 속도가 있습니다.
주당 평균 속도와 거리를 표시하는 보고서 페이지가 필요합니다.
항목의 평균 속도가 전체 평균 속도보다 높으면 사용자에게 SMS로 알립니다(이 예에서는 Nexmo RESTful API를 사용하여 SMS를 보냅니다).
홈페이지에서 조깅에 소요된 거리, 날짜 및 시간을 선택하여 다음과 유사한 항목을 만들 수 있습니다.
또한 기본적으로 주당 평균 속도와 거리를 포함하는 주간 보고서인 statistics
페이지가 있습니다.
- 여기에서 온라인 샘플을 확인할 수 있습니다.
코드
app
디렉토리의 구조는 다음과 같습니다.
⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
User
모델은 인증을 구현하기 위해 Devise와 함께 사용하기 때문에 특별한 것이 아니기 때문에 논의하지 않겠습니다.
Entry
모델의 경우 애플리케이션에 대한 비즈니스 논리가 포함되어 있습니다.
각 Entry
은 User
에 속합니다.
각 항목에 대해 distance
, time_period
, date_time
및 status
속성이 있는지 확인합니다.
항목을 생성할 때마다 사용자의 평균 속도를 시스템의 다른 모든 사용자의 평균과 비교하고 Nexmo를 사용하여 SMS로 사용자에게 알립니다. 외부 라이브러리를 사용하는 경우를 보여주기 위해).
- 요지 샘플
Entry
모델에는 비즈니스 로직 이상의 것이 포함되어 있습니다. 또한 일부 유효성 검사 및 콜백을 처리합니다.
entries_controller.rb
에는 주요 CRUD 작업이 있습니다(업데이트 없음). EntriesController#index
는 현재 사용자에 대한 항목을 가져오고 생성된 날짜별로 레코드를 정렬하는 반면 EntriesController#create
는 새 항목을 생성합니다. EntriesController#destroy
의 명백한 책임과 책임에 대해 논의할 필요가 없습니다.
- 요지 샘플
statistics_controller.rb
가 주간 보고서를 계산하는 역할을 하는 반면, StatisticsController#index
는 로그인한 사용자에 대한 항목을 가져와 주별로 그룹화하여 Rails의 Enumerable 클래스에 포함된 #group_by
메소드를 사용합니다. 그런 다음 몇 가지 개인 방법을 사용하여 결과를 장식하려고 합니다.
- 요지 샘플
소스 코드가 자명하기 때문에 여기에서 보기에 대해 많이 논의하지 않습니다.
다음은 로그인한 사용자( index.html.erb
)에 대한 항목을 나열하는 보기입니다. 이것은 항목 컨트롤러에서 인덱스 작업(메소드)의 결과를 표시하는 데 사용되는 템플릿입니다.
- 요지 샘플
부분 render @entries
를 사용하여 공유 코드를 부분 템플릿 _entry.html.erb
로 가져와서 코드를 DRY로 유지하고 재사용할 수 있도록 하고 있습니다.
- 요지 샘플
_form
부분도 마찬가지입니다. (new 및 edit) 작업에 동일한 코드를 사용하는 대신 재사용 가능한 부분 형식을 만듭니다.
- 요지 샘플
주간 보고서 페이지 보기의 경우 statistics/index.html.erb
는 일부 통계를 보여주고 일부 항목을 그룹화하여 사용자의 주간 실적을 보고합니다.
- 요지 샘플
마지막으로 항목에 대한 도우미 항목인 entries_helper.rb
에는 속성을 사람이 더 읽기 쉽게 만들어야 하는 두 개의 도우미 readable_time_period
및 readable_speed
가 포함되어 있습니다.
- 요지 샘플
지금까지 멋진 것은 없습니다.
대부분의 사람들은 이것을 리팩토링하는 것이 KISS 원칙에 위배되고 시스템을 더 복잡하게 만들 것이라고 주장할 것입니다.
그렇다면 이 애플리케이션에 정말 리팩토링이 필요한가요?
절대 아닙니다 . 하지만 데모용으로만 고려할 것입니다.
결국, 이전 섹션과 앱이 리팩토링이 필요함을 나타내는 특성을 확인하면 이 예제의 앱이 리팩토링에 적합한 후보가 아님이 분명해집니다.
라이프 사이클
이제 Rails MVC 패턴 구조를 설명하는 것으로 시작하겠습니다.
일반적으로 브라우저가 https://www.toptal.com/jogging/show/1
과 같은 요청을 함으로써 시작됩니다.
웹 서버는 요청을 수신하고 routes
를 사용하여 사용할 controller
를 찾습니다.
컨트롤러는 사용자 요청, 데이터 제출, 쿠키, 세션 등을 구문 분석한 다음 model
에 데이터를 가져오도록 요청합니다.
models
은 데이터베이스와 통신하고, 데이터를 저장 및 검증하고, 비즈니스 로직을 수행하고, 그렇지 않으면 무거운 작업을 수행하는 Ruby 클래스입니다. 보기는 사용자가 보는 것입니다: HTML, CSS, XML, Javascript, JSON.
Rails 요청 라이프사이클의 시퀀스를 표시하려면 다음과 같이 보일 것입니다.
내가 달성하고자 하는 것은 PORO(Plain Old Ruby Objects)를 사용하여 더 많은 추상화를 추가하고 create/update
작업에 대해 다음과 같은 패턴을 만드는 것입니다.
list/show
작업의 경우 다음과 같습니다.
PORO 추상화를 추가함으로써 우리는 Rails가 잘하지 못하는 SRP 책임 간의 완전한 분리를 보장할 것입니다.
지침
새로운 디자인을 구현하기 위해 아래에 나열된 지침을 사용할 것이지만 T에서 따라야 하는 규칙은 아닙니다. 리팩토링을 더 쉽게 하는 유연한 지침으로 생각하십시오.
- ActiveRecord 모델은 연관 및 상수를 포함할 수 있지만 다른 것은 포함하지 않습니다. 즉, 콜백이 없고(서비스 개체를 사용하고 거기에 콜백을 추가) 유효성 검사가 없습니다(모델에 대한 이름 지정 및 유효성 검사를 포함하기 위해 Form 개체 사용).
- 컨트롤러를 얇은 레이어로 유지하고 항상 서비스 개체를 호출합니다. 로직을 포함하기 위해 서비스 객체를 계속 호출하기를 원하는데 왜 컨트롤러를 전혀 사용하지 않는지 묻는 분들이 계실 것입니다. 컨트롤러는 HTTP 라우팅, 매개변수 구문 분석, 인증, 콘텐츠 협상, 올바른 서비스 또는 편집기 개체 호출, 예외 포착, 응답 형식 지정, 올바른 HTTP 상태 코드 반환을 수행하기에 좋은 장소입니다.
- 서비스는 Query 개체를 호출해야 하며 상태를 저장해서는 안 됩니다. 클래스 메서드가 아닌 인스턴스 메서드를 사용합니다. SRP를 준수하는 공개 메서드는 거의 없어야 합니다.
- 쿼리는 쿼리 개체에서 수행되어야 합니다. 쿼리 개체 메서드는 ActiveRecord 연결이 아닌 개체, 해시 또는 배열을 반환해야 합니다.
- 도우미를 사용하지 말고 대신 데코레이터를 사용하세요. 왜요? Rails 도우미의 일반적인 함정은 모두 네임스페이스를 공유하고 서로를 밟고 있는 비 OO 기능의 큰 더미로 변할 수 있다는 것입니다. 그러나 훨씬 더 나쁜 것은 Rails 헬퍼와 함께 모든 종류의 다형성을 사용할 수 있는 좋은 방법이 없다는 것입니다. 다른 컨텍스트 또는 유형, 재정의 또는 하위 분류 헬퍼에 대해 서로 다른 구현을 제공합니다. Rails 도우미 클래스는 일반적으로 모든 종류의 프레젠테이션 논리에 대한 모델 속성 형식 지정과 같은 특정 사용 사례가 아니라 유틸리티 메서드에 사용해야 한다고 생각합니다. 가볍고 상쾌하게 유지하십시오.
- 우려 사항을 사용하지 말고 대신 데코레이터/위임자를 사용하세요. 왜요? 결국, 우려는 Rails의 핵심 부분인 것으로 보이며 여러 모델 간에 공유될 때 코드를 건조시킬 수 있습니다. 그럼에도 불구하고 주요 문제는 우려가 모델 개체를 더 응집력 있게 만들지 않는다는 것입니다. 코드가 더 잘 정리되었습니다. 즉, 모델의 API에는 실제 변경 사항이 없습니다.
- 코드를 더 깔끔하게 유지하고 관련 속성을 그룹화하려면 모델에서 값 개체를 추출해 보세요 .
- 항상 보기당 하나의 인스턴스 변수를 전달하십시오.
리팩토링
시작하기 전에 한 가지 더 논의하고 싶습니다. 리팩토링을 시작하면 대개 "정말 좋은 리팩토링인가?"라고 자문하게 됩니다.
책임 사이를 더 많이 분리하거나 격리하고 있다고 느낀다면(더 많은 코드와 새 파일을 추가하는 것을 의미하더라도) 일반적으로 이것은 좋은 일입니다. 결국 응용 프로그램을 분리하는 것은 매우 좋은 방법이며 적절한 단위 테스트를 더 쉽게 수행할 수 있습니다.
컨트롤러에서 모델로 로직을 이동하는 것과 같은 내용은 논의하지 않을 것입니다. 이미 그렇게 하고 있고 Rails(일반적으로 스키니 컨트롤러 및 FAT 모델)를 사용하는 데 익숙하기 때문입니다.
이 기사를 엄격하게 유지하기 위해 여기에서 테스트에 대해 논의하지 않겠지만 그렇다고 해서 테스트를 해서는 안 되는 것은 아닙니다.
반대로 진행하기 전에 항상 테스트를 시작하여 문제가 없는지 확인해야 합니다. 이것은 특히 리팩토링할 때 필수 입니다.
그런 다음 변경 사항을 구현하고 모든 테스트가 코드의 관련 부분에 대해 통과하는지 확인할 수 있습니다.
가치 객체 추출
첫째, 가치 객체란 무엇인가?
마틴 파울러는 다음과 같이 설명합니다.
값 개체는 돈 또는 날짜 범위 개체와 같은 작은 개체입니다. 그들의 주요 속성은 참조 의미론보다 값 의미론을 따른다는 것입니다.
때로는 개념 자체가 추상화되어야 하고 평등이 가치가 아니라 정체성을 기반으로 하는 상황에 직면할 수 있습니다. 예에는 Ruby의 날짜, URI 및 경로 이름이 포함됩니다. 값 개체(또는 도메인 모델)로 추출하는 것은 매우 편리합니다.
귀찮게 왜?
Value 개체의 가장 큰 장점 중 하나는 코드에서 달성하는 데 도움이 되는 표현력입니다. 코드는 훨씬 더 명확해지거나 최소한 좋은 명명 관행이 있는 경우 더 명확해질 것입니다. Value Object는 추상화이기 때문에 더 깔끔한 코드와 더 적은 오류로 이어집니다.
또 다른 큰 승리는 불변성입니다. 객체의 불변성은 매우 중요합니다. 값 개체에서 사용할 수 있는 특정 데이터 집합을 저장할 때 일반적으로 해당 데이터가 조작되는 것을 원하지 않습니다.
언제 유용합니까?
하나의 획일적인 정답은 없습니다. 주어진 상황에서 자신에게 가장 잘 맞는 일을 하십시오.
하지만 그 외에도 그러한 결정을 내리는 데 도움이 되는 몇 가지 지침이 있습니다.
메서드 그룹이 관련되어 있다고 생각하면 Value 개체가 더 많이 표현됩니다. 이 표현력은 Value 개체가 고유한 데이터 집합을 나타내야 한다는 것을 의미하며, 평균 개발자는 개체 이름을 보고 간단히 추론할 수 있습니다.
어떻게 합니까?
값 개체는 몇 가지 기본 규칙을 따라야 합니다.
- 값 개체에는 여러 속성이 있어야 합니다.
- 속성은 개체의 수명 주기 동안 변경할 수 없습니다.
- 평등은 객체의 속성에 의해 결정됩니다.
이 예에서는 Entry#status_weather
및 Entry#status_landform
속성을 자체 클래스로 추상화하는 EntryStatus
값 개체를 생성합니다. 이는 다음과 같습니다.
- 요지 샘플
참고: 이것은 ActiveRecord::Base
에서 상속되지 않는 PORO(Plain Old Ruby Object)일 뿐입니다. 속성에 대한 판독기 메서드를 정의했으며 초기화 시 할당합니다. 또한 (<=>) 메서드를 사용하여 객체를 비교하기 위해 비교 가능한 믹스인을 사용했습니다.
생성한 값 개체를 사용하도록 Entry
모델을 수정할 수 있습니다.
- 요지 샘플
새 값 개체를 적절하게 사용하도록 EntryController#create
메서드를 수정할 수도 있습니다.
- 요지 샘플
서비스 개체 추출
그렇다면 서비스 객체는 무엇입니까?
서비스 개체의 작업은 비즈니스 논리의 특정 비트에 대한 코드를 보유하는 것입니다. 소수의 개체에 필요한 모든 논리에 대한 수많은 메서드가 포함되어 있는 "지방 모델" 스타일과 달리 Service 개체를 사용하면 각각 단일 목적으로 사용되는 많은 클래스가 생성됩니다.
왜요? 혜택은 무엇입니까?
- 디커플링. 서비스 개체를 사용하면 개체 간의 더 많은 격리를 달성할 수 있습니다.
- 시계. 서비스 개체(이름이 잘 지정된 경우)는 응용 프로그램이 수행하는 작업을 보여줍니다. 서비스 디렉토리를 살펴보고 응용 프로그램이 제공하는 기능을 확인할 수 있습니다.
- 청소 모델 및 컨트롤러. 컨트롤러는 요청(매개변수, 세션, 쿠키)을 인수로 변환하여 서비스에 전달하고 서비스 응답에 따라 리디렉션하거나 렌더링합니다. 모델은 연관과 지속성만 취급합니다. 컨트롤러/모델에서 서비스 개체로 코드를 추출하면 SRP를 지원하고 코드가 더 분리됩니다. 그러면 모델의 책임은 연결 및 레코드 저장/삭제만 처리하는 반면 서비스 개체는 단일 책임(SRP)을 갖습니다. 이것은 더 나은 디자인과 더 나은 단위 테스트로 이어집니다.
- 건조하고 변화를 수용하십시오. 나는 가능한 한 간단하고 작게 서비스 객체를 유지합니다. 다른 서비스 개체와 서비스 개체를 구성하고 다시 사용합니다.
- 테스트 스위트를 정리하고 속도를 높이십시오. 서비스는 하나의 진입점(호출 방법)이 있는 작은 Ruby 개체이기 때문에 테스트하기 쉽고 빠릅니다. 복잡한 서비스는 다른 서비스와 함께 구성되어 있으므로 테스트를 쉽게 분할할 수 있습니다. 또한 서비스 객체를 사용하면 전체 레일 환경을 로드할 필요 없이 관련 객체를 더 쉽게 목/스텁할 수 있습니다.
- 어디서나 통화 가능. 서비스 객체는 다른 서비스 객체, DelayedJob / Rescue / Sidekiq Jobs, Rake 작업, 콘솔 등 뿐만 아니라 컨트롤러에서도 호출될 가능성이 높습니다.
반면에 완벽한 것은 없습니다. Service 개체의 단점은 매우 간단한 작업에 대해 과도할 수 있다는 것입니다. 이러한 경우 코드를 단순화하기보다는 복잡하게 만들 수 있습니다.

언제 서비스 개체를 추출해야 합니까?
여기에도 엄격하고 빠른 규칙은 없습니다.
일반적으로 서비스 개체는 중대형 시스템에 더 적합합니다. 표준 CRUD 작업을 넘어서는 상당한 양의 논리를 가진 사람들.
따라서 코드 조각이 추가하려는 디렉터리에 속하지 않을 수 있다고 생각될 때마다 다시 생각하고 서비스 개체로 이동해야 하는지 확인하는 것이 좋습니다.
다음은 서비스 개체를 사용할 시기를 나타내는 몇 가지 지표입니다.
- 동작이 복잡합니다.
- 작업은 여러 모델에 적용됩니다.
- 작업은 외부 서비스와 상호 작용합니다.
- 작업은 기본 모델의 핵심 관심사가 아닙니다.
- 작업을 수행하는 방법에는 여러 가지가 있습니다.
서비스 객체를 어떻게 디자인해야 할까요?
특별한 보석이 필요하지 않고 새로운 DSL을 배울 필요가 없으며 이미 보유하고 있는 소프트웨어 설계 기술에 어느 정도 의존할 수 있기 때문에 서비스 객체에 대한 클래스를 설계하는 것은 비교적 간단합니다.
나는 일반적으로 다음 지침과 규칙을 사용하여 서비스 개체를 디자인합니다.
- 객체의 상태를 저장하지 마십시오.
- 클래스 메서드가 아닌 인스턴스 메서드를 사용합니다.
- 매우 적은 수의 공개 메소드가 있어야 합니다(SRP를 지원하는 것이 바람직합니다.
- 메서드는 부울이 아닌 리치 결과 개체를 반환해야 합니다.
- 서비스는
app/services
디렉토리 아래에 있습니다. 비즈니스 로직이 많은 도메인에는 하위 디렉토리를 사용하는 것이 좋습니다. 예를 들어,app/services/report/generate_weekly.rb
파일은Report::GenerateWeekly
를 정의하는 반면app/services/report/publish_monthly.rb
는Report::PublishMonthly
를 정의합니다. - 서비스는 동사로 시작하고 서비스로 끝나지 않음:
ApproveTransaction
,SendTestNewsletter
,ImportUsersFromCsv
. - 서비스는 호출 방법에 응답합니다. 다른 동사를 사용하면 약간 중복됩니다. ApproveTransaction.approve()가 잘 읽히지 않습니다. 또한 호출 메서드는 람다, 프로시저 및 메서드 개체에 대한 사실상의 메서드입니다.
StatisticsController#index
를 보면 컨트롤러에 연결된 메서드 그룹( weeks_to_date_from
, weeks_to_date_to
, avg_distance
등)을 볼 수 있습니다. 정말 좋지 않습니다. statistics_controller
외부에서 주간 보고서를 생성하려면 결과를 고려하십시오.
이 경우 Report::GenerateWeekly
를 만들고 StatisticsController
에서 보고서 논리를 추출해 보겠습니다.
- 요지 샘플
따라서 StatisticsController#index
는 이제 더 깔끔하게 보입니다.
- 요지 샘플
서비스 객체 패턴을 적용함으로써 우리는 특정하고 복잡한 작업에 대한 코드를 묶고 더 작고 명확한 메소드의 생성을 촉진합니다.
숙제: Struct
대신 WeeklyReport
에 Value 개체를 사용하는 것을 고려하십시오 .
컨트롤러에서 쿼리 개체 추출
쿼리 개체란 무엇입니까?
Query 개체는 데이터베이스 쿼리를 나타내는 PORO입니다. 쿼리 논리를 숨기는 동시에 애플리케이션의 여러 위치에서 재사용할 수 있습니다. 또한 테스트하기에 좋은 격리된 단위를 제공합니다.
복잡한 SQL/NoSQL 쿼리를 자체 클래스로 추출해야 합니다.
각 쿼리 개체는 기준/비즈니스 규칙에 따라 결과 집합을 반환하는 역할을 합니다.
이 예에서는 복잡한 쿼리가 없으므로 Query 개체를 사용하는 것이 효율적이지 않습니다. 그러나 데모를 위해 Report::GenerateWeekly#call
에서 쿼리를 추출하고 generate_entries_query.rb
를 생성해 보겠습니다.
- 요지 샘플
그리고 Report::GenerateWeekly#call
에서 다음을 교체해 보겠습니다.
def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end
와 함께:
def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end
쿼리 개체 패턴은 모델 논리를 클래스의 동작과 엄격하게 관련시키는 동시에 컨트롤러를 슬림하게 유지하는 데 도움이 됩니다. 그것들은 평범한 오래된 Ruby 클래스에 지나지 않기 때문에 쿼리 객체는 ActiveRecord::Base
에서 상속할 필요가 없으며 쿼리 실행 외에는 아무 것도 책임져야 합니다.
서비스 개체에 대한 생성 항목 추출
이제 새 서비스 개체에 대한 새 항목을 만드는 논리를 추출해 보겠습니다. 규칙을 사용하여 CreateEntry
를 생성해 보겠습니다.
- 요지 샘플
이제 EntriesController#create
는 다음과 같습니다.
def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end
유효성 검사를 양식 개체로 이동
이제 여기 상황이 더 흥미로워지기 시작합니다.
우리 가이드라인에서 우리는 모델에 연관과 상수를 포함하기를 원했지만 다른 것은 포함하지 않기로 동의했습니다(검증 및 콜백 없음). 따라서 콜백을 제거하고 대신 Form 객체를 사용하여 시작하겠습니다.
Form 객체는 PORO(Plain Old Ruby Object)입니다. 데이터베이스와 통신해야 하는 곳이면 어디든지 컨트롤러/서비스 개체에서 인계합니다.
왜 Form 객체를 사용합니까?
앱을 리팩토링할 때 항상 단일 책임 원칙(SRP)을 염두에 두는 것이 좋습니다.
SRP는 클래스가 책임져야 하는 것에 대해 더 나은 디자인 결정을 내리는 데 도움이 됩니다.
예를 들어 데이터베이스 테이블 모델(Rails 컨텍스트의 ActiveRecord 모델)은 코드에서 단일 데이터베이스 레코드를 나타내므로 사용자가 수행하는 모든 작업과 관련될 이유가 없습니다.
이것은 Form 객체가 들어오는 곳입니다.
Form 개체는 응용 프로그램에서 양식을 나타내는 역할을 합니다. 따라서 각 입력 필드는 클래스의 속성으로 처리될 수 있습니다. 이러한 속성이 일부 유효성 검사 규칙을 충족하는지 확인할 수 있으며 "정리한" 데이터를 필요한 위치(예: 데이터베이스 모델 또는 검색 쿼리 빌더)로 전달할 수 있습니다.
Form 개체는 언제 사용해야 합니까?
- Rails 모델에서 유효성 검사를 추출하려는 경우.
- 단일 양식 제출로 여러 모델을 업데이트할 수 있는 경우 양식 개체를 만들 수 있습니다.
이렇게 하면 모든 양식 논리(이름 지정 규칙, 유효성 검사 등)를 한 곳에 넣을 수 있습니다.
Form 객체는 어떻게 만드나요?
- 일반 Ruby 클래스를 만듭니다.
-
ActiveModel::Model
포함(Rails 3에서는 대신 Naming, Conversion, Validations를 포함해야 함) - 새 양식 클래스를 일반 ActiveRecord 모델처럼 사용하기 시작합니다. 가장 큰 차이점은 이 개체에 저장된 데이터를 유지할 수 없다는 것입니다.
개혁 보석을 사용할 수 있지만 PORO를 사용하면 다음과 같은 entry_form.rb
가 생성됩니다.
- 요지 샘플
그리고 CreateEntry
를 수정하여 Form 개체 EntryForm
을 사용하기 시작합니다.
class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end
참고: 일부 사용자는 Service 개체에서 Form 개체에 액세스할 필요가 없으며 컨트롤러에서 직접 Form 개체를 호출할 수 있으며 이는 유효한 인수라고 말합니다. 그러나 나는 명확한 흐름을 선호하기 때문에 항상 Service 개체에서 Form 개체를 호출합니다.
콜백을 서비스 개체로 이동
앞서 동의한 것처럼 모델에 유효성 검사 및 콜백이 포함되는 것을 원하지 않습니다. Form 개체를 사용하여 유효성 검사를 추출했습니다. 그러나 우리는 여전히 일부 콜백을 사용하고 있습니다( Entry
모델 after_create
의 compare_speed_and_notify_user
).
모델에서 콜백을 제거하려는 이유는 무엇입니까?
Rails 개발자는 일반적으로 테스트 중에 콜백 문제를 감지하기 시작합니다. ActiveRecord 모델을 테스트하지 않는 경우 나중에 애플리케이션이 성장하고 콜백을 호출하거나 피하기 위해 더 많은 논리가 필요함에 따라 고통을 느끼기 시작할 것입니다.
after_*
콜백은 주로 객체를 저장하거나 유지하는 것과 관련하여 사용됩니다.
객체가 저장되면 객체의 목적(즉, 책임)이 달성됩니다. 따라서 객체가 저장된 후에도 콜백이 호출되는 것을 계속 볼 수 있다면 객체의 책임 영역 외부에 도달하는 콜백을 볼 수 있으며 이때 문제가 발생합니다.
우리의 경우 항목을 저장한 후 사용자에게 SMS를 보내고 있는데 항목의 도메인과 실제로 관련이 없습니다.
문제를 해결하는 간단한 방법은 콜백을 관련 서비스 개체로 이동하는 것입니다. 결국 최종 사용자에게 SMS를 보내는 것은 항목 모델 자체가 아니라 CreateEntry
서비스 개체와 관련이 있습니다.
그렇게 하면 더 이상 테스트에서 compare_speed_and_notify_user
메서드를 제거할 필요가 없습니다. SMS를 보낼 필요 없이 항목을 만드는 것을 간단한 문제로 만들었으며 클래스가 단일 책임(SRP)을 갖도록 함으로써 우수한 객체 지향 설계를 따르고 있습니다.
이제 CreateEntry
는 다음과 같습니다.
- 요지 샘플
도우미 대신 데코레이터 사용
뷰 모델 및 데코레이터의 Draper 컬렉션을 쉽게 사용할 수 있지만 지금까지 해왔듯이 이 기사에서는 PORO를 고수하겠습니다.
내가 필요한 것은 데코레이팅된 객체에서 메서드를 호출하는 클래스입니다.
method_missing
을 사용하여 구현할 수 있지만 Ruby의 표준 라이브러리 SimpleDelegator
를 사용합니다.
다음 코드는 SimpleDelegator
를 사용하여 기본 데코레이터를 구현하는 방법을 보여줍니다.
% app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end
그렇다면 왜 _h
메서드입니까?
이 메서드는 보기 컨텍스트에 대한 프록시 역할을 합니다. 기본적으로 보기 컨텍스트는 보기 클래스의 인스턴스이며 기본 보기 클래스는 ActionView::Base
입니다. 다음과 같이 보기 도우미에 액세스할 수 있습니다.
_h.content_tag :div, 'my-div', class: 'my-class'
더 편리하게 하기 위해 우리는 decorate
메소드를 ApplicationHelper
에 추가합니다:
module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end
이제 EntriesHelper
도우미를 데코레이터로 이동할 수 있습니다.
# app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end
그리고 다음과 같이 readable_time_period
및 readable_speed
를 사용할 수 있습니다.
# app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
- <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>
리팩토링 후 구조
우리는 더 많은 파일을 얻었지만 그것이 반드시 나쁜 것은 아닙니다(그리고 처음부터 이 예제가 단지 설명을 위한 것이며 반드시 리팩토링을 위한 좋은 사용 사례는 아니라는 것을 인정했음을 기억하십시오):
app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
결론
이 블로그 게시물에서 Rails에 중점을 두었지만 RoR은 설명된 서비스 개체 및 기타 PORO의 종속성이 아닙니다. 이 접근 방식은 모든 웹 프레임워크, 모바일 또는 콘솔 앱에서 사용할 수 있습니다.
MVC를 웹 앱의 아키텍처로 사용하면 대부분의 변경 사항이 앱의 다른 부분에 영향을 미치기 때문에 모든 것이 결합 상태를 유지하고 속도가 느려집니다. 또한 비즈니스 로직을 어디에 둘 것인지 생각해야 합니다. 모델, 컨트롤러 또는 뷰 중 어디에 넣어야 할까요?
간단한 PORO를 사용하여 비즈니스 로직을 ActiveRecord
에서 상속하지 않는 모델이나 서비스로 옮겼습니다. 이는 이미 큰 성과입니다. SRP와 더 빠른 단위 테스트를 지원하는 더 깨끗한 코드가 있다는 것은 말할 것도 없고요.
클린 아키텍처는 사용 사례를 구조의 중앙/상단에 배치하여 앱이 수행하는 작업을 쉽게 볼 수 있도록 하는 것을 목표로 합니다. 또한 훨씬 더 모듈화되고 격리되어 있으므로 변경 사항을 쉽게 적용할 수 있습니다.
Plain Old Ruby Objects와 더 많은 추상화를 사용하여 문제를 분리하고 테스트를 단순화하며 깨끗하고 유지 관리 가능한 코드를 생성하는 데 도움이 되는 방법을 보여 주었으면 합니다.