인스턴스 변수가 있는 모듈은 해로운 것으로 간주될 수 있습니다

배경

Rails는 사람들이 모듈과 인스턴스 변수를 어디에나 사용하는 것을 어느 정도 장려합니다.

예를 들어, 컨트롤러, 헬퍼 및 뷰에서 인스턴스 변수를 사용하는 것입니다.

그들은 또한 ActiveSupport::Concern의 사용을 장려하고 있으며, 이는 모든 것을 거대한 단일 객체에 저장한다는 아이디어를 더욱 강화하며, 사람들은 그 하나의 거대한 객체에서 모든 것에 접근할 수 있습니다.

문제점

물론 이것은 개발에 편리합니다.

왜냐하면 모든 것을 손쉽게 접근할 수 있기 때문입니다.

하지만 이러한 방식은 선택한 객체가 커지면 나중에 통제가 불가능해지는 데에는 여러 가지 단점이 있습니다.

같은 맥락에서 너무 많은 것들이 존재하며, 그 것들이 서로 결합되어 있는지 아닌지, 서로 의존하는지 아닌지를 알기 어렵습니다.

복잡성이 특정 지점까지 커질 때, 코드 추적도 매우 어려워집니다.

예를 들어, 클래스는 3개의 서로 다른 인스턴스 변수를 사용할 수 있으며, 이들 모두 3개의 서로 다른 모듈에서 초기화되고 조작될 수 있습니다.

문제가 발생할 때 그 변수를 추적하기 어려워집니다.

어떤 모듈이 갑자기 변수 중 하나를 변경할지 알 수 없습니다.

모든 것이 무엇이든 간섭할 수 있습니다.

유사한 우려

사람들은 다중 상속이 나쁘다고 합니다.

여러 모듈과 여러 인스턴스 변수가 여기저기 흩어져 있는 혼합은 동일한 문제를 겪습니다.

ActiveSupport::Concern에도 동일하게 적용됩니다.

참조: 전용 클래스 및 구성으로 우려 사항을 교체하는 것을 고려해 보세요

유사한 아이디어도 있습니다: 장비 모델 문제를 해결하기 위해 장식자 및 인터페이스 분할을 사용하세요

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 헬퍼와 메일러의 작동 방식 때문에, 그곳에서 인스턴스 변수의 사용을 피할 수 없을 수도 있습니다. 그런 경우, 우리는 현재로서는 그것을 무시할 수 있습니다. 이러한 모듈은 다른 임의의 객체와 공유되지 않으므로 여전히 다소 격리되어 있습니다.

뷰에서의 인스턴스 변수

인스턴스 변수는 나쁜데, 그 이유는 우리가 인스턴스 변수를 사용하는 사람(컨트롤러의 관점에서)과 그것을 설정하는 위치(부분의 관점에서)를 쉽게 파악할 수 없기 때문에 데이터 의존성을 추적하기가 극도로 어렵기 때문입니다.

우리는 대신 다음과 같은 방식을 사용하려고 합니다:

= 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)

이렇게 하면 이러한 값들이 어디에서 오는지 더 명확해지고, 인스턴스 변수를 사용하는 것보다 오타 검사를 할 수 있는 이점을 얻게 됩니다. 앞으로 우리는 부분에서 인스턴스 변수의 사용을 금지해야 합니다.