Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

루비 분리:위임 대 종속성 주입

객체 지향 프로그래밍에서 , 한 개체는 작동하기 위해 종종 다른 개체에 의존합니다.

예를 들어 재무 보고서를 실행하는 간단한 클래스를 만드는 경우:

class FinanceReport
  def net_income
    FinanceApi.gross_income - FinanceApi.total_costs
  end
end

FinanceReport 의존 FinanceApi , 외부 지불 프로세서에서 정보를 가져오는 데 사용합니다.

하지만 어떤 시점에서 다른 API를 사용하고 싶다면 어떻게 해야 할까요? 또는 외부 리소스에 영향을 주지 않고 이 클래스를 테스트하려면 어떻게 해야 할까요? 가장 일반적인 대답은 의존성 주입을 사용하는 것입니다.

종속성 주입에서는 FinanceApi를 명시적으로 참조하지 않습니다. FinanceReport 내부 . 대신 인수로 전달합니다. 우리는 주사합니다. 그것.

의존성 주입을 사용하여 우리 클래스는 다음과 같이 됩니다:

class FinanceReport
  def net_income(financials)
    financials.gross_income - financials.total_costs
  end
end

이제 우리 클래스는 FinanceApi 개체도 존재합니다! 모든 개체를 전달할 수 있습니다. gross_income을 구현하는 한 및 total_costs .

다음과 같은 이점이 있습니다.

  • 이제 코드가 FinanceApi에 덜 "결합"되었습니다. .
  • 우리는 강제로 FinanceApi를 사용해야 합니다. 공개 인터페이스를 통해.
  • 이제 테스트에서 모의 ​​객체 또는 스텁 객체를 전달할 수 있으므로 실제 API에 도달할 필요가 없습니다.

대부분의 개발자는 종속성 주입을 고려합니다. 일반적으로 좋은 일 (나도!). 그러나 모든 기술과 마찬가지로 장단점이 있습니다.

이제 코드가 약간 더 불투명해졌습니다. 명시적으로 FinanceApi를 사용한 경우 , 우리의 가치가 어디에서 오는지 분명했습니다. 종속성 주입을 통합한 코드에서는 명확하지 않습니다.

그렇지 않으면 호출이 self로 갔을 경우 , 우리는 코드를 더 장황하게 만들었습니다. 객체 지향 "객체에 메시지를 보내고 행동하게 하라" 패러다임을 사용하는 대신, 우리는 보다 기능적인 "입력 -> 출력" 패러다임으로 이동하고 있음을 알게 되었습니다.

이것은 마지막 경우입니다(self ) 오늘 보려고 합니다. 다음과 같은 상황에서 종속성 주입에 대한 가능한 대안을 제시하고 싶습니다. 기본 클래스를 동적으로 변경 (약간).

해결해야 할 문제

잠시 시간을 내어 저를 이 길로 이끌었던 문제부터 시작해 보겠습니다. PDF 보고서

내 고객은 다양한 인쇄 가능한 PDF 보고서를 생성할 수 있는 기능을 요청했습니다. 한 보고서에는 계정에 대한 모든 비용이 나열되어 있고 다른 보고서에는 수익이 나열되어 있고 다른 보고서에는 향후 몇 년 동안의 예상 수익이 포함되어 있습니다.

우리는 유서 깊은 prawn를 사용하고 있습니다. 각각의 보고서는 Prawn::Document에서 서브클래싱된 자체 Ruby 객체인 이러한 PDF를 생성하는 gem .

다음과 같습니다.

class CostReport < Prawn::Document
  def initialize(...)
    ...
  end

  def render
    text "Cost Report"
    move_down 20
    ...
  end

여태까지는 그런대로 잘됐다. 하지만 문제가 있습니다. 고객은 다른 모든 보고서의 일부를 포함한 '개요' 보고서를 원합니다. .

해결책 1:의존성 주입

앞서 언급했듯이 이러한 종류의 문제에 대한 한 가지 일반적인 솔루션은 종속성 주입을 사용하도록 코드를 리팩토링하는 것입니다. 즉, 이러한 모든 보고서가 self에 대한 메서드를 호출하는 것보다 , 대신 PDF 문서를 인수로 전달합니다.

이것은 우리에게 다음과 같은 것을 줄 것입니다:

class CostReport < Prawn::Document
...
  def title(pdf = self)
    pdf.text "Cost Report"
    pdf.move_down 20
    ...
  end
end

이것은 작동하지만 여기에 약간의 오버헤드가 있습니다. 우선 모든 단일 그리기 방법은 이제 pdf를 가져와야 합니다. 인수 및 prawn에 대한 모든 단일 호출 이제 이 pdf를 거쳐야 합니다. 주장.

종속성 주입은 몇 가지 이점이 있습니다. 시스템에서 분리된 구성 요소로 우리를 밀어넣고 단위 테스트를 더 쉽게 하기 위해 모의 또는 스텁을 전달할 수 있습니다.

