2009-10-19 39 views
3

这绝对是一个语言不可知论的问题,而且这个问题已经困扰了我很长一段时间了。一个例子可能会帮助我解释我面临的困境:一种方法应该承担多少责任?

让我们说我们有一个方法负责读取文件,用一些对象(它存储来自文件的信息)填充集合,然后返回集合......类似如下:

public List<SomeObject> loadConfiguration(String filename); 

让我们也说,在实施该方法时,它似乎不可行的应用程序继续,如果集合返回的是空(0大小)。现在,问题是,是否应该在该方法内进行验证(检查空集合并可能随后抛出异常)?或者,这种方法的唯一责任是执行文件的加载并忽略验证的任务,允许在方法之外的某个阶段进行验证?

我想一般的问题是:是否更好地将验证与实际正在执行的任务分离?一般而言,这会使事情在稍后的阶段更容易改变或建立 - 就我上面的例子而言,在稍后的阶段可能会出现这样的情况,即增加不同的策略以从空的事件中恢复从'loadConfiguration'方法返回集合.....如果在方法中完成验证(以及生成的异常),这将会很困难。

也许我在寻求一些教条式的回答时过于迂腐,反而它只是依赖于使用方法的上下文。无论如何,我会非常感兴趣的是看到别人对此有何评论。

谢谢大家!

回答

7

我的建议是坚持单一职责原则,说,简而言之,每个对象应该有1个目的。在这种情况下,如果您计算验证方面,您的方法有3个目的,然后有4个目的。

以下是我对如何处理此问题以及如何为将来更新提供大量灵活性的建议。

  1. 让您LoadConfig方法

  2. 有它称之为一种新的方法来读取文件。

  3. 将上一个方法的返回值传递给另一个将文件加载到集合中的方法。

  4. 将对象集合传递给某种验证方法。

  5. 返回集合。

这是最初采取1方法,并打破它与一个调用3其他人。这应该允许你改变对别人没有任何影响的作品。

希望这有助于

+0

我明白这样的建议的好意,但它有煽动过度工程的风险。 – MaD70 2009-10-19 18:45:56

+0

@我同意的范围内的MaD70;然而,如果要使用单元测试,保持小而简单的事情将极大地帮助保持代码的可维护性。如果它是一个微小的系统,那么我同意你的观点。然而,根据我的经验,最好始终将单一责任抽象出来,否则间接错误的风险会增加。 – JamesEggers 2009-10-19 19:36:07

0

方法应该是高度凝聚力...这是一心一意的。所以我的意见是分开你所描述的责任。我有时候很想说...这只是一个简短的方法,所以没关系...然后我在1.5周后感到后悔。

0

我认为这取决于案例:如果你可以想到一个场景,你会使用这种方法,它返回一个空列表,这将是好的,那么我不会把验证内部的方法。但是对于例如一种将数据插入数据库的方法,该数据库必须经过验证(电子邮件地址是否正确,是否指定了名称......),那么应该可以将验证代码放入函数中并引发异常。

4

我想一般的问题是:它 更好的解耦由 方法执行实际任务从 验证?

是的。(至少如果你真的坚持回答这样一个普遍的问题 - 找到反例总是很容易的。)如果你将解决方案的两个部分分开,你可以交换,放弃或重用任何一个。这是一个明显的优点。当然,你必须注意不要通过暴露非验证API来危害你的对象的不变式,但我认为你知道这一点。你将不得不做一些额外的打字,但这不会伤害你。

+0

我认为我们的观点不一致。延迟验证(参数,回报等)是调试恶梦的开放门户。 – 2009-10-19 18:08:30

+0

欢迎您不同意:)跳过单元测试是打开调试噩梦的大门。如果你不刷掉设计,你会保持危险的电线封装和外部API测试,那么你没有什么可担心的。当然,我不会为我的尸体辩护,明确的情况下,你会尽最大努力保持加载和验证一块。 – zoul 2009-10-19 18:13:13

+0

我同意。一个反例:你不想受到例外的困扰,因为程序需要以某些选项的默认值开始,其余的很少,所以你初始化它们,当没有时候不做任何事情一个配置文件(或者是空的)并忘记它。 – MaD70 2009-10-19 18:23:34

2

