지속성은 순수한 기능 언어에 어떻게 맞습니까?


18

지속성을 처리하기 위해 명령 처리기를 사용하는 패턴은 IO 관련 코드를 가능한 한 얇게 만드는 순수 기능 언어에 어떻게 맞습니까?


객체 지향 언어로 도메인 기반 디자인을 구현할 때는 명령 / 핸들러 패턴 을 사용하여 상태 변경을 실행 하는 것이 일반적 입니다. 이 디자인에서 명령 처리기 는 도메인 개체 위에 위치하며 리포지토리 사용 및 도메인 이벤트 게시와 같은 지루한 지속성 관련 논리를 담당합니다. 핸들러는 도메인 모델의 공개 얼굴입니다. UI와 같은 응용 프로그램 코드는 도메인 개체의 상태를 변경해야 할 때 처리기를 호출합니다.

C #의 스케치 :

public class DiscardDraftDocumentCommandHandler : CommandHandler<DiscardDraftDocument>
{
    IDraftDocumentRepository _repo;
    IEventPublisher _publisher;

    public DiscardDraftCommandHandler(IDraftDocumentRepository repo, IEventPublisher publisher)
    {
        _repo = repo;
        _publisher = publisher;
    }

    public override void Handle(DiscardDraftDocument command)
    {
        var document = _repo.Get(command.DocumentId);
        document.Discard(command.UserId);
        _publisher.Publish(document.NewEvents);
    }
}

document도메인 객체 (( "당신은 이미 폐기 된 것 문서를 폐기 할 수 없습니다"또는 "사용자가 문서를 폐기 할 수있는 권한이 있어야"같은) 비즈니스 규칙을 구현하기위한 우리가 게시하는 데 필요한 도메인 이벤트를 생성을 담당 document.NewEvents것 수 IEnumerable<Event>및 아마 포함됩니다 DocumentDiscarded) 이벤트를.

이것은 멋진 디자인입니다-확장하기 쉽고 (새로운 명령 핸들러를 추가하여 도메인 모델을 변경하지 않고 새로운 사용 사례를 추가 할 수 있습니다) 객체가 어떻게 유지되는지에 대해 불가지론 적입니다 (Mongo를 위해 NHibernate 저장소를 쉽게 바꿀 수 있습니다) 저장소 또는 또는 RabbitMQ 게시자를 EventStore 게시자로 교체) 가짜 및 모의를 사용하여 쉽게 테스트 할 수 있습니다. 또한 모델 / 뷰 분리를 준수합니다. 명령 핸들러는 배치 작업, GUI 또는 REST API에서 사용 중인지 여부를 모릅니다.


Haskell과 같은 순전히 기능적인 언어에서는 다음과 같이 명령 핸들러를 모델링 할 수 있습니다.

newtype CommandHandler = CommandHandler {handleCommand :: Command -> IO Result)
data Result a = Success a | Failure Reason
type Reason = String

discardDraftDocumentCommandHandler = CommandHandler handle
    where handle (DiscardDraftDocument documentID userID) = do
              document <- loadDocument documentID
              let result = discard document userID :: Result [Event]
              case result of
                   Success events -> publishEvents events >> return result
                   -- in an event-sourced model, there's no extra step to save the document
                   Failure _ -> return result
          handle _ = return $ Failure "I expected a DiscardDraftDocument command"

이해하기 위해 고군분투하는 부분이 있습니다. 일반적으로 GUI 또는 REST API와 같은 명령 핸들러를 호출하는 일종의 '표시'코드가 있습니다. 이제 프로그램에 IO를 수행해야하는 두 개의 레이어 (명령 처리기 및 뷰)가 있습니다. 이는 Haskell에서 가장 중요합니다.

내가 알아낼 수있는 한, 여기에는 두 가지 상반되는 힘이 있습니다. 하나는 모델 / 뷰 분리이고 다른 하나는 모델을 유지할 필요가 있습니다. 모델을 어딘가에 유지하려면 IO 코드가 필요 하지만 모델 / 뷰 분리에서는 다른 모든 IO 코드와 함께 프레젠테이션 계층에 모델을 넣을 수 없다고 말합니다.

물론 "일반적인"언어에서 IO는 어디서나 발생할 수 있습니다. 좋은 디자인은 서로 다른 유형의 IO를 별도로 유지하도록 지시하지만 컴파일러는이를 강제하지 않습니다.

