编程以供将来使用接口


42

我旁边有一位同事,他设计了这样的界面:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}

问题是,现在,我们在代码中的任何地方都没有使用此“ end”参数,它只是存在,因为将来可能需要使用它。

我们试图说服他,将参数放入当前不使用的接口是一个坏主意,但是他坚持认为,如果我们实现一段时间的“结束”日期使用,则必须做很多工作之后,必须修改所有代码。

现在,我的问题是,是否有任何来源正在处理像“尊敬的”编码专家这样的主题,我们可以将其链接到?


29
“预测非常困难,尤其是对未来。”
约尔格W¯¯米塔格

10
问题的一部分在于,它并不是开始时的最佳界面。获取Foos并对其进行过滤是两个独立的问题。该界面迫使您以一种特定方式过滤列表。更通用的方法是传入一个确定是否应包含foo的函数。但是,只有在可以访问Java 8的情况下
才算

5
反击是重点。简单地使该方法接受的自定义类型卫生组织现在只包含你所需要的,并可能最终添加end参数,该对象,甚至默认情况下,为了不破坏代码
雷米

3
使用Java 8默认方法,没有理由添加不必要的参数。可以在以后使用默认实现的方法添加一个方法,该实现简单地调用,例如null。然后,可以根据需要重写类。
蜘蛛鲍里斯(Boris the Spider)

1
@Doval我对Java不了解,但是在.NET中,您有时会看到类似避免将IQueryableDAL之外的代码(只能使用某些表达式)暴露给怪人的事情
Ben Aaronson 2014年

Answers:


62

邀请他学习YAGNI。Wikipedia页面的“基本原理”部分在这里可能特别有趣:

那些主张使用YAGNI方法的人认为,目前暂时没有必要但将来可能会编写代码的诱惑有以下缺点:

  • 花在增加,测试或改进必要功能上的时间。
  • 必须调试,记录和支持新功能。
  • 任何新功能都会限制将来的操作,因此不必要的功能可能会阻止将来添加所需的功能。
  • 在实际需要该功能之前,很难完全定义其功能并对其进行测试。如果未正确定义和测试新功能,即使最终需要它,也可能无法正常工作。
  • 它导致代码膨胀;该软件变得越来越大,越来越复杂。
  • 除非有规范和某种版本控制,否则使用该功能的程序员可能不知道该功能。
  • 添加新功能可能会建议其他新功能。如果还实现了这些新功能,则可能会导致雪球效应,从而导致特征蠕变。

其他可能的参数:

  • “软件的整个生命周期成本的80%用于维护”及时编写代码可以减少维护成本:人们必须维护更少的代码,并且可以专注于实际需要的代码。

  • 源代码只写一次,但要读几十遍。附加的参数(在任何地方都不会使用)会导致浪费时间,以了解为什么会有不需要的参数。鉴于这是一个具有多种可能实现的接口,只会使事情变得更加困难。

  • 源代码应该是自我记录的。实际的签名具有误导性,因为读者会认为这end会影响方法的结果或执行。

  • 编写此接口的具体实现的人可能不理解不应使用最后一个参数,这将导致不同的方法:

    1. 我不需要end,因此我将忽略其价值,

    2. 我不需要end,因此如果不是null,我将抛出异常,

    3. 我不需要end,但会尝试以某种方式使用它,

    4. 我将编写许多代码,这些代码可能在以后end需要时使用。

但是请注意,您的同事可能是正确的。

前面的所有观点都是基于重构很容易的事实,因此以后添加参数不需要太多的工作。但这是一个接口,作为一个接口,可以由多个团队为您产品的其他部分提供帮助。这意味着更改接口可能会特别痛苦,在这种情况下,YAGNI并不真正适用于此。

hjk的答案给出了一个很好的解决方案:在已使用的接口中添加方法并不是特别困难,但是在某些情况下,它也需要大量成本:

  • 一些框架不支持重载。例如,如果我记得很好(如果我记错了,请纠正我),. NET的WCF不支持重载。

  • 如果该接口具有许多具体的实现,则向该接口添加方法将需要遍历所有实现并在该方法中也添加该方法。


4
我认为考虑此功能将来出现的可能性也很重要。如果您确定可以添加它,但是由于某种原因现在不添加它,那么我想说这是一个好主意,因为它不仅是“防万一”功能。
Jeroen Vannevel 2014年

