3

假设在您的程序中您定义了一个复杂的汽车对象。该对象包含一个很长的预定义键值对列表(wheelsenginecolor、等),每个键值对要么是部件号lightsamountDoors要么是部件号列表,或者是特定值。

//** 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 ... }

现在我们有不同的函数,它们应该接受 aCar并返回关于它的某些值,比如重量、价格或螺丝数量。

要计算这些值,需要将输入汽车的 ID 与设置中的 ID 进行匹配,并应用一些逻辑来获取复杂零件的准确数据(要弄清楚车身的样子,我们需要看看有多少有门,轮子有多大等)。

对于汽车的每个部分,获取价格也会不同且任意复杂。定价的每个部分都可能需要访问有关汽车的不同零件和信息,因此仅映射零件列表是不够的。(对于油漆工作的价格,我们需要所有具有相同颜色的零件的总表面积等。)

一个想法是创建一个中间对象,它已经解决了在价格和重量计算之间共享的有关汽车的所有细节,然后可用于计算重量、价格等。

一种实现可能如下所示:

var detailedCar = getDetailedCar(inputCar, settings);

var priceCar = getPriceCar(detailedCar);
var weightCar = getWeightCar(detailedCar);

这样,部分工作只需完成一次。但在这个例子中detailedCar,它是一个比初始输入对象更复杂的对象,因此参数也将是getPriceCar- 使得它也很难测试,因为我们总是需要一个完整的汽车对象用于每个测试用例。所以我不确定这是否是一个好方法。

问题

对于处理无法以函数式编程风格/纯函数/组合进一步简化的复杂输入数据的程序来说,什么是好的设计模式?

给定一个复杂的、相互依赖的输入,结果如何容易进行单元测试?

4

2 回答 2

4

您所描述的通用术语是使用预测。投影是一种数据结构,它是其他数据结构的抽象,面向您想要进行的计算类型。

在您的示例中,您需要一个“螺钉投影”,它获取描述车辆的数据并提出所需的螺钉。因此,我们定义了一个函数:

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) 此投影方法不允许嵌套的物料清单查找,您可能需要添加这些查找以获得额外的功劳。

这种枚举 => 投影 => 过滤 => 减少的通用方法是许多函数式编程范式的核心。

于 2019-02-25T21:21:05.217 回答
2

我会在这里建议稍微不同的方法。

由于您的问题是关于纯函数式编程,我会说您需要一个高阶函数来负责减轻复杂数据结构所需的位并遮蔽不必要的位: readComplexDataStructure :: (ComplexDataStructure -> a) -> (a -> b) -> ComplexDataStructure -> b,其中a表示您需要从某个ComplexDataStructure实例中提取的数据,并且b是计算的结果.

请注意它与Readermonad 的距离有多近,尽管我不建议立即使用它,除非代码复杂性证明这样的决定是合理的。

PS它可以缩放。您只需要一个函数来生成由投影组成的 n-uple (ComplexDataStructure -> a)。例如,考虑以下签名:double :: (ComplextDataStructure -> a) -> (ComplexDataStructure -> b) -> ( (a, b) -> c) -> ComplexDataStructure -> c. 只要您保持适当的预测,您的代码就不会变得“臃肿”,其余的都是相当组合和自我描述的。

于 2019-02-25T22:00:56.303 回答