从一个具体的类继承的好例子吗?[关闭]


73

背景:

作为Java程序员,我广泛地从接口继承(而不是:实现),有时我设计抽象基类。但是,我从来没有真正感到有必要将具体的(非抽象的)类作为子类(在我这样做的情况下,后来证明了另一种解决方案,例如委托会更好)。

因此,现在我开始感到几乎没有任何情况可以从具体类进行继承。一方面,对于非平凡的类,李斯科夫替换原理(LSP)似乎几乎是无法满足的。还有许多其他问题,这里似乎呼应了类似的意见。

所以我的问题是:

在哪种情况下(如果有)从具体类继承实际上有意义吗?您能否举一个从另一个具体类继承来的类的实际实例,在这种情况下,您认为这是受限制的最佳设计?我对满足LSP的示例(或满足LSP似乎不重要的示例)特别感兴趣。

我主要有Java背景,但是我对任何语言的示例都感兴趣。


4
在编写类的层次结构时,例如琐碎的Dog会扩展Animal。在编码数据模型时,通常会广泛使用继承。对我而言,对现实生活进行建模是在继承发挥作用时进行。
史密斯先生

假设委托代表了我的意思:使用子类的一个很好的理由是,使用Java和许多其他OOP语言来定义将接口的所有方法委托给另一个类的类是一个很大的麻烦。因此,通常的做法是通过建立层次结构来在类中累积越来越多的接口,在该层次结构中,每个子类都实现了基类未实现的另一个接口。不过,理想情况下,使接口委托更有意义。
sinelaw 2011年

5
从对象继承存在病理情况……
Jens Schauder

12
@ZJR:是什么让您相信我的“代码重用统计信息”低?除了继承以外,还有许多其他重用代码的方法。
sleske 2011年

1
我想知道同样的事情。看看我问的问题:stackoverflow.com/questions/3351666/why-use-inheritance-at-all
KaptajnKold 2011年

Answers:


20

您通常对接口有基本的实现I。如果您可以在没有抽象方法的情况下(例如通过钩子)提供可扩展性,则最好具有一个非抽象的骨架类,因为您可以实例化它。

一个示例是转发包装器类,以便能够转发到C实现的具体类的另一个对象I,例如,无需继承自即可进行修饰或简单的代码重用。您可以在“有效的Java”项目16中找到这样的示例,青睐于组合而不是继承。(由于版权原因,我不想将其发布在这里,但实际上只是将所有方法调用转发给包装的实现)。CCI


9
骨骼实现为+1。如果没有MouseAdapter提供多种方法事件侦听器接口的NOOP实现的Swing的“适配器”类(等),GUI组件的编写将更加痛苦。
gustafc 2011年

@gustafc:很好的例子。但是请注意,Swing适配器类是abstract,因此该示例并未真正解决我的问题。
sleske

假设每个类都必须是抽象的或密封的,但是声明具体的可继承类foo将声明一个抽象类foo和一个密封的类__impl_foo,这些类继承foo但不包含其他任何东西。进一步假设new foo()它将转换为new __impl_foo()。除了具体类型的反射名称之外,这样的替换会改变其他语义吗?
2013年

我想不是,除了反射类名称和反射修饰符。但是我看不到任何优势。
DaveFar

18

我认为以下是一个合适的好例子:

public class LinkedHashMap<K,V>
    extends HashMap<K,V>

另一个很好的例子是异常的继承:

public class IllegalFormatPrecisionException extends IllegalFormatException
public class IllegalFormatException extends IllegalArgumentException
public class IllegalArgumentException extends RuntimeException
public class RuntimeException extends Exception
public class Exception extends Throwable

6
异常的继承确实是一个好方法。关于HashMap / LinkedHashMap:为什么您觉得这是一个好例子?让两者都从接口/抽象类继承会更干净吗?
sleske 2011年

1
@sleske在我看来,绝对有理由说LinkedHashMap是HashMap(就LSP而言)。另外,我在这里看不到引入其他抽象级别的任何真正好处。无论如何,在这种特殊情况下,继承更多是实现细节。对于消费者而言,LinkedHashMap扩展HashMap并不重要(您应将Map map = new LinkedHashMap而不是HashMap map = new LinkedHashMap编码)。
安德烈(Andrey)

使用具有子类LinkedHM和HM的AbstractHashMap的一个好处是,您还可以用HM代替LinkedHM,而不仅仅是相反。当然,如果您始终使用地图,那将是一个有争议的问题。
sleske 2011年

14

