最小惊讶原理(POLA)和界面


17

25年前,当我学习C ++时,我被教导说接口应该是宽容的,并且尽可能不在乎方法的调用顺序,因为消费者可能无法访问源代码或文档来代替这个。

但是,每当我指导初级程序员和高级开发人员听到我的消息时,他们都会惊讶地做出反应,这让我怀疑这是否真的是一件事情,或者它是否已经过时了。

像泥一样清澈?

考虑具有以下方法的接口(用于创建数据文件):

OpenFile
SetHeaderString
WriteDataLine
SetTrailerString
CloseFile

现在,您当然可以按顺序浏览这些文件,但是说您不关心文件名(认为a.out)或包括什么标题和尾部字符串,您可以致电AddDataLine

一个不太极端的示例可能是省略标题和结尾。

还有一种可能是在打开文件之前设置标题和结尾字符串。

这是公认的接口设计原则,还是只是在命名之前采用POLA方式?

注意:不要被这个界面的细节所困扰,这仅仅是一个例子。


10
“最少惊讶”原则在用户界面设计中比“应用程序程序员界面”设计中更为普遍。原因是网站或程序的用户在使用前根本无法阅读任何指令,而程序员至少在原则上应在使用它们编程之前阅读API文档。
Kilian Foth '16


7
@KilianFoth:我很确定维基百科对此是错误的-POLA不仅涉及用户界面设计,鲍勃·马丁(Bob Martin)在其功能和类设计中也使用了“最不惊奇的原理”一词(完全相同)。 “清洁代码”书。
布朗

2
无论如何,不​​变的接口通常更好。您可以指定要在构造时设置的所有数据。没有歧义,该类变得更易于编写。(当然,有时这种方案是不可能的。)
usr

4
完全不同意POLA不适用于API。它适用于人类为其他人类创造的任何事物。当事情按预期进行时,它们更易于概念化,因此产生了较低的认知负担,使人们可以用更少的精力去做更多的事情。
Gort Robot'Apr

Answers:


25

坚持最小惊讶原则的一种方法是考虑其他原则,例如ISPSRP甚至是DRY

在您给出的特定示例中,建议似乎是操作文件存在一定顺序依赖性。但是您的API同时控制文件访问和数据格式,这有点像违反SRP。

编辑/更新:这也表明API本身正在要求用户违反DRY,因为他们每次使用API​​时都需要重复相同的步骤

考虑另一种API,其中IO操作与数据操作是分开的。以及API本身“拥有”订单的位置:

内容构建器

SetHeader( ... )
AddLine( ... )
SetTrailer ( ... )

FileWriter

Open(filename) 
Write(content) throws InvalidContentException
Close()

通过上述分离,ContentBuilder除了存储行/标题/尾部(可能也是ContentBuilder.Serialize()知道顺序的方法)之外,不需要实际“做”任何事情。通过遵循其他SOLID原则,在添加行之前还是之后设置标题或尾部都不再重要,因为在ContentBuilder传递给之前,中实际上不会写入文件FileWriter.Write

它还具有更多的灵活性。例如,将内容写出到诊断记录器或将其通过网络传递而不是直接将其写到文件中可能会很有用。

在设计API时,您还应该考虑错误报告,无论是状态,返回值,异常,回调还是其他内容。API的用户可能希望能够以编程方式检测到任何违反其合同的行为,甚至可以检测到它无法控制的其他错误,例如文件I / O错误。


正是我想要的-谢谢!摘自ISP文章:“(ISP)指出,不应强迫任何客户端依赖其不使用的方法”
Robbie Dee

5
这不是一个坏答案,不过,内容构建器仍然可以按调用顺序SetHeaderAddLine重要顺序的方式实现。要消除此顺序依赖性既不是ISP也不是SRP,它就是POLA。
布朗

当订单很重要时,您仍然可以通过定义操作来满足POLA,这样执行后续步骤需要从先前步骤返回一个值,从而使用类型系统强制执行订单。 FileWriter然后可以从方法的最后ContentBuilder一步中获取值,Write以确保所有输入内容均完整InvalidContentException无用。
丹·里昂斯2016年

@DanLyons我觉得这很接近问问者要避免的情况;当用户的API需要知道或关心的顺序。理想情况下,API本身应强制执行该命令,否则可能会要求用户违反DRY。这就是拆分ContentBuilder并允许FileWriter.Write封装这些知识的原因。万一内容出现任何异常(例如缺少标题),则有必要提供例外。返回也可以,但是我不喜欢将异常转换为返回码。
Ben Cottrell

但绝对值得在DRY上添加更多关于DRY的注意事项和答案顺序。
Ben Cottrell

12

这不仅与POLA有关,而且与防止无效状态(可能是错误的来源)有关。

让我们看看如何在不提供具体实现的情况下为您的示例提供一些约束:

第一步:打开文件之前,不允许调用任何内容。

CreateDataFileInterface
  + OpenFile(filename : string) : DataFileInterface

DataFileInterface
  + SetHeaderString(header : string) : void
  + WriteDataLine(data : string) : void
  + SetTrailerString(trailer : string) : void
  + Close() : void

现在很明显,CreateDataFileInterface.OpenFile必须调用它来检索DataFileInterface可以写入实际数据实例。

第二步:确保始终设置标题和结尾。

CreateDataFileInterface
  + OpenFile(filename : string, header: string, trailer : string) : DataFileInterface

DataFileInterface
  + WriteDataLine(data : string) : void
  + Close() : void

现在,您必须预先提供所有必需的参数才能获得DataFileInterface:文件名,标题和结尾。如果在写入所有行之前都无法使用预告片字符串,则也可以将此参数移动到Close()(可能将方法重命名为WriteTrailerAndClose()),以便至少在没有预告片字符串的情况下无法完成文件。


回复评论:

我喜欢界面的分离。但是我倾向于认为您关于强制执行的建议(例如WriteTrailerAndClose())正在违反SRP。(这是我在很多情况下一直在努力的事情,但是您的建议似乎是一个可能的例子。)您将如何回应?

真正。我不想把所有的精力都集中在示例上,以说明我的观点,但这是一个好问题。在这种情况下,我想我会称呼它Finalize(trailer)并且认为它不会做太多。编写预告片和结束语仅是实现细节。但是,如果您不同意或有不同的类似情况,则可以采用以下解决方案:

CreateDataFileInterface
  + OpenFile(filename : string, header : string) : IncompleteDataFileInterface

IncompleteDataFileInterface
  + WriteDataLine(data : string) : void
  + FinalizeWithTrailer(trailer : string) : CompleteDataFileInterface

CompleteDataFileInterface
  + Close()

在这个示例中,我实际上不会这样做,但是它显示了如何进行这项技术。

顺便说一句,我假设实际上必须按此顺序调用方法,例如要顺序写入许多行。如果不需要这样做那么我总是喜欢Ben Cottrel建议的建造者。


1
您a进了陷阱,我明确警告您从一开始就避免。不需要文件名-标头和尾标都不需要。但是拆分接口的一般主题是一个不错的选择,所以+1 :-)
Robbie Dee 2016年

哦,那我误会了你,我以为这是在描述用户的意图,而不是实现。
Fabian Schmengler '16

我喜欢界面的分离。但是我倾向于认为您对执行的建议(例如WriteTrailerAndClose())正在违反SRP。(这是我在很多情况下一直在努力的事情,但是您的建议似乎是一个可能的例子。)您将如何回应?
kmote

1
@kmote回答的评论太久了,请参阅我的更新
Fabian Schmengler '16

1
如果文件名是可选的,则可以提供OpenFile不需要的重载。
5gon12eder '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.