인스턴스 변수를 포함하는 모듈의 사용은 유해할 수 있습니다

배경

레일즈는 어떤 이유에서인지 모듈과 인스턴스 변수를 사용하는 것을 장려합니다. 예를 들어, 컨트롤러, 헬퍼, 뷰에서 인스턴스 변수를 사용하는 것입니다. 또한, ActiveSupport::Concern의 사용을 장려하고, 이로써 하나의 거대한 객체에 모든 것을 저장하고 여기저기에서 모든 것에 접근할 수 있게 하는 아이디어를 강화합니다.

문제점

물론, 개발하는 데에는 편리하겠지만, 이 선택한 객체가 성장하면 같은 이유 때문에 제어하기 힘들어질 수 있습니다.

같은 문맥 안에 너무 많은 것들이 있는데, 그것들이 서로 강하게 결합되어 있는지 여부, 서로 의존하고 있는지 여부를 알 수 없습니다. 복잡성이 어느 정도로 증가했을 때 이를 판단하는 것은 매우 어렵고 코드를 추적하는 것도 매우 어려워집니다. 예를 들어, 클래스가 3가지 다른 인스턴스 변수를 사용하고, 이 모두가 3가지 다른 모듈에서 초기화되고 조작될 수도 있습니다. 이러한 변수들이 문제를 일으킬 때 그것들을 추적하기가 어렵습니다. 어떤 모듈이 갑자기 변수 중 하나를 변경할지 우리는 알 수 없습니다. 모든 것이 어떤 것이든 건들 수 있습니다.

비슷한 우려 사안

사람들은 다중 상속이 나쁘다고 말하고 있습니다. 여기저기 흩어져 있는 다중 모듈과 다중 인스턴스 변수를 섞는 것은 같은 문제를 겪습니다. ActiveSupport::Concern에도 동일한 적용이 가능합니다. 자세한 내용은 다음을 참조하세요: 전용 클래스와 구성으로 concerns 대체를 고려해보세요

비슷한 아이디어가 하나 더 있습니다: 과도하게 커지는 모델 문제를 해결하기 위해 데코레이터와 인터페이스 분리 사용

included가 전체 문제를 해결해 주지 않는다는 것에 유의하십시오. 이것들은 의존성을 정의하지만, 여전히 최종 거대한 객체 내의 인스턴스 변수를 통해 각 모듈이 암시적으로 대화를 허용하고 있으며, 이것이 문제의 원인입니다.

해결책

우리는 거대한 객체를 여러 객체로 분할하고, 각각의 객체들을 API, 즉 공개 메소드를 통해 서로 통신하도록 해야 합니다. 요컨대, 상속 대신 구성. 이러면 각각의 작은 객체는 각자의 제한된 상태, 즉 인스턴스 변수를 갖게 될 것입니다. 만약 한 개의 인스턴스 변수에 문제가 생기면, 그것이 단일 작은 객체에서 오는 것이 분명해질 것입니다. 다른 누구도 건들 일 수 없기 때문입니다.

명확히 정의된 API로 인해, 이것은 것들을 결합시키는 것을 덜하고 디버깅하고 추적하기가 훨씬 쉬워지고, 다른 객체들이 사용하기에 훨씬 확장 가능하게 만들어줄 것입니다.왜냐하면 그들은 암시적인 종속 관계보다 명확한 방식으로 대화하기 때문입니다.

허용 가능한 사용

그러나, 모듈 안에서만 인스턴스 변수를 사용하는 것은 항상 나쁜 것은 아닙니다. 다른 모듈이나 객체가 이에 영향을 미치지 않는 한 허용할 수 있습니다.

우리는 특히 한 개의 인스턴스 변수만을 ||=로 설정하는 경우를 허용합니다. 다음과 같이 보일 것입니다:

module M
  def f
    @f ||= true
  end
end

유감스럽게도, 보다 복잡한 규칙을 코프에 쉽게 코딩하는 것은 어렵기 때문에, 우리는 사람들의 최선의 판단에 의존해야 합니다. 또 다른 좋은 패턴을 찾을 경우 쉽게 코프에 추가할 수 있습니다.

이 규칙을 해제하고 다시 적용하는 방법

이 규칙을 해제할 수 있지만, 그러지 말아야 합니다. 어떤 코드는 간단한 형태로 다시 작성할 수 있습니다. 다음을 고려하세요:

module Gitlab
  module Emoji
    def emoji_unicode_version(name)
      @emoji_unicode_versions_by_name ||=
        JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
      @emoji_unicode_versions_by_name[name]
    end
  end
end

이 메소드는 이미 완전히 자체 포함되어 있으므로 전혀 문제가 없습니다. 다른 메소드는 @emoji_unicode_versions_by_name을 사용해서는 안 되고, 이것이 좋습니다. 그러나 여전히 이 규칙을 위반한 것으로 나오지만 이것이 괜찮다는 것을 별로 인식하지 못하는 것입니다.

반면에, 이 메소드를 두 개로 나눌 수도 있습니다:

module Gitlab
  module Emoji
    def emoji_unicode_version(name)
      emoji_unicode_versions_by_name[name]
    end

    private

    def emoji_unicode_versions_by_name
      @emoji_unicode_versions_by_name ||=
        JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
    end
  end
end

이제 이 규칙은 불만스럽지 않을 것입니다.

이 규칙을 해제하는 방법

코드 바로 뒤에 해제 주석을 추가하십시오:

module M
  def violating_method
    @f + @g # rubocop:disable Gitlab/ModuleWithInstanceVariables
  end
end

여러 줄이 있다면, 섹션을 위해 활성화 및 비활성화도 가능합니다:

module M
  # rubocop:disable Gitlab/ModuleWithInstanceVariables
  def violating_method
    @f = 0
    @g = 1
    @h = 2
  end
  # rubocop:enable Gitlab/ModuleWithInstanceVariables
end

아래 내용이 전혀 확인되지 않는 경우를 방지하기 위해 어느 시점에서라도 다시 활성화해야 합니다.

지금은 무시할 필요가 있는 항목들

Rails 도우미(helper)와 메일러(mailers)의 작동 방식 때문에, 우리는 그곳에서 인스턴스 변수의 사용을 피할 수 없을 수도 있습니다. 그런 경우에는 우리는 지금은 그것들을 무시할 수 있을 것입니다. 이러한 모듈들은 다른 임의의 객체와 공유되지 않으므로, 그들은 여전히 어느 정도 격리되어 있습니다.

뷰(view)에서의 인스턴스 변수

컨트롤러 관점에서는 누가 인스턴스 변수를 사용하는지, 부분(partial) 관점에서는 어디에서 그것들을 설정했는지 쉽게 알기 어려워 데이터 의존성을 추적하기가 굉장히 어려워집니다.

우리는 대신 다음과 같은 것을 사용하려고 노력하고 있습니다:

= render 'projects/commits/commit', commit: commit, ref: ref, project: project

그리고 부분에서는:

- ref = local_assigns.fetch(:ref)
- commit = local_assigns.fetch(:commit)
- project = local_assigns.fetch(:project)

이 방법으로는 그 값이 어디에서 왔는지 더 명확하게 알 수 있으며, 인스턴스 변수를 사용하는 것보다 오타 확인을 할 수 있는 이점을 얻게 됩니다. 앞으로는 부분에서의 인스턴스 변수의 사용을 금지해야 합니다.