我能想到的一种非常常见的情况是从基本的UI控件派生而来,例如表单,文本框,组合框等。它们是完整的,具体的并且能够独立存在。但是,它们中的大多数也是非常基本的,有时它们的默认行为不是您想要的。例如,几乎没有人会使用纯净Form的实例,除非他们可能会创建一个完全动态的UI层。

例如,在我写的一段软件中,最近达到了相对成熟的程度(这意味着我没有时间专注于开发它了:)),我发现我需要向ComboBoxes添加“延迟加载”功能,所以它不会第一个窗口加载需要50年(以计算机年为单位)。我还需要能够根据另一个组合中显示的内容自动过滤一个组合框中的可用选项,最后,我需要一种方法来“镜像”另一个组合控件中一个组合框的值,并对其中一个控件进行更改其他也一样。因此,我扩展了基本的ComboBox使其具有这些额外的功能,并创建了两种新类型:LazyComboBox,然后是MirroringComboBox。两者均基于完全可用的具体ComboBox控件,只是覆盖一些行为并添加一些其他行为。它们不是很松散耦合,因此也不是很牢固,但是增加的功能足够通用,如果我必须的话,我可以从头开始重写这两个类以完成相同的工作,可能更好。


12

一般而言,我从具体类派生的唯一时间是它们在框架中。源自AppletJApplet成为琐碎的例子。


1
JPanel或者,JComponent如果您想在Swing组件上进行一些自定义绘制。
Lukas Knuth

7

这是我正在执行的当前实现的示例。

在OAuth 2环境中,由于文档仍然处于起草阶段,因此规范一直在变化(截至撰写本文时,我们的版本为21)。

因此,我必须扩展我的具体AccessToken类以容纳不同的访问令牌。

在较早的草案中,没有token_type设置字段,因此实际的访问令牌如下:

public class AccessToken extends OAuthToken {

    /**
     * 
     */
    private static final long serialVersionUID = -4419729971477912556L;
    private String accessToken;
    private String refreshToken;
    private Map<String, String> additionalParameters;

    //Getters and setters are here
}

现在,有了返回的访问令牌token_type,我有了

public class TokenTypedAccessToken extends AccessToken {

    private String tokenType;
    //Getter and setter are here...
}

因此,我可以将两者都返回,而最终用户则是最明智的选择。:-)

简介:如果您希望自定义类具有与具体类相同的功能,而又不更改具体类的结构,则建议扩展具体类。


6

我主要有Java背景,但是我对任何语言的示例都感兴趣。

与许多框架一样,ASP.NET大量使用继承来在类之间共享行为。例如,HtmlInputPassword具有以下继承层次结构:

System.Object
  System.Web.UI.Control
    System.Web.UI.HtmlControls.HtmlControl          // abstract
      System.Web.UI.HtmlControls.HtmlInputControl   // abstract
        System.Web.UI.HtmlControls.HtmlInputText
          System.Web.UI.HtmlControls.HtmlInputPassword

在其中可以看到派生具体类的示例。

如果您正在构建框架-并且确定要这样做-您可能会发现自己想要一个不错的大继承层次结构。


6

其他用例将是覆盖默认行为:

可以说有一个使用标准Jaxb解析器进行解析的类

public class Util{

    public void mainOperaiton(){..}
    protected MyDataStructure parse(){
        //standard Jaxb code 
    }
} 

现在说我要对解析操作使用一些不同的绑定(说XMLBean),

public class MyUtil extends Util{

    protected MyDataStructure parse(){
      //XmlBean code code 
    }
}

现在,我可以将新绑定与超类的代码重用一起使用。


2
好点子。当然,这里的继承仅是必需的,因为基类愚蠢地忽略了对分析器使用依赖注入...
sleske 2011年

2
更普遍地说,OP就是这样说的:“ ...另一种解决方案,例如授权会更好。”

1
我同意Ben和Sleske的观点,这是一个典型的案例,其中DI是比继承更好的解决方案。您在这里仅将继承用于代码重用。
Sverre Rabbelier 2011年

我同意@SverreRabbelier,这就像使用继承进行代码重用一样,除非我遗漏了我不太了解的JAXB和XMLBean固有的东西。是否有充分的理由为什么它必须是具有通用逻辑并由XMLBean util类扩展的JAXB util类,或者很容易就相反了?如果是后者,对我而言,这始终意味着我不应该让两者中的一个扩展另一个,而应该拥有具有公共逻辑的抽象基类,并且让两个util类都可以对此进行扩展。
SantiBailors,2016年

4

