기능성 프로그래밍이 의존성 주입 패턴에 대한 대안입니까?


21

최근 에 C #에서 Functional Programming 이라는 제목의 책을 ​​읽었으며, 함수 프로그래밍 의 변경 불가능하고 상태 비 저장 특성은 종속성 주입 패턴과 유사한 결과를 달성하며 특히 단위 테스트와 관련하여 더 나은 접근 방식 일 수 있습니다.

두 가지 접근 방식을 모두 경험 한 사람이 자신의 생각과 경험을 공유하여 주된 질문에 대답 할 수 있다면 감사 할 것입니다 .


10
이것은 나에게별로 이해가되지 않으며, 불변성은 의존성을 제거하지 않습니다.
Telastyn

종속성을 제거하지 않는다는 데 동의합니다. 아마도 내 이해가 올바르지 않을 수도 있지만 원래 객체를 변경할 수 없으면 그것을 사용하는 모든 함수에 전달해야합니다.
Matt Cashatt


5
또한 OO 프로그래머를 사랑하는 기능적 프로그래밍으로 속이는 방법 도 있는데 , 이는 실제로 OO와 FP 관점에서 DI에 대한 자세한 분석입니다.
Robert Harvey

1
이 질문, 링크 된 기사 및 허용되는 답변도 유용 할 수 있습니다. stackoverflow.com/questions/11276319/… 무서운 Monad 단어는 무시하십시오. Runar가 그의 답변에서 지적했듯이,이 경우에는 복잡한 개념이 아닙니다 (단지 함수).
itsbruce

답변:


27

종속성 관리는 다음 두 가지 이유로 OOP에서 큰 문제입니다.

  • 데이터와 코드의 긴밀한 결합.
  • 부작용의 유비쿼터스 사용.

대부분의 OO 프로그래머는 데이터와 코드의 긴밀한 결합이 전적으로 유익하다고 생각하지만 비용이 따릅니다. 계층을 통한 데이터 흐름 관리는 패러다임에서 프로그래밍의 불가피한 부분입니다. 데이터와 코드를 결합 하면 특정 지점에서 함수 를 사용하려는 경우 객체를 해당 지점으로 가져 오는 방법을 찾아야 한다는 추가적인 문제가 추가 됩니다.

부작용을 사용하면 비슷한 어려움이 발생합니다. 일부 기능에 부작용을 사용하지만 해당 구현을 스왑 아웃하려면 해당 종속성을 주입하는 것 외에 다른 선택의 여지가 없습니다.

예를 들어 이메일 주소를 위해 웹 페이지를 긁어 내고 이메일을 보내는 스패머 프로그램을 고려하십시오. DI 마인드가 있다면 지금 당장 인터페이스 뒤에 캡슐화 할 서비스와 어떤 서비스를 어디에 삽입 할 것인지 생각하고 있습니다. 나는 그 디자인을 독자들을위한 연습으로 남겨 둘 것이다. FP 마인드가 있다면 지금 당장 다음과 같은 가장 낮은 기능 계층에 대한 입력 및 출력을 생각합니다.

  • 웹 페이지 주소를 입력하고 해당 페이지의 텍스트를 출력하십시오.
  • 페이지의 텍스트를 입력하고 해당 페이지의 링크 목록을 출력하십시오.
  • 페이지의 텍스트를 입력하고 해당 페이지의 이메일 주소 목록을 출력하십시오.
  • 이메일 주소 목록을 입력하고 중복이 제거 된 이메일 주소 목록을 출력하십시오.
  • 이메일 주소를 입력하고 해당 주소에 대한 스팸 이메일을 출력하십시오.
  • 스팸 이메일을 입력하고 SMTP 명령을 출력하여 해당 이메일을 보냅니다.

입력 및 출력과 관련하여 생각할 때 함수 종속성이없고 데이터 종속성 만 있습니다. 그것이 단위 테스트를 매우 쉽게 만드는 것입니다. 다음 계층은 한 함수의 출력이 다음 함수의 입력으로 공급되도록 준비하고 필요에 따라 다양한 구현을 쉽게 교체 할 수 있습니다.

매우 실제적인 의미에서, 함수형 프로그래밍은 자연스럽게 함수 종속성을 항상 반전 시키도록 자극하므로 일반적으로 사실 후에 특별한 조치를 취할 필요가 없습니다. 그렇게하면 고차 함수, 클로저 및 부분 적용과 같은 도구를 사용하여 보일러 플레이트를 줄이면 더 쉽게 달성 할 수 있습니다.

문제가되는 것은 의존성 자체가 아닙니다. 잘못된 길가리키는 의존성입니다 . 다음 레이어는 다음과 같은 기능을 할 수 있습니다.

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

하위 계층 기능을 함께 붙이는 것이 유일한 목적이기 때문에이 계층이 이와 같이 하드 코딩 된 종속성을 갖는 것은 괜찮습니다. 구현 스왑은 다른 컴포지션을 만드는 것만 큼 간단합니다.

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

