作为一个将我的大脑包裹在Haskell类型和类型类中的练习,我试图实现一个简单的DDD / CQRS样式解决方案。我在Lev Gorodinski's F# Simple CQRS实施后直接对其进行建模。
我实施了一个非常简单的Vehicle
聚合,这或多或少是Lev InventoryItem
聚合
module Vehicle where
data State = State { isActive :: Bool } deriving (Show)
zero = State { isActive = True }
type Mileage = Int
data Command =
Create String Mileage
| UpdateMileage Mileage
| Deactivate
deriving (Show)
data Event =
Created String
| MileageUpdated Mileage
| Deactivated
deriving (Show)
-- Define transitions from a command to one or more events
execute :: State -> Command -> [Event]
execute state (Create name mileage) = [Created name, MileageUpdated mileage]
execute state (UpdateMileage mileage) = [MileageUpdated mileage]
execute state Deactivate = [Deactivated]
-- Apply an event against the current state to get the new state
apply :: State -> Event -> State
apply state (Created _) = state
apply state (MileageUpdated _) = state
apply state Deactivated = state { isActive = False }
我试图弄清楚的部分是如何为域聚合创建更高级别的抽象,因为所有聚合都将由相同的组件部分组成。在Lev的例子中,他定义了一个类型Aggregate<'TState, 'TCommand, 'TEvent>
,它允许他定义一个通用命令处理程序,它可以对付域聚合的任何实例。在Haskell中,这感觉就像我应该使用Typeclasses。当然,因为我不知道我在做什么,这可能是对它们用途的完全误解。
我的想法是Aggregate
类型类将定义execute
和apply
命令的接口,以及某种类型需要关联类型来表示其状态,命令,和事件。从那里我可以定义一些通用命令处理程序,它能够对聚合的任何实例执行命令。例如
class Aggregate a where =
execute :: state -> command -> [event]
apply :: state -> event -> state
其中state
,command
和event
是表示给定Aggregate
实例的状态,命令和事件的类型变量。当然,这不起作用。
我是否正在尝试正确使用类型类?如果是这样,我应该如何定义类,使Aggregate
的实例必须具有相应的State,Command和Event类型?
如果这是错误的方法,我应该如何定义Aggregate
以创建更高级别的抽象?
更新
按照@ jberryman的建议,我使用MPTC来定义我的Aggregate,这允许我创建我正在寻找的通用命令处理程序:
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
module Aggregate where
class Aggregate state command event
| state -> command event, command -> state event, event -> state command where
execute :: state -> command -> [event]
apply :: state -> event -> state
zero :: state
makeHandler (load, commit) =
(\ (id, expectedVersion) command ->
do events <- load id
let state = foldl apply zero events
let newEvents = execute state command
commit (id, expectedVersion) newEvents)
这导致我的Vehicle
模块中的以下实例声明
instance Aggregate State Command Event where
execute = Vehicle.execute
apply = Vehicle.apply
zero = Vehicle.zero
然后是一个示例脚本将它们捆绑在一起:
import Aggregate
import Vehicle
-- Mock out IO to a domain repository (EventStore)
load :: Int -> IO [Event]
load id = do return [Created "Honda", MileageUpdated 15000]
commit (id, expectedVersion) events = putStrLn (show events)
-- Create the handler provide a command to it
handler = makeHandler (load, commit)
handler (1,1) Deactivate
答案 0 :(得分:4)
如果state
,command
和event
是特定实例所特有的三元组,则可以MultiParameterTypeClasses
与FunctionalDependencies
一样使用1} p>
class Aggregate state command event | state -> command event, command -> state event, event -> state command where
execute :: state -> command -> [event]
apply :: state -> event -> state
|
读取后的fundeps:&#34;其中命令和事件由状态唯一确定,状态和事件由命令唯一确定,并且......&#34;如果我们可以推断实例头中的任何一种类型,这允许解析实例。你可以对类型系列做同样的事情。
但是在定义类型类之前你应该问自己三个问题:
如果答案对所有这些都不是,那么你可能不应该定义一个类型类。很难回答这是否属于这种情况。
但它可能是例如你真正想要的是:
data Aggregate state command where
Aggregate :: (state -> command -> [event]) -> (state -> event -> state) -> Aggregate state command
这是一个隐藏event
的GADT。
答案 1 :(得分:4)
我不熟悉Lev Gorodinski的F#示例,因此不能保证指向正确的方向,但我看一下它,看起来它的Aggregate
类型的一个用途是到{{3}}。如果我们忽略这些函数所暗示的所有不纯操作,核心操作似乎是:
因此,受@ jberryman的回答启发,您可以定义类似的类型类:
class Aggregate state command event | event -> state command where
eventsToState :: [event] -> state
execute :: state -> command -> [event]
为了编译它,您需要FunctionalDependencies
语言扩展名。
现在,您可以基于该类型类编写处理函数的核心作为泛型函数:
handle :: Aggregate state command event => command -> [event] -> [event]
handle command = flip execute command . eventsToState
您现在可以为OP中定义的类型定义Aggregate
的实例:
instance Aggregate State Command Event where
eventsToState = foldl folder zero
where
folder state (Created _) = state
folder state (MileageUpdated _) = state
folder state Deactivated = state { isActive = False }
execute _ (Create name mileage) = [Created name, MileageUpdated mileage]
execute _ (UpdateMileage mileage) = [MileageUpdated mileage]
execute _ Deactivate = [Deactivated]
在GHCI中尝试:
*Vehicle> handle (Create "BMW 520i" 250000) [] :: [Event]
[Created "BMW 520i",MileageUpdated 250000]
*Vehicle> handle (UpdateMileage 256000) it
[MileageUpdated 256000]
*Vehicle> handle Deactivate it
[Deactivated]
我没有在Lev Gorodinski的F#示例中进一步了解,所以我不确定这是否足够,但我希望这是一个开始。