装饰图案,添加额外的行为一类没有使它过于笼统的一个方便的方式,大量使用了具体类的继承。它已经在这里提到过,但是在某种科学上叫“转发包装类”。


3

答案很多,但我虽然会加上自己的$ 0.02。

我不经常重写concreate类,但是在某些特定情况下。在设计框架类以进行扩展时,至少已经提到了1种。我想到了另外两个例子:

1)如果我想调整具体班级的行为。有时,我想更改具体类的工作方式,或者想知道何时调用某个方法,以便触发某些事情。通常,具体的类会定义一个hook方法,其唯一用法是子类重写该方法。

示例:我们MBeanExporter之所以无法实现,是因为我们需要能够注销JMX bean:

public class MBeanRegistrationSupport {
    // the concrete class has a hook defined
    protected void onRegister(ObjectName objectName) {
    }

我们班:

public class UnregisterableMBeanExporter extends MBeanExporter {
    @Override
    protected void onUnregister(ObjectName name) {
        // always a good idea
        super.onRegister(name);
        objectMap.remove(name);
    }

这是另一个很好的例子。 LinkedHashMap旨在使其removeEldestEntry方法被重写。

private static class LimitedLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
    @Override
    protected boolean removeEldestEntry(Entry<K, V> eldest) {
        return size() > 1000;
    }

2)如果一个类与具体的类有很多重叠,除了对功能的一些调整。

示例:我的ORMLite项目处理持久化的Long对象字段和long原始字段。两者的定义几乎相同。 LongObjectType提供描述数据库如何处理long字段的所有方法。

public class LongObjectType {
    // a whole bunch of methods

whileLongType覆盖LongObjectType并且仅调整一个单一的方法来处理原语。

public class LongType extends LongObjectType {
    ...
    @Override
    public boolean isPrimitive() {
        return true;
    }
}

希望这可以帮助。


关于1):是的,提供钩子方法来重写是一种众所周知的模式。但是,我觉得依赖注入(或使用方面)是更好的选择,通常是更好的选择。当然,如果您迷上了带有hook方法的类,则必须忍受它。
sleske 2011年

关于2):在那里,我真的会创建一个抽象类和两个实现。我觉得这也更好地反映了类的关系(通用的基本功能,以两种不同的方式应用)。另外,如果以后的LongObjectType需要新的方法,则此方法更健壮。如果将它们放入抽象的顶级类中,那么您将不会忘记在两个子类中都覆盖它们。使用“ LongType扩展LongObjectType”版本,您可以添加到LongObjectType,而忘记在LongType中覆盖。
sleske 2011年

大约1):如果基类支持依赖注入,则很好。同样,这些是我正在谈论的当前实现的类。我真的不喜欢AOP。从架构的角度来看,这非常难以调试,而且显然那么明显。
灰色

关于2):当然,在某些情况下,我会使用抽象类,但这似乎并不正确。确实有一个BaseDataType我没有描述的isPrimitive()错误设置。然后,LongObjectType子类描述如何持久保留longs并LongType重写isPrimitive()to以返回true。有一个BaseLongObjectType似乎只是多余的给我。
灰色

2
  1. 如果您想扩展侧库功能,则继承混凝土类是唯一的选择。

  2. 例如,在现实生活中,您可以查看DataInputStream的层次结构,该层次结构为FilterInputStream实现了DataInput接口。


2

我开始感到几乎没有任何情况可以从具体类进行继承。

这是一个“几乎”。尝试写一个不扩展AppletJApplet

这是来自applet信息的例如页面

/* <!-- Defines the applet element used by the appletviewer. -->
<applet code='HelloWorld' width='200' height='100'></applet> */
import javax.swing.*;

/** An 'Hello World' Swing based applet.

To compile and launch:
prompt> javac HelloWorld.java
prompt> appletviewer HelloWorld.java  */
public class HelloWorld extends JApplet {

    public void init() {
        // Swing operations need to be performed on the EDT.
        // The Runnable/invokeLater() ensures that happens.
        Runnable r = new Runnable() {
            public void run() {
                // the crux of this simple applet
                getContentPane().add( new JLabel("Hello World!") );
            }
        };
        SwingUtilities.invokeLater(r);
    }
}

2

另一个很好的例子是数据存储类型。举一个精确的例子:红黑树是一种更具体的二叉树,但是检索数据和其他信息(例如大小)可以完全相同。当然,一个好的库应该已经实现了,但是有时您必须为问题添加特定的数据类型。