이 쉬운 재구성은 부작용이 없어서 가능합니다. 하위 계층 기능은 서로 완전히 독립적입니다. 다음 계층 processText은 일부 사용자 구성에 따라 실제로 사용되는 것을 선택할 수 있습니다 .

actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText

다시 말하지만, 모든 종속성이 한 방향을 가리 키기 때문에 문제가되지 않습니다. 순수한 함수는 이미 우리에게 그렇게하도록 강요했기 때문에 의존성을 모두 같은 방식으로 가리 키기 위해 의존성을 뒤집을 필요는 없습니다.

config맨 위를 확인하는 대신 가장 낮은 레이어 로 전달 하여 더 많이 결합 할 수 있습니다. FP는이 작업을 방해하지는 않지만 시도하면 더 성가신 경향이 있습니다.


3
"부작용을 사용하는 경우에도 비슷한 어려움이 발생합니다. 일부 기능에 부작용을 사용하지만 해당 기능을 구현할 수있게하려면 해당 종속성을 주입 할 수밖에 없습니다." 부작용과 관련이 있다고 생각하지 않습니다. Haskell에서 구현을 바꾸려면 여전히 의존성 주입을 수행해야 합니다. 타입 클래스를 제거하고 모든 함수에 대한 첫 번째 인수로 인터페이스를 전달합니다.
Doval

2
문제의 핵심은 거의 모든 언어가 다른 코드 모듈에 대한 참조를 하드 코딩하도록 강요하므로 구현을 바꾸는 유일한 방법은 어디에서나 동적 디스패치를 ​​사용하는 것이므로 런타임에 종속성을 해결해야합니다. 모듈 시스템을 사용하면 유형 확인시 종속성 그래프를 표현할 수 있습니다.
Doval

@ Doval-- 재미 있고 생각을 자극하는 의견에 감사드립니다. 나는 당신을 오해했을지도 모르지만, DI 스타일 (전통적인 C # 의미에서)을 통해 기능적 스타일의 프로그래밍을 사용한다면 런타임과 관련된 디버깅 좌절을 피할 수 있다고 귀하의 의견에서 유추하는 것이 맞습니다 의존성의 해결?
Matt Cashatt

@MatthewPatrickCashatt 스타일이나 패러다임이 아니라 언어 기능에 관한 문제입니다. 언어가 모듈을 일류로 지원하지 않으면 종속성을 정적으로 표현할 방법이 없으므로 스왑 구현을 위해 동적 디스패치 및 종속성 주입을 수행해야합니다. 약간 다르게 말하자면 C # 프로그램에서 문자열을 사용하는 경우에 하드 코딩 된 종속성이 있습니다 System.String. 모듈 시스템을 사용하면 System.String문자열 구현의 선택이 하드 코딩되지 않지만 컴파일 타임에 여전히 해결되도록 변수로 바꿀 수 있습니다 .
Doval

8

함수형 프로그래밍은 의존성 주입 패턴에 대한 실행 가능한 대안입니까?

이것은 나를 이상한 질문으로 생각합니다. 함수형 프로그래밍 접근법은 의존성 주입에 크게 접합니다.

물론, 불변의 상태를 가짐으로써 부작용을 갖거나 클래스 상태를 함수 사이의 암묵적 계약으로 사용하여 "속임수"가되지 않도록 할 수 있습니다. 그것은 데이터의 전달을보다 명확하게 만듭니다. 이것은 가장 기본적인 형태의 의존성 주입이라고 가정합니다. 그리고 함수를 전달하는 함수형 프로그래밍 개념으로 훨씬 쉽게 할 수 있습니다.

그러나 의존성을 제거하지는 않습니다. 운영 상태는 변경 가능할 때 필요한 모든 데이터 / 작업이 여전히 필요합니다. 그리고 당신은 여전히 ​​어떻게 그러한 의존성을 가져와야합니다. 따라서 함수형 프로그래밍 접근 방식이 DI를 전혀 대체 한다고 말하지 는 않으므로 대안이 아닙니다.

OO 코드가 암묵적 종속성을 생성하여 프로그래머가 거의 생각하지 않는 것보다 OO 코드가 얼마나 나쁜지를 보여주었습니다.


대화에 기여한 Telastyn에게 다시 한 번 감사드립니다. 당신이 지적했듯이, 내 질문은 잘 구성되지 않았습니다 (내 말). 여기의 피드백 덕분에 나는이 모든 것에 대해 내 두뇌에 불이 붙는 것이 무엇인지 조금 더 잘 이해하기 시작했습니다. 우리 모두 동의합니다 DI가 없으면 단위 테스트가 악몽이 될 수 있다고 생각합니다. 불행히도, 특히 IoC 컨테이너와 함께 DI를 사용하면 런타임에 종속성을 해결한다는 사실 때문에 새로운 형태의 디버깅 악몽을 만들 수 있습니다. DI와 유사하게 FP는 단위 테스트를보다 쉽게 ​​수행 할 수 있지만 런타임 종속성 문제는 없습니다.
Matt Cashatt

(위에서 계속). . 이것은 어쨌든 나의 현재 이해입니다. 마크가없는 경우 알려주십시오. 나는 내가 거인들 사이에서 단순한 필사자라는 것을 인정하지 않는다!
Matt Cashatt

@MatthewPatrickCashatt-DI는 런타임 종속성 문제를 의미하지는 않습니다.
Telastyn

7

귀하의 질문에 빠른 답은 없습니다 .

그러나 다른 사람들이 주장한 것처럼,이 질문은 다소 관련이없는 두 가지 개념과 결혼합니다.

이 단계를 단계별로 수행하겠습니다.

비 기능적 스타일의 DI 결과

함수 프로그래밍의 핵심에는 순수한 함수-입력을 출력에 매핑하는 함수가 있으므로 주어진 입력에 대해 항상 동일한 출력을 얻습니다.

DI는 일반적으로 출력이 주입에 따라 달라질 수 있으므로 장치가 더 이상 순수하지 않음을 의미합니다. 예를 들어 다음 기능에서

const bookSeats = ( seatCount, getBookedSeatCount ) => { ... }

getBookedSeatCount(함수)는 동일한 주어진 입력에 대해 다른 결과를 산출 할 수 있습니다. 이것은 bookSeats또한 불완전합니다.

여기에는 예외가 있습니다. 다른 알고리즘을 사용하더라도 동일한 입력-출력 매핑을 구현하는 두 가지 정렬 알고리즘 중 하나를 삽입 할 수 있습니다. 그러나 이것들은 예외입니다.

시스템은 순수 할 수 없습니다

시스템이 순수 할 수 없다는 사실은 기능적 프로그래밍 소스에서 주장한 것과 동일하게 무시됩니다.

시스템은 명백한 예와 함께 부작용이 있어야합니다.

  • UI
  • 데이터 베이스
  • API (클라이언트 서버 아키텍처)

따라서 시스템의 일부에는 부작용이 포함되어야하며 해당 부분에는 명령형 스타일 또는 OO 스타일도 포함될 수 있습니다.

쉘 코어 패러다임

Gary Bernhardt의 탁월한 경계에 관한 용어를 빌려 보면 , 좋은 시스템 (또는 모듈) 아키텍처에는 다음 두 계층이 포함됩니다.

  • 핵심
    • 순수한 기능
    • 분기
    • 의존성 없음
  • 껍질
    • 불순 (부작용)
    • 분기 없음
    • 의존성
    • OO 스타일 등이 필요합니다.

핵심 요소는 시스템을 순수한 부분 (핵심)과 불순한 부분 (쉘)으로 '분할'하는 것입니다.

약간의 결함이있는 솔루션 (및 결론)을 제공하지만 이 Mark Seemann의 기사 는 매우 동일한 개념을 제안합니다. Haskell 구현은 FP를 사용하여 모두 수행 할 수 있음을 보여 주므로 특히 통찰력이 있습니다.

DI와 FP

대부분의 응용 프로그램이 순수한 경우에도 DI를 사용하는 것이 합리적입니다. 핵심은 불순한 껍질 안에 DI를 가두는 것입니다.

예를 들어 API 스텁이 있습니다. 프로덕션에서는 실제 API를 원하지만 테스트에는 스텁을 사용하십시오. 쉘 코어 모델을 준수하면 여기에서 많은 도움이 될 것입니다.

결론

따라서 FP와 DI는 정확한 대안이 아닙니다. 시스템에 둘 다있을 수 있으며 FP와 DI가 각각 상주하는 시스템의 순수한 부분과 불순한 부분을 분리하는 것이 좋습니다.


쉘 코어 패러다임을 언급 할 때 어떻게 쉘에서 분기를 달성하지 못합니까? 응용 프로그램이 값을 기준으로 하나의 불순한 일을 해야하는 많은 예를 생각할 수 있습니다. 이 분기 금지 규칙이 Java와 같은 언어에 적용 가능합니까?
jrahhali

@jrahhali 자세한 내용은 Gary Bernhardt 's Talk를 참조하십시오 (답변에 링크되어 있음).
이자키

또 다른 관련 Seemann 시리즈 blog.ploeh.dk/2017/01/27/…
jk.

1

OOP 관점에서 함수는 단일 메소드 인터페이스로 간주 될 수 있습니다.

인터페이스는 함수보다 강력한 계약입니다.

기능적 접근 방식을 사용하고 DI를 많이 사용하는 경우 OOP 접근 방식을 사용하는 것과 비교하여 각 종속성에 대해 더 많은 후보를 얻게됩니다.

void DoStuff(Func<DateTime> getDateTime) {}; //Anything that satisfies the signature can be injected.

vs

void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.

3
인터페이스를 구현하기 위해 모든 클래스를 래핑 할 수 있으므로 "강력한 계약"이 그다지 강력하지 않습니다. 더 중요한 것은 각 함수에 다른 유형을 지정하면 함수 구성을 수행하는 것이 불가능하다는 것입니다.
Doval

함수형 프로그래밍은 "고차 함수를 사용한 프로그래밍"을 의미하는 것이 아니라 훨씬 넓은 개념을 의미하며 고차 함수는 패러다임에 유용한 기술 중 하나 일뿐입니다.
Jimmy Hoffa
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.