覆盖具体方法是否有代码气味?


31

重写具体方法是否确实有代码异味?因为我认为您需要重写具体方法:

public class A{
    public void a(){
    }
}

public class B extends A{
    @Override
    public void a(){
    }
}

可以重写为

public interface A{
    public void a();
}

public class ConcreteA implements A{
    public void a();
}

public class B implements A{
    public void a(){
    }
}

如果B要在A中重用a(),则可以重写为:

public class B implements A{
    public ConcreteA concreteA;
    public void a(){
        concreteA.a();
    }
}

不需要继承来覆盖该方法,是真的吗?



4
由于(在下面的评论中)Telastyn和Doc Brown同意覆盖,所以予以否决,但是对标题问题给出了相反的是/否答案,因为他们不同意“代码气味”的含义。因此,旨在解决代码设计的问题已经与question语紧密相关。而且我认为是不必要的,因为问题特别是关于一种技术与另一种技术的对立。
Steve Jessop

5
该问题未标记为Java,但是代码似乎是Java。在C#(或C ++)中,您必须使用virtual关键字来支持替代。因此,该语言的细节使问题变得(或更少)模棱两可。
Erik Eidt

1
似乎足够多的年后,几乎每种编程语言功能都不可避免地被归类为“代码异味”。
布兰登

2
我觉得final修饰符确实存在于此类情况。如果该方法的行为不适合重写,则将其标记为final。否则,期望开发人员选择覆盖它。
Martin Tuskevicius

Answers:


34

不,这不是代码的味道。

  • 如果某个类不是最终的,则可以对其进行子类化。
  • 如果方法不是最终方法,则可以覆盖该方法。

每个类都有责任仔细考虑子类是否合适,以及哪些方法可以被覆盖

该类可以将自身或任何方法定义为final,也可以对其子类的生成方式和位置施加限制(可见性修饰符,可用的构造函数)。

覆盖方法的通常情况是基类中的默认实现,可以在子类中对其进行自定义或优化(尤其是在Java 8中,随着接口中默认方法的出现)。

class A {
    public String getDescription(Element e) {
        // return default description for element
    }
}

class B extends A {
    public String getDescription(Element e) {
        // return customized description for element
    }
}

替代行为的替代方法是策略模式,其中行为被抽象为接口,并且可以在类中设置实现。

interface DescriptionProvider {
    String getDescription(Element e);
}

class A {
    private DescriptionProvider provider=new DefaultDescriptionProvider();

    public final String getDescription(Element e) {
       return provider.getDescription(e);
    }

    public final void setDescriptionProvider(@NotNull DescriptionProvider provider) {
        this.provider=provider;
    }
}

class B extends A {
    public B() {
        setDescriptionProvider(new CustomDescriptionProvider());
    }
}

我了解,在现实世界中,重写具体方法可能会使某些人闻到代码臭味。但是,我喜欢这个答案,因为在我看来,它更多是基于规范。
Darkwater23年

在您的最后一个示例中,不应该A实现DescriptionProvider吗?
斑点

@Spotted:A 可以实现DescriptionProvider,因此是默认的描述提供程序。但是这里的想法是关注点分离:A需要描述(其他一些类也可能需要),而其他实现则提供它们。
彼得·沃尔瑟

2
好的,听起来更像是策略模式
斑点

@Spotted:是的,该示例说明了策略模式,而不是装饰器模式(装饰器会向组件添加行为)。我会纠正我的回答,谢谢。
彼得·沃尔瑟

25

重写具体方法是否确实有代码异味?

是的,总的来说,压倒具体的方法是代码的味道。

因为基本方法具有开发人员通常会尊重的行为,所以更改将在您的实现执行其他操作时导致错误。更糟糕的是,如果他们改变了行为,您以前的正确实现可能会加剧问题。总的来说,遵守《里斯科夫替代原则》相当困难对于非平凡的具体方法,。

现在,在某些情况下可以做到这一点并且很好。半平凡的方法可以在一定程度上安全地覆盖。基本方法是只存在一个“理智默认”的行为,那种是将超过缠身有一些很好的用途。

因此,这对我来说似乎是一种气味-有时好,通常是坏,看看以确保。


29
您的解释很好,但我得出的结论恰恰相反:“不,通常,覆盖具体方法不是代码的味道”-当一个方法覆盖了不打算覆盖的方法时,它只是代码的味道。;-)
Doc Brown

2
@DocBrown就是这样!我还认为,解释(尤其是结论)与最初的陈述相反。
Tersosauros '16

1
@Spotted:最简单的反例是一个Number带有increment()将值设置为其下一个有效值的方法的类。如果将其子类化为EvenNumber在其中increment()添加两个而不是一个的地方(并且setter强制执行均匀性),则OP的第一个建议要求该类中每个方法的重复实现,第二个要求编写包装器方法以调用成员中的方法。继承的部分目的是让子类仅通过提供需要不同的方法来明确它们与父类的区别。
Blrfl

5
@DocBrown:有代码气味并不意味着某些事情一定很坏,只是可能
mikołak

3
@docbrown-对我来说,这与“偏重继承而非继承”并存。如果您要超越具体的方法,那么您已经因为拥有继承而处于微不足道的境地。您超越了本不应该做的事情的几率是多少?根据我的经验,我认为这很有可能。YMMV。
Telastyn '16