그러나 우리는 우리의 시나리오에서 이러한 이점의 보상을 얻지 못하고 있습니다. 우리는 이미 강력하게 prawn와 결합 API이므로 다른 PDF 라이브러리로 변경하려면 코드를 완전히 다시 작성해야 합니다.

여기에서는 테스트도 큰 문제가 되지 않습니다. 우리의 경우 자동화된 테스트로 생성된 PDF 보고서를 테스트하는 것은 가치가 있기에 너무 번거롭기 때문입니다.

따라서 Dependency Injection은 우리가 원하는 동작을 제공하지만 최소한의 이점으로 추가 오버 헤드를 도입합니다. 다른 옵션을 살펴보겠습니다.

해결책 2:위임

Ruby의 표준 라이브러리는 SimpleDelegator를 제공합니다. 데코레이터 패턴을 구현하는 쉬운 방법입니다. 개체를 생성자에 전달하면 위임자에 대한 모든 메서드 호출이 개체에 전달됩니다.

SimpleDelegator 사용 , 우리는 prawn을 감싸는 기본 보고서 클래스를 생성할 수 있습니다. .

class PrawnWrapper < SimpleDelegator
  def initialize(document: nil)
    document ||= Prawn::Document.new(...)
    super(document)
  end
end

그런 다음 이 클래스에서 상속하도록 보고서를 업데이트할 수 있으며 초기화 프로그램에서 생성된 기본 문서를 사용하여 이전과 동일하게 작동합니다. 개요에서 이것을 사용하면 마법이 일어납니다. 보고:

class OverviewReport < PrawnWrapper
  ...
  def render
    sales = SaleReport.new(..., document: self)
    sales.sales_table
    costs = CostReport.new(..., document: self)
    costs.costs_pie_chart
    ...
  end
end

여기 SaleReport#sales_tableCostReport#costs_pie_chart 변경되지 않은 상태로 유지되지만 prawn에 대한 호출 (예:text(...) , move_down 20 등)이 이제 OverviewReport로 전달됩니다. SimpleDelegator를 통해 우리가 만들었습니다.

행동 측면에서 우리는 본질적으로 SalesReport 이제 OverviewReport의 하위 클래스입니다. . 우리의 경우 이것은 prawn에 대한 모든 호출이 의 API는 이제 SalesReport -> OverviewReport -> Prawn::Document로 이동합니다. .

SimpleDelegator 작동 방식

SimpleDelegator 방식 내부적으로 작동하는 것은 기본적으로 Ruby의 method_missing을 사용하는 것입니다. 메서드 호출을 다른 개체로 전달하는 기능입니다.

그래서 SimpleDelegator (또는 그것의 서브클래스) 메소드 호출을 받습니다. 해당 방법을 구현하면 훌륭합니다. 다른 개체와 마찬가지로 실행합니다. 하지만 , 해당 메서드가 정의되어 있지 않으면 method_missing이 발생합니다. . method_missing 그런 다음 call을 시도합니다. 생성자에 주어진 객체에 대한 그 메소드.

간단한 예:

require 'simple_delegator'
class Thing
  def one
    'one'
  end
  def two
    'two'
  end
end

class ThingDecorator < SimpleDelegator
  def two
    'three!'
  end
end

ThingDecorator.new(Thing.new).one #=> "one"
ThingDecorator.new(Thing.new).two #=> "three!"

SimpleDelegator를 서브클래싱하여 자체 ThingDecorator로 여기 클래스에서 일부 메서드를 덮어쓰고 다른 메서드는 기본 Thing으로 넘어갈 수 있습니다. 개체.

위의 간단한 예제는 실제로 SimpleDelegator를 수행하지 않습니다. 그래도 정의. 이 코드를 보고 "Thing 같은 결과를 주시겠습니까?”

예, 그렇습니다. 그러나 여기에 주요 차이점이 있습니다. SimpleDelegator 생성자의 인수로 위임할 개체를 사용합니다. 이는 런타임에 다른 개체를 전달할 수 있음을 의미합니다. .

이것은 호출을 prawn으로 리디렉션하는 데 사용할 수 있는 것입니다. 위의 솔루션 2의 개체입니다. 단일 보고서를 호출하면 prawn 호출은 생성자에서 생성된 새 문서로 이동합니다. 그러나 개요 보고서는 이를 변경할 수 있으므로 prawn 해당으로 전달됩니다. 문서.

결론

의존성 주입은 아마도 대부분의 분리 문제 대부분 시간.

그러나 모든 기술과 마찬가지로 절충점이 있습니다. 제 경우에는 DI로 인해 발생하는 오버헤드가 DI가 제공하는 이점만큼 가치가 있다고 생각하지 않아 다른 솔루션을 찾았습니다.

Ruby의 모든 것과 마찬가지로 항상 다른 방법이 있습니다. . 이 솔루션을 자주 사용하지는 않겠지만 이러한 상황에서 Ruby 도구 모음에 추가하면 좋을 것입니다.