设计大型应用程序时的一个主要架构目标是减少耦合和依赖性。依赖关系是指源代码依赖关系,当一个函数或数据类型使用另一个函数或另一种类型时。高级架构指南似乎是Ports & Adapters架构,略有不同也称为洋葱架构、六边形架构或干净架构:建模应用程序域的类型和功能位于中心,然后使用在领域的基础上提供有用服务的案例,在最外圈是技术方面,如持久性、网络和 UI。
依赖规则说依赖必须只指向内部。例如; 持久性可能取决于用例中的函数和类型,而用例可能取决于域中的函数和类型。但不允许域依赖于外环。我应该如何在 Haskell 中实现这种架构?具体来说:我如何实现一个不依赖(=导入)持久性模块中的函数和类型的用例模块,即使它需要检索和存储数据?
假设我想通过一个函数实现一个用例订单放置U.placeOrder :: D.Customer -> [D.LineItem] -> IO U.OrderPlacementResult
,该函数从订单项创建一个订单并尝试保留该订单。这里,U
表示用例模块和D
域模块。该函数返回一个 IO 操作,因为它需要以某种方式保持订单。然而,持久化本身是在最外层的架构环中——在某些模块中实现P
;因此,上述函数不能依赖于从P
.
我可以想象两种通用解决方案:
- 高阶函数:该函数
U.placeOrder
接受一个额外的函数参数,例如U.OrderDto -> U.PersistenceResult
。这个函数是在persistence(P
)模块中实现的,但是它依赖于模块的类型U
,而U
模块不需要声明依赖P
。 - 类型类:
U
模块定义了一个Persistence
声明上述函数的类型类。该P
模块依赖于该类型类并为其提供一个实例。
变体 1 非常明确,但不是很通用。它可能会导致具有许多参数的函数。变体 2 不那么冗长(例如,请参见此处)。然而,变体 2 导致了许多无原则的类型类,这在大多数现代 Haskell 教科书和教程中被认为是不好的做法。
所以,我有两个问题:
- 我错过了其他选择吗?
- 通常推荐哪种方法(如果有)?