我目前正在开发一个为用户计算矩阵的应用程序。用户可以提供影响计算的设置。可以计算几种类型的矩阵,但是有一个明显的相似性,尤其是在可配置性方面:矩阵A可以使用矩阵B的所有设置,但可以使用其他参数。在那种情况下,我从ConfigObjectB继承了ConfigObjectA,并且效果很好。


2

通常,从抽象类继承比从具体类继承更好。具体的类必须为其数据表示提供定义,而某些子类将需要不同的表示。由于抽象类不必提供数据表示形式,因此将来的子类可以使用任何表示形式,而不必担心与它们继承的表示形式发生冲突。甚至我从来没有发现我觉得需要具体继承的情况。但是,在为软件提供向后兼容性时,尤其是在某些情况下可能会产生具体继承。在那种情况下class A,您可能有一个专门的a,但是您希望它是具体的,因为您的旧应用程序可能正在使用它。


2

出于您所说的原因,经典原则“偏重于继承而不是继承”也表达了您的担忧。我不记得我上次从一个具体的类继承过来的事情。子类需要重用的任何通用代码几乎总是需要为这些类声明抽象接口。按照这种顺序,我尝试采用以下策略:

  1. 组成(无继承)
  2. 接口
  3. 抽象类

从具体的类继承确实不是一个好主意。

[编辑]我将通过声明这种陈述来限定您的发言权,即当控制该体系结构时,我认为它没有很好的用例。当然,当使用期望的API时,whaddaya会做什么?但是我不理解这些API所做的设计选择。调用类应始终能够根据依赖关系反转原理声明和使用抽象。如果子类有其他接口要使用,则必须违反DIP或进行一些丑陋的转换才能获得这些接口。


1

来自gdata项目

com.google.gdata.client.Service设计为充当基类,可以针对特定类型的GData服务进行自定义。

服务javadoc:

Service类表示与GData服务的客户端连接。它封装了与GData服务器的所有协议级交互,并充当调用服务器上的操作并处理其结果的更高级别实体(提要,条目等)的帮助类。

此类提供访问任何GData服务所需的基本级通用功能。它还被设计为可以为特定类型的GData服务定制的基类。支持的自定义示例包括:

身份验证-为需要身份验证并且使用HTTP基本身份验证或摘要身份验证以外的服务的服务实现自定义身份验证机制。

扩展-为Feed,条目和与服务相关联的其他类型定义期望的扩展。

格式-定义可能由服务和客户端解析器以及生成器使用或处理的其他自定义资源表示形式。


好的例子。但是,对我而言,此类看起来应该是抽象的。它甚至具有默认可见性的构造函数,这意味着您只能在同一包内调用它,因此要从外部使用它,则无论如何都必须对其进行子类化。
sleske 2011年

0

我发现java集合类是一个很好的例子。因此,您有一个AbstractCollection,其子类如AbstractList,AbstractSet,AbstractQueue ...我认为此层次结构已经过精心设计..为了确保不爆炸,存在具有内部所有静态类的Collections类。


那只是我的意思:java集合类的类层次结构使用抽象类(AbstractCollection,AbstractList等)。我对具体类的继承很感兴趣。
sleske 2011年

哦,拜托,您是否没有注意到这些抽象类确实是在Java集合API本身中实现的(ArrayList,HashSet)。这些抽象类的很多实现都构成了骨干。
Scorpion

Erm,恐怕我不能跟着你。是的,抽象基类在den API中具有具体的子类。但这不是我的问题。我的问题是关于从具体类继承的,只有很少的Collection类可以继承
sleske 2011年

实际上,我可以找到的具体类的唯一集合类是LinkedHashSet(<HashSet)和Stack(<Vector)。可以说堆栈不是一个很好的设计(作为一个Vector,它可以让您以一种非常类似于堆栈的方式直接修改元素),并且LinkedHashSet也可以直接从AbstractSet继承。
sleske 2011年

哦,我将您的问题误解为“从类继承”,因此错过了注意到从具体类继承并不是您所关注的内容。从具体的类继承的范围非常有限(如果有的话),而且实例很少且实例值得商I,我不足为奇。
Scorpion

0

例如,您可以在GUI库中执行此操作。从纯粹的Component继承并委派给Panel并没有多大意义。直接从面板继承可能会容易得多。


0

只是一般的想法。抽象类缺少某些内容。每个派生类中缺少的内容是否有所不同是有道理的。但是您可能会遇到不想修改类而只想添加一些东西的情况。为了避免重复代码,您将继承。而且,如果您需要这两个类,那将是具体类的继承。

所以我的答案是:在所有情况下,您实际上只想添加一些内容。也许这种情况并不经常发生。

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.