수색…


자신을 반복하지 마십시오 (DRY).

깨끗한 코드를 유지하기 위해 Rails는 DRY 원칙을 따릅니다.

가능할 때마다 유사한 코드를 여러 부분에 복제하는 대신 (예 : 부분을 사용하여) 최대한 많은 코드를 재사용합니다. 이렇게하면 오류가 줄어들고 코드가 깨끗하게 유지되며 코드작성한 다음 다시 사용하는 원칙이 적용됩니다. 동일한 코드의 여러 부분을 업데이트하는 것보다 한 곳에서 코드를 업데이트하는 것이 더 쉽고 효율적입니다. 따라서 코드를 모듈화하고 강력하게 만듭니다.

또한 Fat Model, Skinny Controller 는 DRY입니다. 왜냐하면 모델에 코드를 작성하고 컨트롤러에서 다음과 같이 호출 만하기 때문입니다.

# Post model
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) } 


# Any controller
def index
    ....
    @unpublished_posts = Post.unpublished
    ....
end

def others
    ...
    @unpublished_posts = Post.unpublished
    ...
end

또한 내부 메소드가 숨겨지고 API 형식으로 매개 변수를 전달하여 변경이 이루어지는 API 기반 구조로이 끕니다.

컨벤션 오버 구성

Rails에서 데이터베이스의 컨트롤러, 뷰 및 모델 을 살펴 보았습니다.

무거운 구성의 필요성을 줄이기 위해 Rails는 애플리케이션 작업을 쉽게하기위한 규칙을 구현합니다. 자신 만의 규칙을 정의 할 수 있지만 처음에는 레일스가 제공하는 규칙을 고수하는 것이 좋습니다.

이러한 규칙은 개발 속도를 높이고 코드를 간결하고 읽기 쉽게 유지하며 응용 프로그램 내에서 쉽게 탐색 할 수 있도록합니다.

협약은 또한 초보자를위한 진입 장벽을 낮 춥니 다. 초보자도 알 필요가 없지만 무지에서 이득을 볼 수있는 수많은 규칙이 있습니다. 왜 모든 것이 정상인지 알지 못해도 훌륭한 응용 프로그램을 만들 수 있습니다.

예를 들어

기본 키 id 사용하여 orders 라는 데이터베이스 테이블이있는 경우 일치하는 모델을 order 라고하고 모든 논리를 처리하는 컨트롤러를 orders_controller 라고 orders_controller . 뷰는 다른 동작으로 분할됩니다. 컨트롤러에 new 동작과 edit 동작이있는 경우 new 뷰와 edit 뷰가 있습니다.

예를 들어

앱을 생성하려면 rails new app_name 실행하기 만하면 rails new app_name . 이렇게하면 레일즈 애플리케이션을위한 인프라와 토대를 구성하는 약 70 개의 파일과 폴더가 생성됩니다.

그것은 다음을 포함합니다 :

  • 모델 (데이터베이스 계층), 컨트롤러 및 뷰를 유지하는 폴더
  • 응용 프로그램에 대한 단위 테스트를 수행하는 폴더
  • Javascript 및 CSS 파일과 같은 웹 자산을 보유 할 폴더
  • HTTP 400 응답의 기본 파일 (즉, 파일을 찾을 수 없음)
  • 많은 다른 사람

지방 모델, 스키니 컨트롤러

"Fat Model, Skinny Controller"는 MVC의 M과 C 부분이 이상적으로 함께 작동하는 방식을 나타냅니다. 즉, 응답이없는 로직이 모델에 들어가야합니다. 이상적으로는 테스트 가능한 좋은 방법입니다. 한편, "마른 체형"컨트롤러는 뷰와 모델 사이의 멋진 인터페이스입니다.

실제로 이것은 다양한 유형의 리팩터링을 필요로 할 수 있지만, 모두 하나의 아이디어로 넘어갑니다. 응답 대신 컨트롤러가 아닌 모델로 이동하면 재사용을 촉진했을뿐만 아니라 가능하면 요청 컨텍스트 외부에서 코드를 테스트 할 수있게 만들었습니다.

