Dependency Injection 또는 정적 팩토리를 사용해야합니까?


81

시스템을 설계 할 때 다른 모듈에서 사용하는 많은 모듈 (로깅, 데이터베이스 액세스 등)이 발생하는 문제에 종종 직면합니다. 문제는 이러한 구성 요소를 다른 구성 요소에 제공하는 방법은 무엇입니까? 두 가지 대답은 가능한 의존성 주입 또는 팩토리 패턴을 사용하는 것 같습니다. 그러나 둘 다 잘못된 것 같습니다.

  • 팩토리는 테스트를 어렵게 만들고 구현을 쉽게 교체 할 수 없습니다. 또한 종속성을 명확하게 나타내지 않습니다 (예 : 데이터베이스를 사용하는 메소드를 호출하는 메소드를 호출하는 메소드를 호출한다는 사실을 알지 못하는 메소드 검사).
  • Dependecy injection은 생성자 인수 목록을 크게 팽창시키고 코드 전체에서 일부 측면을 번집니다. 일반적인 상황은 반 이상의 클래스 생성자가 다음과 같은 경우입니다.(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

여기에 문제가있는 일반적인 상황이 있습니다. 예외 클래스가 있습니다. 사용자 클래스 개체에있는 사용자 언어 설정 매개 변수가있는 쿼리를 사용하여 데이터베이스에서로드 된 오류 설명을 사용하는 예외 클래스가 있습니다. 따라서 새로운 예외를 만들려면 데이터베이스 세션과 사용자 세션이 필요한 설명이 필요합니다. 따라서 예외를 던질 필요가있는 경우를 대비하여 모든 메소드 에서이 모든 객체를 드래그하는 운명입니다.

그런 문제를 어떻게 해결합니까?


2
팩토리가 모든 문제를 해결할 수 있다면 팩토리를 객체에 주입하고 LoggingProvider, DbSessionProvider, ExceptionFactory, UserSession을 얻을 수 있습니다.
Giorgio

1
메소드에 너무 많은 "입력"이 전달되거나 주입 되더라도 메소드 디자인 자체의 문제점입니다. 어떤 방법을 사용하든 분석법의 크기를 조금 줄이고 싶을 수도 있습니다 (주사 한 후에는 더 쉽습니다)
Bill K

여기서 해결책은 논쟁을 줄여서는 안됩니다. 대신 객체의 모든 작업을 수행하고 이점을 제공하는 상위 레벨 객체를 작성하는 추상화를 작성하십시오.
Alex

답변:


74

의존성 주입을 사용하지만 생성자 인수 목록이 너무 커질 때마다 Facade Service를 사용하여 리팩터링하십시오 . 아이디어는 생성자 인수 중 일부를 그룹화하여 새로운 추상화를 도입하는 것입니다.

예를 들어, SessionEnvironmenta DBSessionProvider및 the UserSessionand loaded를 캡슐화하는 새로운 유형을 도입 할 수 Descriptions있습니다. 그러나 어떤 추상화가 가장 적합한 지 알기 위해서는 프로그램의 세부 사항을 알아야합니다.

비슷한 질문이 이미 여기에 요청 되었습니다 .


9
+1 : 생성자 인수를 클래스로 그룹화하는 것이 매우 좋습니다. 또한 이러한 논증을보다 의미있는 구조로 구성해야합니다.
Giorgio

5
그러나 결과가 의미있는 구조가 아닌 경우 SRP를 위반하는 복잡성을 숨기고 있습니다. 이 경우 클래스 리팩토링을 수행해야합니다.
danidacar

1
@Giorgio 나는 "생성자 인자를 클래스로 그룹화하는 것이 매우 좋은 생각"이라는 일반적인 진술에 동의하지 않는다. "이 시나리오에서"로이 조건을 충족하면 다릅니다.
tymtam

19

Dependecy injection은 생성자 인수 목록을 크게 팽창시키고 코드 전체에서 일부 측면을 번집니다.

그로부터 DI를 제대로 이해하지 못하는 것 같습니다. 공장 내부에서 객체 인스턴스화 패턴을 뒤집는 것이 아이디어입니다.

특정 문제가 더 일반적인 OOP 문제인 것 같습니다. 왜 객체가 런타임 중에 사람이 읽을 수없는 일반적인 예외를 던질 수 없으며 최종 시도 / 포착 전에 예외를 잡는 무언가를 가질 수 없으며 그 시점에서 세션 정보를 사용하여 새롭고 더 예쁘게 예외를 던지는 이유는 무엇입니까? ?

또 다른 방법은 예외 팩토리를 만드는 것입니다. 예외 팩토리는 생성자를 통해 객체로 전달됩니다. 클래스는 새로운 예외를 던지는 대신 팩토리의 메소드를 던질 수 있습니다 (예 : throw PrettyExceptionFactory.createException(data).

팩토리 오브젝트와는 별도로 오브젝트를 사용해서는 안됩니다 new. 예외는 일반적으로 특별한 경우이지만 귀하의 경우에는 예외 일 수 있습니다!


1
매개 변수 목록이 너무 길어지면 의존성 주입을 사용하기 때문이 아니라 더 많은 의존성 주입이 필요하다는 것을 읽었습니다.
Giorgio

그 이유 중 하나가 될 수 - 일반적으로, 언어에 따라, 자신을 생성자가 더 이상 6-8 인수가 없어야한다, 그 중 더 이상 3-4 없어야 객체 (등 특정 패턴하지 않는 Builder패턴) 지시합니다. 객체가 다른 객체를 인스턴스화하기 때문에 생성자에 매개 변수를 전달하는 경우 IoC의 경우가 분명합니다.
Jonathan Rich

12

정적 팩토리 패턴의 단점을 이미 잘 설명했지만 종속성 주입 패턴의 단점에 대해서는 동의하지 않습니다.

의존성 주입을 위해서는 각 의존성에 대한 코드를 작성해야하지만 버그는 아닙니다. 이러한 의존성이 실제로 필요한지 여부를 생각하게하여 느슨한 결합을 촉진합니다. 귀하의 예에서 :

여기에 문제가있는 일반적인 상황이 있습니다. 예외 클래스가 있습니다. 사용자 클래스 개체에있는 사용자 언어 설정 매개 변수가있는 쿼리를 사용하여 데이터베이스에서로드 된 오류 설명을 사용하는 예외 클래스가 있습니다. 따라서 새로운 예외를 만들려면 데이터베이스 세션과 사용자 세션이 필요한 설명이 필요합니다. 따라서 예외를 던질 필요가있는 경우를 대비하여 모든 메소드 에서이 모든 객체를 드래그하는 운명입니다.

아니, 당신은 운명이 아닙니다. 특정 사용자 세션에 대한 오류 메시지를 현지화하는 것이 비즈니스 로직의 책임 인 이유는 무엇입니까? 나중에 언젠가 사용자 프로그램이없는 배치 프로그램에서 해당 비즈니스 서비스를 사용하려면 어떻게해야합니까? 또는 현재 로그인 한 사용자에게 오류 메시지를 표시하지 말고 관리자 (다른 언어를 선호하는 사람)에게 표시해야하는 경우 어떻게해야합니까? 또는 클라이언트에서 비즈니스 로직을 재사용하려는 경우 (데이터베이스에 액세스 할 수없는 ...)?

분명히 메시지를 지역화하는 것은 누가이 메시지를 보는지에 달려 있습니다. 즉, 프리젠 테이션 레이어의 책임입니다. 따라서 비즈니스 서비스에서 일반적인 예외를 처리하고 메시지 식별자를 전달한 다음 프리젠 테이션 계층의 예외 핸들러를 사용하는 메시지 소스에서 찾아 볼 수 있습니다.

이렇게하면 불필요한 종속성 3 개 (UserSession, ExceptionFactory 및 설명)를 제거하여 코드를 더 단순하고 다양하게 만들 수 있습니다.

일반적으로, 나는 유비쿼터스 액세스가 필요한 것들에 대해서만 정적 팩토리를 사용하고 있으며 우리가 코드를 실행하고 싶었던 모든 환경 (예 : 로깅)에서 사용할 수 있도록 보장합니다. 다른 모든 것에는 평범한 오래된 의존성 주입을 사용합니다.


이. 나는하기 위해 DB를 칠 필요가 볼 수없는 던질 예외를.
Caleth

1

의존성 주입을 사용하십시오. 정적 팩토리를 사용하는 것은 Service Locator반 패턴을 사용하는 것입니다. Martin Fowler의 주요 작품을 여기에서 확인하십시오-http: //martinfowler.com/articles/injection.html

생성자 인수가 너무 커져 DI 컨테이너를 사용하지 않는 경우 인스턴스화를 위해 자체 팩토리를 작성하여 XML로 구성하거나 인터페이스에 구현을 바인딩하여 구성 할 수 있습니다.


5
Service Locator는 반 패턴이 아닙니다. Fowler는 귀하가 게시 한 URL에서이를 참조합니다. 서비스 로케이터 패턴은 남용 될 수 있지만 (싱글 톤이 남용되는 것과 같은 방식으로 글로벌 상태를 추상화하기 위해) 매우 유용한 패턴입니다.
Jonathan Rich

1
흥미 롭습니다. 나는 항상 안티 패턴이라고 들었습니다.
Sam

4
서비스 로케이터를 사용하여 전역 상태를 저장하는 경우 반 패턴 일뿐입니다. 서비스 로케이터는 인스턴스화 후 상태 비 저장 객체 여야하며, 변경 불가능한 것이 좋습니다.
Jonathan Rich

XML은 형식이 안전하지 않습니다. 다른 모든 방법이 실패 후에 나는 그것을 계획 Z를 고려할 것
리처드 설렘을

1

Dependency Injection과 함께 갈 것입니다. DI는 생성자뿐만 아니라 속성 설정자를 통해서도 수행됩니다. 예를 들어, 로거는 특성으로 주입 될 수 있습니다.

또한 도메인 논리에 의해 런타임에 필요한 항목에 생성자 매개 변수를 유지하는 등 (예 : 클래스 및 실제 도메인 종속성) 및 속성을 통해 다른 도우미 클래스를 주입 할 수 있습니다.

한 단계 더 나아가면 Aspect-Oriented Programmnig가 여러 주요 프레임 워크에서 구현됩니다. 이를 통해 클래스의 생성자를 가로 채거나 ( "AspectJ 용어를 사용하도록 조언") 특수 속성이 주어지면 관련 속성을 주입 할 수 있습니다.


4
나는 setter를 통해 DI를 피합니다. 개체와 생성자 호출 사이에 객체가 완전히 초기화되지 않은 시간 창을 소개하기 때문입니다. 즉, 가능한 경우 피하는 메소드 호출 순서 (Y보다 X를 호출해야 함)를 소개합니다.
RokL

1
속성 설정기를 통한 DI는 선택적 종속성에 이상적입니다. 로깅이 좋은 예입니다. 로깅이 필요한 경우 Logger 속성을 설정하고 그렇지 않은 경우 설정하지 마십시오.
Preston

1

팩토리는 테스트를 어렵게 만들고 구현을 쉽게 교체 할 수 없습니다. 또한 종속성을 명확하게 나타내지 않습니다 (예 : 데이터베이스를 사용하는 메소드를 호출하는 메소드를 호출하는 메소드를 호출한다는 사실을 알지 못하는 메소드 검사).

동의하지 않습니다. 적어도 일반적으로 아닙니다.

간단한 공장 :

public IFoo GetIFoo()
{
    return new Foo();
}

간단한 주사 :

myDependencyInjector.Bind<IFoo>().To<Foo>();

두 스 니펫은 동일한 목적으로 사용되며 IFoo와 사이에 링크를 설정합니다 Foo. 다른 모든 것은 단지 구문입니다.

변경 Foo하려면 ADifferentFoo두 코드 샘플에서 정확히 많은 노력이 필요합니다.
사람들은 의존성 주입이 다른 바인딩을 사용할 수 있다고 주장하지만, 다른 팩토리를 만드는 것에 대해 동일한 주장을 할 수 있다고 들었습니다. 올바른 바인딩을 선택하는 것은 올바른 팩토리를 선택하는 것만큼이나 복잡합니다.

공장 방법을 사용하면 Foo일부 장소 및 ADifferentFoo다른 장소에서 사용할 수 있습니다 . 어떤 사람들은 이것을 좋은 것으로 부를 수도 있고 (필요한 경우에 유용 할 수도 있습니다), 어떤 사람들은 이것을 나쁜 것으로 부를 수도 있습니다 (모든 것을 교체하는 데 절반의 노력을 기울일 수 있습니다).
그러나 IFoo항상 단일 소스를 갖도록 리턴하는 단일 메소드를 고수하는 경우이 모호성을 피하는 것이 그리 어려운 것은 아닙니다. 발로 자신을 쏘고 싶지 않다면, 장전 된 총을 잡거나 발에 겨냥하지 마십시오.


Dependecy injection은 생성자 인수 목록을 크게 팽창시키고 코드 전체에서 일부 측면을 번집니다.

이런 이유로 일부 사람들은 생성자에서 다음과 같이 명시 적으로 종속성을 검색하는 것을 선호합니다.

public MyService()
{
    _myFoo = DependencyFramework.Get<IFoo>();
}

나는 인수 pro (생성자 부풀림 없음)를 들었고, 인수 con을 들었습니다 (생성자를 사용하면 더 많은 DI 자동화가 가능합니다).

개인적으로 생성자 인수를 사용하려는 선배에게 양보했지만 VS의 드롭 다운 목록 (오른쪽 상단, 현재 클래스의 메소드를 탐색)에서 문제가 발생했을 때 메소드 이름이 보이지 않는 문제를 발견했습니다. 메소드 서명 중 화면이 내 화면보다 깁니다 (=> 부풀린 생성자).

기술적 인 수준에서 나는 어느 쪽이든 상관하지 않습니다. 어느 옵션이든 입력하는 데 많은 노력이 필요합니다. DI를 사용하기 때문에 일반적으로 생성자를 수동으로 호출하지 않습니다. 그러나 Visual Studio UI 버그는 생성자 인수를 부 풀리지 않는 것을 선호합니다.


부수적으로, 의존성 주입과 팩토리는 상호 배타적이지 않습니다 . 종속성을 삽입하는 대신 종속성을 생성하는 팩토리를 삽입 한 경우가 있습니다 (NInject Func<IFoo>를 사용하면 실제로 팩토리 클래스를 만들 필요가 없습니다).

이에 대한 사용 사례는 드물지만 존재합니다.


OP는 정적 팩토리 에 대해 묻습니다 . stackoverflow.com/questions/929021/…
Basilevs

@Basilevs "문제는 이러한 구성 요소를 다른 구성 요소에 제공하는 방법은 무엇입니까?"
Anthony Rutledge

1
@Basilevs 사이드 노트 이외의 모든 내용은 정적 팩토리에도 적용됩니다. 구체적으로 지적하려는 내용이 확실하지 않습니다. 링크는 무엇입니까?
Flater

팩토리를 주입하는 이러한 유스 케이스 중 하나가 추상 HTTP 요청 클래스를 고안하고 GET, POST, PUT, PATCH 및 DELETE를위한 5 개의 다른 다형성 하위 클래스를 전략화 한 사례 일 수 있습니까? 당신은 HTTP 요청 클래스에 부분적으로 의존 할 수있는 MVC 형 라우터 (말했다 클래스 계층 구조를 통합하려고 할 때마다 사용되는 HTTP 방법을 알 수 없다 PSR-7 HTTP 메시지 인터페이스가 밉다..
앤서니 러 틀리지

@AnthonyRutledge : DI 팩토리는 두 가지를 의미 할 수 있습니다. (1) 여러 메소드를 노출하는 클래스. 나는 이것이 당신이 말하는 것이라고 생각합니다. 그러나 이것은 실제로 공장에만 국한된 것은 아닙니다. 공용 메소드가 여러 개인 비즈니스 로직 클래스이든 공용 메소드가 여러 개인 팩토리인지 여부는 의미상의 문제이며 기술적 차이가 없습니다. (2) 공장의 DI 특정 사용 사례는 공장이 아닌 종속성은 한 번 (인젝션 중) 인스턴스화 되는 반면 팩토리 버전은 실제 단계의 종속성을 나중에 (그리고 여러 번) 인스턴스화하는 데 사용할 수 있다는 것입니다.
Flater

0

이 모의 예제에서 팩토리 클래스는 런타임시 HTTP 요청 메소드를 기반으로 인스턴스화 할 인바운드 HTTP 요청 오브젝트의 종류를 판별하는 데 사용됩니다. 팩토리 자체에는 의존성 주입 컨테이너의 인스턴스가 주입됩니다. 이것은 팩토리가 런타임을 결정하고 의존성 주입 컨테이너가 의존성을 처리하게합니다. 각 인바운드 HTTP 요청 오브젝트에는 최소한 네 개의 종속성 (슈퍼 글로벌 및 기타 오브젝트)이 있습니다.

<?php
namespace TFWD\Factories;

/**
 * A class responsible for instantiating
 * InboundHttpRequest objects (PHP 7.x)
 * 
 * @author Anthony E. Rutledge
 * @version 2.0
 */
class InboundHttpRequestFactory 
{
    private const GET = 'GET';
    private const POST = 'POST';
    private const PUT = 'PUT';
    private const PATCH = 'PATCH';
    private const DELETE = 'DELETE';

    private static $di;
    private static $method;

    // public function __construct(Injector $di, Validator $httpRequestValidator)
    // {
    //    $this->di = $di;
    //    $this->method = $httpRequestValidator->getMethod();
    // }

    public static function setInjector(Injector $di)
    {
        self::$di = $di;
    }    

    public static setMethod(string $method)
    {
        self::$method = $method;
    }

    public static function getRequest()
    {
        if (self::$method == self::GET) {
            return self::$di->get('InboundGetHttpRequest');
        } elseif ((self::$method == self::POST) && empty($_FILES)) {
            return self::$di->get('InboundPostHttpRequest');
        } elseif (self::$method == self::POST) {
            return self::$di->get('InboundFilePostHttpRequest');
        } elseif (self::$method == self::PUT) {
            return self::$di->get('InboundPutHttpRequest');
        } elseif (self::$method == self::PATCH) {
            return self::$di->get('InboundPatchHttpRequest');
        } elseif (self::$method == self::DELETE) {
            return self::$di->get('InboundDeleteHttpRequest');
        } else {
            throw new \RuntimeException("Unexpected HTTP request. Invalid request.");
        }
    }
}

centralized 내의 MVC 유형 설정을위한 클라이언트 코드 index.php는 다음과 같습니다 (검증 생략).

InboundHttpRequestFactory::setInjector($di);
InboundHttpRequestFactory::setMethod($httpRequestValidator->getMethod());
$di->set('InboundHttpRequest', InboundHttpRequestFactory::getRequest());
$router = $di->get('Router');  // The Router class depends on InboundHttpRequest objects.
$router->dispatch(); 

또는 팩토리의 정적 특성 (및 키워드)을 제거하고 종속성 인젝터가 전체 항목 (따라서 주석 처리 된 생성자)을 관리하도록 허용 할 수 있습니다. 그러나 클래스 멤버 참조 ( self) 의 일부 (상수 아님 )를 인스턴스 멤버 ( $this)로 변경해야합니다.


주석이없는 다운 보트는 유용하지 않습니다.
Anthony Rutledge
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.