为了将问题转移到更基本的问题,每种方法应该尽可能少的。因此,在你的例子中,应该有一个方法读取文件,一种方法从文件中提取必要的数据,另一种方法将数据写入集合,另一种方法调用这些方法。验证可以采用单独的方法,也可以采用其他方法,具体取决于最有意义的位置。

private byte[] ReadFile(string fileSpec) 
    { 
     // code to read in file, and return contents 
    } 
    private FileData GetFileData(string fileContents) 
    { 
     // code to create FileData struct from file contents 
    } 
    private void FileDataCollection: Collection<FileData> { } 

    public void DoItAll (string fileSpec, FileDataCollection filDtaCol) 
    { 
     filDtaCol.Add(GetFileData(ReadFile(fileSpec))); 
    } 

每个方法添加验证,验证适当

+0

“基本”方法应尽可能少,但一种方法可以聚合3/4其他方法。 – 2009-10-19 18:10:15

2

我将一个问题回答你的问题:你想为你的方法的产品不同的验证方法?

这与“构造函数”问题相同:在构建过程中引发异常还是初始化无效对象,然后调用'init'方法会更好......您肯定会在此提出争论!

一般来说,我会建议尽快进行验证:这就是所谓的Fail Fast,它主张尽快发现问题比延迟检测更好,因为诊断是立竿见影的,而以后您将不得不恢复整个流程......

如果你不确定,这样想:你真的想每次载入文件时写3行吗? (加载,解析,验证)那么,这违反了DRY原则。

所以,去敏捷有:

  • 写你的方法与验证:它负责装载有效配置(1)
  • 如果你需要一些参数化,加上它,然后(如一个'检查'参数,默认值保留旧的行为当然)

(1)当然,我不主张一个方法一次完成所有这一切......它是一个组织事情:在封面下此方法应该调用专用方法来组织代码:)

+0

+1,即使我们给出了相反的答案。我认为我们两个人的意思是一样的:你说保持加载的验证,但将实现拆分为封面下的单独方法;我说要让它们分开,但在暴露外部API中的零件时要小心。 – zoul 2009-10-19 18:21:17

+0

“总的来说,我会推荐......”但是那里没有“一般的节目”只有特定的人编写的特定程序才能解决特定的问题(不过一般人需要它,程序不能解决所有存在的问题)。看到我对zoul的回复,其中一个例子是,如果一个空的(或不存在的)配置文件不想引发异常。 – MaD70 2009-10-22 04:57:41

+0

我没有看到你的观点......当然,如果有些情况下你满足于某种情况,那么不要把它当作是一种错误。但是,如果不处理任何错误是一种懒惰的方法,那么必须仔细定义方法的契约,并在需要重构时根据需要进行演变。 – 2009-10-22 17:37:29

2

您正在设计一个API,不应对您的客户端做出任何不必要的假设。一个方法应该只取得它所需要的信息,只返回所请求的信息,并且只在它无法返回一个有意义的值时才会失败。

所以,考虑到这一点,如果配置是可加载但空的,那么返回一个空列表对我来说似乎是正确的。如果您的客户在提供空列表时对应用程序的特定要求失败,那么它可能会这样做,但未来的客户可能没有这个要求。方法本身在失败时会失败,比如当它无法读取或解析文件时。

但是你可以继续解耦你的界面。例如,为什么必须将配置存储在文件中?为什么我不能提供URL,数据库中的一行或包含配置数据的原始字符串?很少有方法应该将文件路径作为参数,因为它将它们紧紧地绑定到本地文件系统,并使它们除了其核心逻辑之外还负责打开,读取和关闭文件。考虑接受一个输入流作为替代。或者,如果您想允许精心制作替代方案 - 比如来自数据库的数据 - 请考虑接受ConfigurationReader接口或类似方法。

0

上面没有提到的另一种方法是支持依赖注入,并让方法客户端注入一个验证器。这将允许保留“强大的”资源获取是初始化原则,也就是说任何成功装载的对象都已准备就绪(Matthieu提到的快速失败也是一个概念)。

它还允许资源实现类创建它自己的低级验证器,它依赖于资源的结构而不会不必要地暴露客户端的实现细节,这在处理多个不同的资源提供者(如Ryan列出的)时非常有用。

相关问题