간단한 예를 살펴 보겠습니다. 다음과 같은 코드가 있다고 가정 해보십시오.

def index
  @published_posts = Post.where('published_at <= ?', Time.now)
  @unpublished_posts = Post.where('published_at IS NULL OR published_at > ?', Time.now)
end

다음과 같이 변경할 수 있습니다.

def index
  @published_posts = Post.published
  @unpublished_posts = Post.unpublished
end

그런 다음 논리를 게시물 모델로 이동하면 다음과 같이 표시됩니다.

scope :published, ->(timestamp = Time.now) { where('published_at <= ?', timestamp) }
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }

default_scope에주의하십시오.

ActiveRecord는 기본적으로 모델을 자동으로 범위 지정하기 위해 default_scope 포함합니다.

class Post
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

위 코드는 모델에 대한 쿼리를 수행 할 때 이미 게시 된 게시물을 제공합니다.

Post.all # will only list published posts 

그 범위는 무해한 반면, 원하지 않는 여러 가지 숨겨진 부작용이 있습니다.

default_scopeorder

default_scope 에서 order 을 선언 default_scope Post 에 대한 호출 order 는 기본값을 재정의하는 대신 추가 주문으로 추가됩니다.

Post.order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC

이것은 아마도 당신이 원하는 행동이 아닙니다. 먼저 스코프에서 order 를 제외하여이를 재정의 할 수 있습니다.

Post.except(:order).order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC

default_scope 및 모델 초기화

다른 ActiveRecord::Relation 과 마찬가지로 default_scope 는 초기화 된 모델의 기본 상태를 변경합니다.

위 예제에서 Post 에는 기본적으로 where(published: true) 설정되어 있으므로 Post 새 모델에도 설정됩니다.

Post.new # => <Post published: true>

unscoped

default_scopeunscoped 먼저 호출하여 명목상 지울 수 있지만 부작용도 있습니다. 예를 들어 STI 모델을 예로 들어 보겠습니다.

class Post < Document
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

기본적으로 Post 에 대한 쿼리의 범위는 'Post' 포함 된 열을 type 하는 데 사용됩니다. 그러나 unscoped 는 자신의 default_scope 와 함께 이것을 지울 것이므로 unscoped 를 사용한다면 이것을 고려해야한다.

Post.unscoped.where(type: 'Post').order(updated_at: :desc)

unscoped 모델 연합

PostUser 의 관계를 생각해보십시오.

class Post < ApplicationRecord
  belongs_to :user
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

class User < ApplicationRecord
  has_many :posts
end

개인 User 를 확보하면 해당 User 와 관련된 게시물을 볼 수 있습니다.

user = User.find(1)
user.posts
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' AND "posts"."user_id" = ? ORDER BY "posts"."created_at" DESC [["user_id", 1]]

그러나 posts 릴레이션에서 default_scope 을 지우려고하므로 unscoped 를 사용한다.

user.posts.unscoped
SELECT "posts".* FROM "posts"

이것은 default_scope 뿐만 아니라 user_id 조건을 지 default_scope .

default_scope 대한 예제 유스 케이스

이 모든 것에도 불구하고 default_scope 사용하는 것이 정당한 경우가 있습니다.

동일한 응용 프로그램에서 여러 하위 도메인이 제공되지만 격리 된 데이터가있는 다중 점유자 시스템을 생각해보십시오. 이 격리를 수행하는 한 가지 방법은 default_scope 입니다. 다른 경우의 단점은 여기에서 위로가됩니다.

class ApplicationRecord < ActiveRecord::Base
  def self.inherited(subclass)
    super

    return unless subclass.superclass == self
    return unless subclass.column_names.include? 'tenant_id'

    subclass.class_eval do
      default_scope ->{ where(tenant_id: Tenant.current_id) }
    end
  end