따라서 : 모델을 유지해야 할 때 IO 코드를 프로그램의 최첨단으로 푸시하려는 욕구로 모델 / 뷰 분리를 어떻게 조정합니까? 우리는 어떻게 서로 다른 두 가지 유형의 IO를 개별적으로 유지하면서 모든 순수한 코드에서 멀리 떨어져 있습니까?


업데이트 : 바운티가 24 시간 이내에 만료됩니다. 나는 현재 답변 중 하나가 내 질문을 전혀 다루지 않았다고 생각하지 않습니다. @ Ptharien 's Flame의 의견은 acid-state유망한 것으로 보이지만 답변이 아니며 세부 정보가 부족합니다. 이 포인트가 낭비되는 것을 싫어합니다!


1
아마도 Haskell에서 다양한 퍼시스턴스 라이브러리의 디자인을 보는 것이 도움이 될 것입니다. 특히, acid-state당신이 묘사하는 것에 가깝습니다 .
Ptharien 's Flame

1
acid-state그 링크에 감사드립니다. API 디자인의 관점에서는 여전히 바인딩 된 것처럼 보입니다 IO. 내 질문은 지속성 프레임 워크가 더 큰 아키텍처에 어떻게 적용되는지에 관한 것입니다. acid-state프리젠 테이션 레이어와 함께 사용 하고 두 개를 분리하여 유지하는 데 성공한 오픈 소스 응용 프로그램에 대해 알고 있습니까?
Benjamin Hodgson

QueryUpdate모나드는 꽤 멀리에서 제거됩니다 IO사실. 대답에 간단한 예를 제시하려고 노력할 것입니다.
Ptharien 's Flame

이 방법으로 명령 / 처리기 패턴을 사용하는 독자에게는 주제가 맞지 않을 수 있으므로 Akka.NET을 확인하는 것이 좋습니다. 액터 모델은 여기에 잘 맞는 느낌입니다. Pluralsight에는 훌륭한 코스가 있습니다. (저는 홍보용 로봇이 아니라 팬보이에 불과하다고 맹세합니다.)
RJB

답변:


6

Haskell에서 구성 요소를 분리하는 일반적인 방법은 모나드 변압기 스택을 사용하는 것입니다. 아래에서 더 자세히 설명합니다.

대규모 구성 요소가 여러 개인 시스템을 구축한다고 상상해보십시오.

  • 디스크 또는 데이터베이스와 통신하는 구성 요소 (하위 모델)
  • 도메인 (모델) 에서 변환을 수행하는 구성 요소
  • 사용자와 상호 작용하는 구성 요소 (보기)
  • 뷰, 모델 및 하위 모델 (컨트롤러) 간의 연결을 설명하는 구성 요소
  • 전체 시스템을 시작하는 구성 요소 (드라이버)

우리는 좋은 코드 스타일을 유지하기 위해이 컴포넌트들을 느슨하게 연결해야한다고 결정합니다.

따라서 다양한 MTL 클래스를 사용하여 각 구성 요소를 다형성으로 코딩하여 다음을 수행합니다.

  • 하위 모델의 모든 기능은 유형입니다 MonadState DataState m => Foo -> Bar -> ... -> m Baz
    • DataState 데이터베이스 또는 스토리지 상태의 스냅 샷을 완벽하게 표현한 것입니다.
  • 모델의 모든 기능은 순수합니다
  • 보기의 모든 기능은 유형입니다 MonadState UIState m => Foo -> Bar -> ... -> m Baz
    • UIState 사용자 인터페이스 상태의 스냅 샷을 완벽하게 표현한 것입니다.
  • 컨트롤러의 모든 기능은 유형입니다 MonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
    • 컨트롤러는 뷰 상태와 하위 모델 상태에 모두 액세스 할 수 있습니다.
  • 드라이버는 오직 하나의 정의 만 가지고 main :: IO ()있는데, 이것은 다른 구성 요소를 하나의 시스템으로 결합하는 거의 간단한 작업을 수행
    • 뷰와 서브 모델은 컨트롤러 zoom또는 이와 유사한 결합기를 사용하여 컨트롤러와 동일한 상태 유형으로 들어 올려야합니다.
    • 모델은 순수하므로 제한없이 사용할 수 있습니다
    • 결국 모든 것이 (호환 가능한 유형) StateT (DataState, UIState) IO에 있으며, 데이터베이스 또는 스토리지의 실제 내용으로 실행됩니다 IO.

1
이것은 훌륭한 조언이며 정확히 내가 찾던 것입니다. 감사!
Benjamin Hodgson

2
이 답변을 요약하고 있습니다. 이 아키텍처에서 '서브 모델'의 역할을 명확히 설명해 주시겠습니까? IO를 수행하지 않고 어떻게 "디스크 또는 데이터베이스와 통신"합니까? " DataState데이터베이스 또는 스토리지 상태의 스냅 샷을 순수하게 표현한 것입니다 "라는 말의 의미에 대해 특히 혼란스러워 합니다. 아마도 당신은 전체 데이터베이스를 메모리에로드한다는 의미는 아닙니다!
Benjamin Hodgson

1
이 논리의 C # 구현에 대한 당신의 생각을 절대적으로보고 싶습니다. 내가 당신에게 공감대에게 뇌물을 줄 수 있다고 생각하지 않습니까? ;-)
RJB

1
@RJB 불행히도, C # 개발 팀에게 뇌물이 없으면 높은 수준의 언어를 허용해야합니다.
Ptharien 's Flame

4

따라서 : 모델을 유지해야 할 때 IO 코드를 프로그램의 최첨단으로 푸시하려는 욕구로 모델 / 뷰 분리를 어떻게 조정합니까?

모델을 유지해야합니까? 많은 프로그램에서 상태를 예측할 수 없기 때문에 모델 저장이 필요합니다. 모든 작업은 어떤 방식 으로든 모델을 변경시킬 수 있으므로 모델의 상태를 알 수있는 유일한 방법은 모델에 직접 액세스하는 것입니다.

시나리오에서 일련의 이벤트 (검증 및 승인 된 명령)가 항상 상태를 생성 할 수있는 경우 반드시 상태가 아닌 지속되어야하는 이벤트입니다. 이벤트를 재생하여 상태를 항상 생성 할 수 있습니다.

말하지만, 종종 상태가 저장되지만 필수 프로그램 데이터가 아닌 명령 재생을 피하기 위해 스냅 샷 / 캐시로 저장됩니다.

이제 프로그램에 IO를 수행해야하는 두 개의 레이어 (명령 처리기 및 뷰)가 있습니다. 이는 Haskell에서 가장 중요합니다.

명령이 승인 된 후, 이벤트는 두 개의 목적지 (이벤트 저장 영역 및보고 시스템)와 동일한 프로그램 계층에 전달됩니다.

참조
이벤트 소싱
열망 읽기 유도는


2
나는 이벤트 소싱에 익숙하고 (위의 예제에서 사용하고 있습니다!) 머리카락이 나뉘어지는 것을 피하기 위해 이벤트 소싱은 지속성의 문제에 대한 접근 방식이라고 여전히 말하고 싶습니다. 어쨌든 이벤트 소싱 으로 인해 명령 핸들러에 도메인 객체를로드 할 필요가 없습니다 . 명령 핸들러는 오브젝트가 이벤트 스트림, ORM 또는 스토어드 프로 시저에서 왔는지 여부를 알지 못합니다. 리포지토리에서 가져옵니다.
Benjamin Hodgson

1
이해는 뷰와 명령 핸들러를 함께 결합하여 여러 IO를 만드는 것으로 보입니다. 내 이해는 핸들러가 이벤트를 생성하고 더 이상 관심이 없다는 것입니다. 이 인스턴스의보기는 기술적으로 동일한 응용 프로그램에 있더라도 별도의 모듈로 작동하며 명령 처리기에 연결되지 않습니다.
FMJaguar

1
나는 우리가 교차 목적으로 이야기하고 있다고 생각합니다. 'view'라고 말할 때 REST API 또는 모델 뷰 컨트롤러 시스템 일 수있는 전체 프리젠 테이션 레이어에 대해 이야기하고 있습니다. (보기는 MVC 패턴으로 모델에서 분리되어야한다는 데 동의합니다.) 기본적으로 "명령 처리기를 호출하는 모든 것"을 의미합니다.
Benjamin Hodgson

2

모든 비 IO 활동을 위해 IO 집약적 애플리케이션에 공간을 두려고합니다. 불행히도 일반적인 CRUD 앱은 IO 이외의 다른 작업은 거의 없습니다.

