无副作用二传手的方法
Posted
技术标签:
【中文标题】无副作用二传手的方法【英文标题】:Approach to side-effect-free setters 【发布时间】:2010-03-16 03:16:34 【问题描述】:我想就无副作用的二传手能走多远征求您的意见。
考虑以下示例:
Activity activity;
activity.Start = "2010-01-01";
activity.Duration = "10 days"; // sets Finish property to "2010-01-10"
请注意,日期和持续时间的值仅供参考。
因此,对 Start
、Finish
和 Duration
的任何属性使用 setter 将因此更改其他属性,因此不能被视为无副作用。
同样适用于Rectangle
类的实例,其中X
的设置器正在更改Top
和Bottom
的值等等。
问题是在使用设置器(具有更改逻辑相关属性的值的副作用)和使用方法(无论如何都不能更具描述性)之间划清界限。例如,定义一个名为 SetDurationTo(Duration duration)
的方法也不反映 Start 或 Finish 将被更改。
【问题讨论】:
【参考方案1】:我认为您误解了“副作用”一词,因为它适用于程序设计。设置属性是副作用,无论它改变多少内部状态,只要它改变某种状态。 “无副作用的设置器”不会很有用。
副作用是您希望避免对属性getters 的影响。读取属性的值是调用者不希望改变任何状态的事情(即导致副作用),所以如果这样做,它通常是错误的或至少是有问题的(有例外,例如延迟加载)。但无论如何,getter 和 setter 都只是方法的包装器。就 CLR 而言,Duration
属性只是 set_Duration
方法的语法糖。
这正是类等抽象的意义所在——提供粗粒度操作,同时保持一致的内部状态。如果您刻意避免在单个属性分配中产生多个副作用,那么您的类最终只不过是愚蠢的数据容器。
所以,直接回答这个问题:我在哪里画线?无处,只要方法/属性确实如其名称所暗示的那样。如果设置Duration
也改变了ActivityName
,那可能是个问题。如果它改变了Finish
属性,那应该很明显; 应该不可能更改Duration
并让Start
和Finish
保持不变。 OOP 的基本前提是对象具有足够的智能,可以自行管理这些操作。
如果这在概念层面上困扰您,那么根本就没有 mutator 属性 - 使用具有只读属性的不可变数据结构,其中所有必要的参数都在构造函数中提供。然后有两个重载,一个采用Start
/Duration
,另一个采用Start
/Finish
。或者只将其中一个属性设为可写——假设Finish
使其与Start
保持一致——然后将Duration
设为只读。使用可变和不可变属性的适当组合,以确保只有一种方法可以更改某个状态。
否则,不要太担心这个。属性(和方法)不应该有 unintended 或 undocumented 副作用,但这是我会使用的唯一准则。
【讨论】:
谢谢,这就是我一直在寻找的东西,我从来没有想过“无副作用的设置器”实际上是不可能的,因为它会改变状态。正如您指出的那样,CLR 无论如何都会转换为方法。 是的,setter 的副作用是设置它引用的成员变量。但另一个副作用是修改其他成员变量。 我只是在想同样的事情,我什至会说成员变量的设置不是副作用,而是设置器的预期效果。更改其他成员变量是一个副作用。 你可能说设置成员字段不是副作用,但你是不正确的。该术语有一个非常具体的定义; 副作用表示操作改变状态,周期。它可能根本不会影响任何字段 - 也许它会保存到磁盘或从套接字发送数据。这就是为什么我要区分unintended 或undocumented(或者可能是undesired 的副作用)。然后它变成一个简单的问题:这个附加副作用是否预期?重要的不是变化的数量,而是变化的什么。 好的,我已经完成了我的功课并且同意你的说法。再次感谢您的宝贵意见。【参考方案2】:就我个人而言,我认为有一个副作用来保持一致的状态是有意义的。就像你说的,改变逻辑相关的值是有意义的。从某种意义上说,副作用是意料之中的。但重要的是要明确这一点。也就是说,很明显该方法正在执行的任务具有某种副作用。所以你可以调用你的函数ChangeDurationTo
,而不是SetDurationTo
,这意味着正在发生其他事情。您也可以通过使用调整持续时间AdjustDurationTo
并传入delta
值的函数/方法来执行此操作。如果您将该功能记录为具有副作用,这将有所帮助。
我认为另一种看待它的方法是查看是否会出现副作用。在您的矩形示例中,我希望它更改 top
或 bottom
的值以保持内部一致的状态。我不知道这是否是主观的;这对我来说似乎很有意义。与往常一样,我认为文档胜出。如果有副作用,请认真记录。最好通过方法的名称和支持文档。
【讨论】:
【参考方案3】:一种选择是让您的类不可变,并让方法创建并返回类的所有适当值都已更改的新实例。然后没有副作用或二传手。想想DateTime
之类的东西,您可以在其中调用AddDays
和AddHours
之类的东西,这将返回一个新的DateTime
实例并应用更改。
【讨论】:
【参考方案4】:我一直遵循一般规则,不允许 public
setter 对没有副作用的属性进行设置,因为公共设置器的调用者无法确定可能会发生什么,但当然,修改的人程序集本身应该有一个很好的主意,因为他们可以看到代码。
当然,有时您必须为了可读性、使您的对象模型合乎逻辑或只是为了让事情正常工作而打破规则。就像你说的,这实际上是一个总体偏好问题。
【讨论】:
【参考方案5】:我认为这主要是常识问题。
在这个特定的示例中,我的问题不在于您拥有调整“相关”属性的属性,而是您拥有获取字符串值的属性,然后您可以在内部解析为 DateTime(或其他)价值观。
我更愿意看到这样的东西:
Activity activity;
activity.Start = DateTime.Parse("2010-01-01");
activity.Duration = Duration.Parse("10 days");
也就是说,您明确指出您正在解析字符串。允许程序员在适当的时候指定强类型对象。
【讨论】:
正如我所提到的“请注意,日期和持续时间的值仅供参考。”所以你的评论确实没有解决所提出的问题,但无论如何谢谢你......我知道有人会指出这个;-)以上是关于无副作用二传手的方法的主要内容,如果未能解决你的问题,请参考以下文章