end

Tenant.current_id 를 요청 초기에 설정 Tenant.current_id tenant_id 가 들어있는 테이블은 자동으로 추가 코드없이 범위가 지정됩니다. 레코드를 인스턴스화하면 작성된 입주자 ID가 자동으로 상속됩니다.

이 유스 케이스의 중요한 점은 범위가 요청마다 한 번 설정되고 변경되지 않는다는 것입니다. 여기서 unscoped 필요 unscoped 있는 유일한 경우는 요청 범위 밖에있는 백그라운드 작업자와 같은 특별한 경우입니다.

너는 필요하지 않을거야 (YAGNI)

기능에 대해 "YAGNI"(꼭 필요한 것은 아닙니다)라고 말할 수 있다면 구현하지 않는 것이 좋습니다. 단순성에 집중하여 많은 개발 시간을 절약 할 수 있습니다. 어쨌든 이러한 기능을 구현하면 문제가 발생할 수 있습니다.

문제들

Overengineering

제품이 더 복잡한 경우에는 설계가 끝났습니다. 일반적으로 이러한 "아직 사용되지 않은"기능은 의도 한 방식으로 사용되지 않으며 사용 된 경우 리팩토링해야합니다. 조기 최적화, 특히 성능 최적화를 통해 설계 결정이 내려지며, 이는 향후 잘못된 것으로 판명 될 것입니다.

코드 부 풀기

Code Bloat은 불필요한 복잡한 코드를 의미합니다. 이는 예를 들어 추상화, 중복 또는 디자인 패턴의 잘못된 적용에 의해 발생할 수 있습니다. 코드 기반은 이해하기 어렵고, 혼란스럽고 유지 보수 비용이 많이 든다.

피쳐 크립

Feature Creep은 제품의 핵심 기능을 뛰어 넘는 새로운 기능을 추가하는 것으로 불필요하게 높은 복잡성을 초래합니다.

긴 개발 시간

필요한 기능을 개발하는 데 사용할 수있는 시간은 불필요한 기능을 개발하는 데 소비됩니다. 제품 배달 시간이 오래 걸립니다.

솔루션

KISS - 간단하고, 어리 석다.

KISS에 따르면, 대부분의 시스템이 단순하게 설계된 경우 가장 잘 작동합니다. 복잡성을 줄이기 위해서는 단순성이 기본 설계 목표가되어야합니다. 예를 들어 "Single Responsibility Principle (단일 책임 원칙)"에 따라 수행 할 수 있습니다.

YAGNI - 너는 필요 없어.

적은 것이 더 많습니다. 모든 기능에 대해 생각해보십시오. 정말 필요한가요? YAGNI가 어떤 식 으로든 생각하면 떠날 수 있습니다. 필요할 때 개발하는 편이 낫습니다.

지속적인 리팩터링

제품이 꾸준히 개선되고 있습니다. 리팩토링을 통해 제품이 모범 사례에 따라 수행되고 패치 작업으로 변질되지 않도록 할 수 있습니다.

도메인 객체 (더 이상 뚱뚱한 모델 없음)

"Fat Model, Skinny Controller"는 아주 좋은 첫 단계이지만 코드베이스가 커지면 확장되지 않습니다.

모델의 Single Responsibility 에 대해 생각해 봅시다. 모델의 단일 책임은 무엇입니까? 비즈니스 논리를 유지하는 것입니까? 비 응답 관련 논리를 유지하는 것입니까?

아니요. 그 책임은 지속 레이어와 그 추상화를 처리하는 것입니다.

비 응답 관련 논리 및 비 지속성 관련 논리뿐 아니라 비즈니스 논리도 도메인 객체에 있어야합니다.

도메인 객체는 문제 영역에서 유일한 책임을 지도록 설계된 클래스입니다. 학생들이 해결 한 문제에 대한 수업을 " 비명을 지르는 건축물 "로 만들어주십시오.