3
@JeroenVannevel:当然,但这是非常投机的。根据我的经验,我相信肯定会实施的大多数功能要么被取消,要么永远被推迟。这包括利益相关者最初强调的非常重要的功能。
阿森尼·穆尔琴科(Arseni Mourzenko)2014年

26

但是他一直坚持认为,如果我们稍后再实现使用“结束”日期,然后必须修改所有代码,则必须完成许多工作。

(一段时间之后)

public class EventGetter implements IEventGetter {

    private static final Date IMPLIED_END_DATE_ASOF_20140711 = new Date(Long.MAX_VALUE); // ???

    @Override
    public List<FooType> getFooList(String fooName, Date start) throws Exception {
        return getFooList(fooName, start, IMPLIED_END_DATE_ASOF_20140711);
    }

    @Override
    public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception {
        // Final implementation goes here
    }
}

这就是您所需要的,方法重载。将来可以透明地引入其他方法,而不会影响对现有方法的调用。


6
除了您的更改意味着它不再是接口之外,并且在对象已经从对象派生并实现接口的情况下,将无法使用。如果实现接口的所有类都在同一个项目中(追随编译错误),这将是一个小问题。如果它是多个项目使用的库,那么在接下来的x个月/年中,该字段的添加会在其他时间造成混乱并为其他人工作。
Kieveli 2014年

1
如果OP的团队(保存同事)确实认为end现在不需要该参数,并且在end整个项目中继续使用-less方法,则更新界面是他们的最不担心的地方,因为更新该接口的必要性实现类。我的答案专门针对“适应所有代码”部分,因为听起来同事正在考虑必须在整个项目中更新原始方法的调用者以引入第三个默认/虚拟参数的思路。
hjk 2014年

18

[那里]我们可以将他链接到[说服他]的“受尊敬的”编码专家吗?

对权威的呼吁并不是特别令人信服;最好提出一个有效的论据,无论是谁说的。

这是一个简化的荒谬论据,应该说服或证明您的同事不管敏感性如何,都坚持“正确”:


您真正需要的是

getFooList(String fooName, Date start, Date end, Date middle, 
           Date one_third, JulianDate start, JulianDate end,
           KlingonDate start, KlingonDate end)

您永远都不知道何时需要对Klingon进行国际化,因此您现在最好照料一下,因为这需要大量的工作才能进行翻新,而且Klingon的耐心性并不为人所知。


22
You never know when you'll have to internationalize for Klingon。这就是为什么您应该只传递a Calendar,然后让客户端代码决定他是否要发送a GregorianCalendar或a KlingonCalendar(我敢打赌有些人已经做了一个)。咄。:-p
SJuan76

3
怎么样StarDate sdStartStarDate sdEnd?我们毕竟不能忘记联邦...
WernerCD 2014年

所有这些显然都可以是的子类或可以转换为Date
Darkhogg 2014年

如果就这么简单
msw 2014年

@msw,我不确定他要求链接到“受尊敬的编码专家”时是否要求“呼吁权威”。如您所说,他需要“无论谁说都有效的论点”。“上师”型的人往往了解很多。你也忘了HebrewDate startHebrewDate end
trysis

12

从软件工程的角度来看,我认为针对此类问题的正确解决方案是构建器模式。这绝对是'guru'作者为您的同事提供的链接,网址为http://en.wikipedia.org/wiki/Builder_pattern

在构建器模式中,用户创建一个包含参数的对象。然后,此参数容器将传递到方法中。这将照顾您的同事将来需要的任何扩展和参数重载,同时在需要进行更改时使整个过程变得非常稳定。

您的示例将变为:

public interface IEventGetter {
    public List<FooType> getFooList(ISearchFooRequest req) {
        throws Exception;
    ....
    }
}

public interface ISearchFooRequest {
        public String getName();
        public Date getStart();
        public Date getEnd();
        public int getOffset();
        ...
    }
}

public class SearchFooRequest implements ISearchFooRequest {

    public static SearchFooRequest buildDefaultRequest(String name, Date start) {
        ...
    }

    public String getName() {...}
    public Date getStart() {...}
    ...
    public void setEnd(Date end) {...}
    public void setOffset(int offset) {...}
    ...
}

3
这是一个很好的通用方法,但是在这种情况下,似乎只是将问题从推IEventGetterISearchFootRequest。相反,我建议public IFooFilter使用与member 相似的方法,public bool include(FooType item)该方法返回是否包含该项目。然后,各个接口实现可以决定如何进行过滤
Ben Aaronson 2014年

@Ben Aaronson我只是编辑代码以显示如何创建请求参数对象
InformedA

这里不需要构建器(坦率地说,总体而言,过度使用/过度推荐模式);关于如何设置参数没有复杂的规则,因此,如果对方法参数的复杂性或频繁更改有合理的关注,那么使用参数对象就可以了。我同意@BenAaronson的观点,尽管使用参数对象的目的不是立即包含不必要的参数,而是使它们在以后的添加中非常容易添加(即使有多个接口实现)。
2014年

7

您现在不需要它,所以不要添加它。如果以后需要,请扩展接口:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start)
         throws Exception;
    ....

}

public interface IBoundedEventGetter extends IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}

+1用于不更改现有(可能已经测试/发货)的接口。
塞巴斯蒂安·戈德莱特

4

没有设计原则是绝对的,因此尽管我大部分都同意其他答案,但我认为我会扮演恶魔的拥护者,并讨论一些可以考虑接受您同事的解决方案的条件:

  • 如果这是公共API,并且您希望该功能对第三方开发人员有用,即使您未在内部使用它。
  • 如果它具有可观的即时收益,而不仅仅是未来的收益。如果隐式结束日期为Now(),则添加参数将消除副作用,这对于缓存和单元测试有好处。也许它允许更简单的实现。或者,它与您代码中的其他API更加一致。
  • 如果您的开发文化有问题的历史。如果流程或地域性使得很难更改诸如界面之类的中心内容,那么我之前看到的事情是人们实施客户端变通方法而不是更改界面,那么您正在尝试维护多个临时结束日期过滤器而不是一个。如果这种事情在您的公司中经常发生,那么在将来的打样中付出更多的努力就很有意义。不要误会我的意思,最好更改您的开发流程,但这通常说起来容易做起来难。

话虽这么说,以我的经验,YAGNI的最大推论是YDKWFYN:您不知道需要哪种形式(是的,我只是缩写了该形式)。即使需要一些 limit参数可能是相对可预测的,它也可能采用页数限制或天数的形式,或者指定使用用户首选项表中的结束日期的布尔值或其他任何形式。

由于您还没有要求,因此您无法知道该参数应为哪种类型。您通常最终会遇到一个尴尬的界面,该界面并非最适合您的要求,或者无论如何都必须对其进行更改。


2

没有足够的信息来回答这个问题。这取决于getFooList实际执行的操作以及执行方式。

这是一个明显的方法示例,该方法应支持是否使用其他参数。

void CapitalizeSubstring (String capitalize_me, int start_index);

实现对集合进行操作的方法通常是很愚蠢的,您可以在其中指定集合的​​开始而不是结束。

您确实必须查看问题本身,并询问该参数在整个界面的上下文中是否是荒谬的,以及实际上该附加参数施加了多少负担。


1

恐怕您的同事可能有一个非常正确的观点。尽管他的解决方案实际上并不是最好的。

从他提议的界面可以清楚地看出

public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception;

返回在一定时间间隔内找到的实例。如果客户端当前不使用end参数,则不会改变他们期望在一定时间间隔内找到实例的事实。这仅表示当前所有客户都使用开放式时间间隔(从开始到永恒)

因此,更好的接口是:

public List<FooType> getFooList(String fooName, Interval interval) throws Exception;

如果您使用静态工厂方法提供间隔:

public static Interval startingAt(Date start) { return new Interval(start, null); }

这样客户甚至都不需要指定结束时间。

同时,您的界面可以更正确地传达其功能,因为getFooList(String, Date)它不会传达这涉及到一个间隔。

请注意,我的建议来自该方法的当前功能,而不是其将来应做或将来可能做的事情,因此,YAGNI原则(确实非常有效)在这里不适用。


0

添加未使用的参数会造成混淆。人们可能会在假定此功能有效的情况下调用该方法。

我不会添加。稍后使用重构将其添加并机械修复呼叫站点是微不足道的。在静态类型的语言中,这很容易做到。不必急切添加它。

如果一个理由,该参数可以防止这样的困惑:在接口的实现添加断言执行这个参数设置为默认值传递。如果有人不小心使用了该参数,至少他会在测试期间立即注意到未实现此功能。这样就消除了将bug投入生产的风险。

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.