관련 분리를 잘 이해하고 있다고 생각하지만 프리젠 테이션 코드에서 멀리 떨어진 여러 계층에 퍼시스턴스 IO 코드를 배치하려고 할 때 문제의 일반적인 사실은 컨트롤러에서 호출 해야하는 곳입니다. 퍼시스턴스 레이어는 프리젠 테이션에 너무 가깝게 느껴질 수 있습니다. 그러나 이는 해당 유형의 앱에서 우연의 일치 일뿐입니다.

프레젠테이션과 지속성은 기본적으로 여기에 설명 된 앱 유형의 전체를 구성합니다.

복잡한 비즈니스 논리 및 데이터 처리가 많은 유사한 응용 프로그램에 대해 생각하면 프레젠테이션 IO 및 지속성 IO와 어떻게 분리되는지 상상할 수 있습니다. 그것에 대해 아무것도 알 필요가 없습니다. 당신이 지금 가지고있는 문제는 그 문제가 시작되지 않는 응용 프로그램 유형의 문제에 대한 해결책을 보려고 시도함으로써 발생하는 지각적인 문제 일뿐입니다.


1
CRUD 시스템이 지속성과 프리젠 테이션을 결합해도된다고 말하는 것입니다. 이것은 나에게 합리적이다. 그러나 나는 CRUD에 대해서는 언급하지 않았다. 비즈니스 오브젝트가 복잡한 상호 작용, 지속성 계층 (명령 처리기) 및 프레젠테이션 계층을 포함하는 DDD에 대해 구체적으로 묻습니다. IO 래퍼 를 유지하면서 두 IO 레이어를 어떻게 별도로 유지 합니까?
Benjamin Hodgson

1
NB, 질문에 설명 된 도메인 매우 복잡 할 수 있습니다. 초안 문서를 폐기하면 일부 권한 검사가 적용되거나 동일한 초안의 여러 버전을 처리하거나 알림을 보내거나 조치를 다른 사용자의 승인이 필요하거나 초안이 여러 단계를 거쳐야 할 수 있습니다. 종료 전 라이프 사이클 단계 ...
Benjamin Hodgson

2
@ BenjaminHodgson 나는 DDD 또는 다른 본질적으로 OO 디자인 방법을 머리 속에이 상황에 섞지 말 것을 강력히 권합니다. 혼란 될 것입니다. 예, 순수한 FP에서 비트 및 보블과 같은 객체를 만들 수는 있지만이를 기반으로하는 디자인 방식이 반드시 첫 번째 도달 범위 인 것은 아닙니다. 이 시나리오에서는 위에서 언급했듯이 두 IO와 순수 코드 사이에서 통신하는 컨트롤러 인 프레젠테이션 IO에 대해 설명합니다. 프리젠 테이션 IO는 컨트롤러로 이동하여 컨트롤러에서 요청하며 컨트롤러는 모든 것을 순수 섹션과 지속성 섹션으로 전달합니다.
Jimmy Hoffa

1
@ BenjaminHodgson 당신은 당신이 좋아하는 디자인에 원하는 모든 레이어와 공상과 함께 모든 순수한 코드가 살고있는 거품을 상상할 수 있습니다. 이 거품의 시작점은 프레젠테이션, 지속성 및 순수한 부분 사이의 통신을 수행하는 "컨트롤러"라고 부르는 작은 조각이 될 것입니다. 이런 식으로 당신의 영속성은 프리젠 테이션에 대해 아무것도 알지 못하며 그 반대도 마찬가지입니다. 그리고 이것은 당신의 IO 시스템을 순수한 시스템의 버블 위의 얇은 층에 유지합니다.
Jimmy Hoffa

2
@BenjaminHodgson이 "스마트 오브젝트"접근 방식은 본질적으로 FP에 대한 나쁜 접근 방식입니다. FP의 스마트 오브젝트의 문제점은 너무 많이 결합하고 너무 적게 일반화한다는 것입니다. FP는 데이터와 기능이 느슨하게 결합되어 기능을 일반화하고 여러 유형의 데이터를 처리 할 수 ​​있도록하는 기능을 선호합니다. 내 대답을 읽으십시오 : programmers.stackexchange.com/questions/203077/203082#203082
Jimmy Hoffa

1

내가 당신의 질문을 이해할 수있는 한 (내가 아닐 수도 있지만 내 2 센트를 던질 것이라고 생각했다), 당신은 객체 자체에 액세스 할 필요가 없기 때문에 자체 객체 데이터베이스를 가지고 있어야합니다. 시간이 지남에 따라 만료됩니다).

이상적으로 객체 자체는 상태를 저장하도록 향상되어 "통과"될 때 다른 명령 프로세서가 작업중인 것을 알 수 있습니다.

이것이 가능하지 않은 경우 (icky icky) 유일한 방법은 일반적인 DB와 같은 키를 사용하는 것입니다. 다른 명령 사이에서 공유 할 수 있도록 설정된 저장소에 정보를 저장하는 데 사용할 수 있습니다. 인터페이스 및 / 또는 코드를 "열어"다른 명령 작성자도 메타 정보 저장 및 처리에 인터페이스를 채택합니다.

파일 서버 영역에서 samba는 호스트 OS가 제공하는 것에 따라 액세스 목록 및 대체 데이터 스트림과 같은 항목을 저장하는 다양한 방법을 가지고 있습니다. 이상적으로, samba는 파일 시스템에서 호스팅되며 파일에 대한 확장 된 속성을 제공합니다. 'linux'의 'xfs'예-더 많은 명령이 파일과 함께 확장 된 속성을 복사하는 것입니다 (기본적으로 Linux의 대부분의 utils는 확장 된 속성처럼 생각하지 않습니다).

공통 파일 (객체)에서 작업하는 서로 다른 사용자의 여러 삼바 프로세스에서 작동하는 대체 솔루션은 파일 시스템이 확장 속성과 같이 파일에 직접 리소스 연결을 지원하지 않는 경우 구현하는 모듈을 사용한다는 것입니다 삼바 프로세스에 대한 확장 된 속성을 에뮬레이트하는 가상 파일 시스템 계층 삼바 만이 그것에 대해 알고 있지만, 객체 형식이이를 지원하지 않을 때 작동하는 이점이 있지만, 이전 상태에 따라 파일에서 일부 작업을 수행하는 다양한 삼바 사용자 (명령 프로세서 참조)와 여전히 작동합니다. 메타 정보를 파일 시스템의 공통 데이터베이스에 저장하여 데이터베이스 크기를 제어하는 ​​데 도움을줍니다.

작업중인 구현에 고유 한 추가 정보가 필요한 경우에는 유용하지 않지만 개념적으로 동일한 이론이 두 문제 세트 모두에 적용될 수 있습니다. 따라서 원하는 작업을 수행하는 알고리즘과 방법을 찾고 있다면 도움이 될 것입니다. 특정 프레임 워크에서 더 구체적인 지식이 필요하다면 도움이되지 않을 수도 있습니다 ... ;-)

BTW-내가 '자기 만료'를 언급하는 이유는 어떤 객체가 있는지, 얼마나 오래 지속되는지 알면 명확하지 않기 때문입니다. 객체가 삭제되는 시점을 직접 알 수있는 방법이 없다면 사용자가 오랫동안 객체를 삭제 한 오래된 또는 고대 메타 정보로 채워지지 않도록 자체 메타 DB를 잘라야합니다.

개체가 만료 / 삭제되는시기를 알고 있다면 게임보다 앞서서 메타 DB에서 동시에 만료 할 수 있지만 해당 옵션이 있는지 확실하지 않습니다.

건배!


1
나에게 이것은 완전히 다른 질문에 대한 답변처럼 보입니다. 도메인 기반 디자인의 맥락에서 순수 기능 프로그래밍의 아키텍처에 대한 조언을 찾고있었습니다. 당신의 요점을 명확하게 설명해 주시겠습니까?
Benjamin Hodgson

순전히 기능적인 프로그래밍 패러다임에서 데이터 지속성에 대해 묻고 있습니다. 인용 위키피디아 : "순수하게 기능하는 것은 프로그램 실행 환경에서 엔티티의 파괴적인 수정 (업데이트)을 배제하는 알고리즘, 데이터 구조 또는 프로그래밍 언어를 설명하는 데 사용되는 컴퓨팅 용어입니다." ==== 정의에 따르면, 데이터 지속성은 관련이 없으며 데이터를 수정하지 않는 어떤 것에도 사용되지 않습니다. 엄밀히 말하면 귀하의 질문에 대한 답변이 없습니다. 나는 당신이 쓴 것에 대해 더 느슨한 해석을 시도하고있었습니다.
Astara
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.