실제로, 당신은 마른 모델, 마른 전망과 마른 컨트롤러쪽으로 노력해야합니다. 솔루션의 아키텍처가 선택한 프레임 워크의 영향을받지 않아야합니다.

예를 들어

스트라이프를 통해 고객에게 15 %의 수수료를 부과하는 마켓 플레이스라고 가정 해 보겠습니다. 고정 된 15 % 커미션을 청구하면 스트라이프가 2.9 % + 30 ¢를 부과하므로 커미션이 주문 금액에 따라 변경된다는 의미입니다.

위탁 수수료는 amount*0.15 - (amount*0.029 + 0.30) 이어야합니다.

이 논리를 모델에 쓰지 마십시오.

# app/models/order.rb
class Order < ActiveRecord::Base
  SERVICE_COMMISSION = 0.15
  STRIPE_PERCENTAGE_COMMISSION = 0.029
  STRIPE_FIXED_COMMISSION = 0.30

  ...

  def commission
    amount*SERVICE_COMMISSION - stripe_commission  
  end

  private

  def stripe_commission
    amount*STRIPE_PERCENTAGE_COMMISSION + STRIPE_FIXED_COMMISSION
  end
end

새 결제 수단과 통합하자 마자이 모델에서이 기능을 확장 할 수 없습니다.

또한 더 많은 비즈니스 로직을 통합하자 마자 Order 객체의 응집력 이 사라지기 시작합니다.

영속 명령의 책임으로부터 완전히 파생 된위원회의 계산과 함께 도메인 객체를 선호하십시오.

# app/models/order.rb
class Order < ActiveRecord::Base
  ...
  # No reference to commission calculation
end

# lib/commission.rb
class Commission
  SERVICE_COMMISSION = 0.15

  def self.calculate(payment_method, model)
    model.amount*SERVICE_COMMISSION - payment_commission(payment_method, model)  
  end

  private

  def self.payment_commission(payment_method, model)
    # There are better ways to implement a static registry,
    # this is only for illustration purposes.
    Object.const_get("#{payment_method}Commission").calculate(model)
  end
end

# lib/stripe_commission.rb
class StripeCommission
  STRIPE_PERCENTAGE_COMMISSION = 0.029
  STRIPE_FIXED_COMMISSION = 0.30

  def self.calculate(model)
    model.amount*STRIPE_PERCENTAGE_COMMISSION
      + STRIPE_PERCENTAGE_COMMISSION
  end
end

# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def create
    @order = Order.new(order_params)
    @order.commission = Commission.calculate("Stripe", @order)
    ...
  end
end

도메인 개체를 사용하면 다음과 같은 아키텍처 이점이 있습니다.

  • 논리를 사용하여 객체를 인스턴스화하는 데 조명기 또는 팩토리가 필요하지 않으므로 단위 테스트가 매우 쉽습니다.
  • 메시지 amount 을 허용하는 모든 작업을 수행합니다.
  • 명확하게 정의 된 책임과보다 높은 응집력으로 각 도메인 객체를 작게 유지합니다.
  • 수정이 아닌 추가 로 새로운 지불 방법으로 쉽게 확장됩니다.
  • 각 Ruby on Rails 애플리케이션에서 계속 증가하는 User 객체를 갖는 경향을 멈 춥니 다.

나는 개인적으로 lib 에 도메인 객체를 넣는 것을 좋아한다. 그렇게하는 경우 autoload_paths 에 추가해야합니다.

# config/application.rb
config.autoload_paths << Rails.root.join('lib')

또한 명령 / 쿼리 패턴에 따라보다 행동 지향적 인 도메인 개체를 만드는 것이 좋습니다. 이 경우 모든 app 하위 디렉토리가 자동로드 경로에 자동으로 추가되므로 app/commands 에 이러한 객체를 넣는 것이 더 좋은 장소 일 수 있습니다.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow