인스턴스 변수가 있는 모듈은 해로운 것으로 간주될 수 있습니다
배경
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)
이렇게 하면 이러한 값들이 어디에서 오는지 더 명확해지고, 인스턴스 변수를 사용하는 것보다 오타 검사를 할 수 있는 이점을 얻게 됩니다. 앞으로 우리는 부분에서 인스턴스 변수의 사용을 금지해야 합니다.