7

如果您需要重写具体方法,可以将其重写为

如果可以用这种方式重写它,则意味着您可以控制这两个类。但是,然后您知道是否将类A设计为以这种方式派生,如果这样做,则不是代码味道。

另一方面,如果您无法控制基类,则无法以这种方式进行重写。因此,要么将类设计为以这种方式派生(在这种情况下只是继续,它不是代码的味道),否则就不是,在这种情况下,您有两个选择:完全寻找另一种解决方案,或者无论如何继续进行,因为工作王牌设计纯正。


如果将A类设计为派生的,那么使用抽象方法清楚地表达意图会更有意义吗?重写该方法的人如何知道该方法是以此方式设计的?
斑点

@Spotted,在许多情况下,可能会提供默认实现,并且每个专业化只需要覆盖其中的一些,以扩展功能或提高效率。极端的例子是访客。用户可以从文档中了解如何设计这些方法。它必须在那里解释两种方式对替代的期望。
2016年

我理解您的观点,但我仍然认为,为了知道可以重写方法的方法,开发人员拥有的唯一方法就是阅读文档,这是很危险的:1)如果必须重写该方法,我不能保证将会完成。2)如果不能重写它,为了方便起见,有人会这样做。3)并非每个人都阅读该文档。另外,我从没有遇到需要覆盖整个方法而只覆盖部分方法的情况(并且我最终使用了模板模式)。
斑点

@ Spotted,1)如果必须重写它,则应为abstract; 但在很多情况下并不一定非要如此。2)如果不能被覆盖,则应该是final(非虚拟的)。但是仍然有很多情况下方法可能会被覆盖。3)如果下游开发人员不阅读文档,那么他们将自发自足,但是无论您采取何种措施,他们都将这样做。
Jan Hudec

好的,所以我们的观点分歧仅在于1个字。您说每个方法都应该是抽象或最终的,而我说每个方法都必须是抽象或最终的。:)在必须(且安全)重写方法的情况下,这些情况是什么?
斑点

3

首先,让我指出这个问题适用于某些打字系统。例如,在结构化打字Duck打字系统中,仅具有相同名称和签名的方法的类将意味着该类是类型兼容的(至少对于该特定方法,在Duck打字的情况下)。

(显然)这与静态类型语言(例如Java,C ++等)的领域非常不同。以及Java和C ++以及C#和其他语言中使用的强类型化的性质。


我猜您来自Java背景,如果合同设计者希望重写这些方法,则必须将这些方法显式标记为final。但是,在C#等语言中,默认设置相反。方法是(没有任何修饰的特殊关键字)“ 密封的 ”(C#的版本),并且必须明确声明为允许它们被覆盖。finalvirtual

如果已经使用重写的具体方法这些语言设计了API或库,则必须(至少在某些情况下)使其有意义。因此,覆盖未密封/虚拟/非final具体方法不是代码异味。     (当然,这是假定API的设计者遵循约定并将其设计的适当方法标记为virtual(C#)或标记为final(Java)。)


也可以看看


在鸭子类型中,两个具有相同方法签名的类是否足以使其类型兼容,或者是否还必须使这些方法具有相同的语义,以便它们可以被liskov替换为某些隐含基类?
韦恩·康拉德

2

是的,因为它可以导致调用超级反模式。它还可以使方法的最初目的变性,引入副作用(=>错误)并使测试失败。您是否会使用单元测试为红色(失败)的类?我不会

我会进一步说,最初的代码味道是方法不是abstractnor时final。因为它允许更改(通过重写)已构造实体的行为(此行为可以丢失或更改)。用abstractfinal的意图是明确的:

  • 摘要:您必须提供一种行为
  • 最终:您不能修改行为

向现有类添加行为的最安全方法是上一个示例所示:使用装饰器模式。

public interface A{
    void a();
}

public final class ConcreteA implements A{
    public void a() {
        ...
    }
}

public final class B implements A{
    private final ConcreteA concreteA;
    public void a(){
        //Additionnal behaviour
        concreteA.a();
    }
}

9
这种黑白方法并不是真的有用。想想Object.toString()在Java中......如果这是abstract,很多事情默认是行不通的,而当有人未覆盖的代码甚至不会编译toString()方法!如果是final,情况会更糟-除Java方式外,其他任何功能都无法将对象转换为字符串。正如@Peter Walser的答案所言默认实现(例如Object.toString()重要。特别是在Java 8默认方法中。
Tersosauros

@Tersosauros你是对的,如果toString()是这样的话,abstract否则final会很痛苦。我个人认为这种方法是坏的,最好不要完全使用它(例如equals和hashCode)。Java默认值的最初目的是允许具有兼容兼容性的API演变。
斑点

“可逆兼容性”一词有很多音节。
Craig

@Spotted对于此站点上对具体继承的普遍接受,我感到非常失望,我期望更好:/您的回答应该有更多的支持
TheCatWhisperer

1
@TheCatWhisperer我同意,但是另一方面,我花了很长时间才发现使用装饰而不是具体继承的微妙优势(实际上,当我开始编写测试时就很清楚了),你不能为此而责怪任何人。最好的办法是尝试教育其他人,直到他们发现真相(玩笑)为止。:)
斑点
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.