假设在你的程序中你定义了一个复杂的汽车对象。该对象包含很长的预定义键值对列表(wheels
、engine
、color
、lights
、amountDoors
等),每个键值对要么是部件号,要么是部件号列表,要么是特定值。
//** PSEUDO CODE:
var inputCar = {
"engine": "engine-123",
"lights": ["light-type-a", "light-type-b"],
"amountDoors": 6,
etc ... lets assume a lot more properties
}
让我们也假设,这个对象已经尽可能简单,不能进一步简化。
此外,我们还有一个设置列表,它告诉我们有关零件号的更多信息,并且每种零件都不同。对于引擎,它可能看起来像这样:
var settingsEngine = [
{ "id": "engine-123", weight: 400, price: 11000, numberScrews: 120, etc ... },
{ "id": "engine-124" etc ... }
]
将所有设置捆绑在一个主设置对象中
settings = { settingsEngine, settingsWheel, settingsLight ... }
现在我们有不同的函数,它们应该接受Car
并返回有关它的某些值,例如重量、价格或螺钉数量。
要计算这些值,必须将输入汽车的ID与设置中的ID相匹配,并应用一些逻辑来获取复杂零件的确切数据(要弄清楚车身的外观,我们需要查看有多少门,车轮有多大等)。
对于汽车的每个部分,获得价格也会有所不同且任意复杂。定价的每个部分可能需要访问有关汽车的不同零件和信息,因此仅映射零件清单是不够的。(对于油漆工作的价格,我们需要具有相同颜色的所有零件的总表面积等。
一个想法是创建一个中间对象,该对象解决了价格和重量计算之间共享的有关汽车的所有详细信息,然后可用于计算重量,价格等。
一个实现可能如下所示:
var detailedCar = getDetailedCar(inputCar, settings);
var priceCar = getPriceCar(detailedCar);
var weightCar = getWeightCar(detailedCar);
这样,部分工作只需完成一次。但是在这个例子中,detailedCar
将是一个比初始输入对象更复杂的对象,因此getPriceCar
的参数也是如此 - 这使得测试也非常困难,因为我们总是需要一个完整的汽车对象对于每个测试用例。所以我不确定这是否是一个好方法。
问题
对于处理无法在函数式编程风格/纯函数/组合中进一步简化的复杂输入数据的程序来说,什么是好的设计模式?
给定复杂、相互依赖的输入,如何使结果易于单元测试?
你所描述的一般术语是投影的使用。投影是一种数据结构,它是其他数据结构的抽象,面向您要进行的计算类型。
从您的示例中,您需要一个"螺钉投影",它获取描述车辆的数据并提出所需的螺钉。因此,我们定义了一个函数:
screwProjection(vehicle, settings) -> [(screwType, screwCount)]
它需要车辆和描述组件的设置,并提出构成车辆的螺钉。如果您不关心screwType
,您还可以进行进一步的投影,简单地对元组中的第二项求和。
现在,要分解screwProjection()
,您将需要一些迭代车辆的每个组件并根据需要进一步分解的东西。例如,示例中的第一步,获取engine
并找到适合发动机的设置,并根据发动机类型进行筛选,然后根据螺钉字段筛选结果:
partProjection(part, settings) -> [(partType, partCount)]
所以,screwProjection()
看起来像:
vehicle.parts
.flatMap( part -> partProjection( part, settings ) ) // note 1
.filter( (partType, partCount) -> partType == 'screw' )
.map( (partType, partCount) -> partCount )
.sum()
注1)此投影方法不允许嵌套物料清单查找,您可能需要添加这些查找以获得额外的积分。
这种枚举 => 投影 => 筛选器 => reduce的通用方法是许多函数式编程范式的核心。
我在这里建议稍微不同的方法。
由于您的问题是关于纯函数式编程的,因此我认为您需要一个高阶函数来减轻复杂数据结构所需的位并遮蔽不必要的数据结构:readComplexDataStructure :: (ComplexDataStructure -> a) -> (a -> b) -> ComplexDataStructure -> b
,其中a
表示您需要从某个ComplexDataStructure
实例中提取的数据,b
是计算的结果。
请注意它与Reader
monad 有多接近,尽管我不建议立即使用它,除非代码复杂性证明这样的决定是合理的。
附言它缩放。你只需要一个函数来生成由(ComplexDataStructure -> a)
投影组成的n-uple。例如,请考虑以下签名:double :: (ComplextDataStructure -> a) -> (ComplexDataStructure -> b) -> ( (a, b) -> c) -> ComplexDataStructure -> c
。只要你只保持适当的投影,你的代码就不会变得"臃肿",其余的都是相当复合和自我描述的。