73

背景:

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

所以现在我开始觉得从具体类继承几乎没有合适的情况。一方面,对于非平凡类而言,里氏替换原则(LSP) 似乎几乎不可能满足;这里的许多其他问题似乎也反映了类似的观点。

所以我的问题:

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

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

4

18 回答 18

20

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

一个例子是转发包装类,能够转发到C实现的具体类的另一个对象I,例如启用装饰或简单的代码重用,C而无需继承自C您可以在Effective Java item 16中找到这样的示例,偏好组合而不是继承。(由于版权,我不想在这里发布它,但它实际上只是将所有方法调用转发I到包装的实现)。

于 2011-09-21T07:38:43.373 回答
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
于 2011-09-21T11:49:28.497 回答
14

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

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

于 2011-09-21T14:37:20.870 回答
12

一般来说,我从具体类派生的唯一时间是它们在框架中。源自AppletJApplet成为微不足道的例子。

于 2011-09-21T07:39:03.327 回答
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...
}

所以,我可以同时返回,最终用户也不明智。:-)

总结:如果您想要一个具有与您的具体类相同功能的自定义类而不改变具体类的结构,我建议扩展具体类。

于 2011-09-21T07:30:47.537 回答
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

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

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

于 2011-09-21T10:21:43.507 回答
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 
    }
}

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

于 2011-09-21T07:45:01.473 回答
4

装饰器模式是一种向类添加额外行为而又不会使其过于笼统的便捷方式,它大量使用了具体类的继承。它已经在这里提到过,但在某种程度上是“转发包装类”的学名。

于 2011-09-21T11:00:07.173 回答
3

很多答案,但我想添加自己的 0.02 美元。

我很少覆盖 concreate 类,但在某些特定情况下。当框架类被设计为可扩展时,至少已经提到了 1 个。通过一些示例,我想到了另外 2 个:

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

示例:我们覆盖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;
    }
}

希望这可以帮助。

于 2011-09-27T17:28:32.590 回答
2

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

我目前正在开发一个为用户计算矩阵的应用程序。用户可以提供设置来影响计算。有几种类型的矩阵可以计算,但有明显的相似之处,特别是在可配置性方面:矩阵 A 可以使用矩阵 B 的所有设置,但有额外的参数可以使用。在这种情况下,我从 ConfigObjectB 继承了我的 ConfigObjectA 并且它工作得很好。

于 2011-09-21T08:56:54.227 回答
2

出于您所说的原因,您的担忧也反映在经典原则“有利于组合而不是继承”中。我不记得上次从具体类继承是什么时候了。任何需要被子类重用的公共代码几乎总是需要为这些类声明抽象接口。按照这个顺序,我尝试更喜欢以下策略:

  1. 组合(无继承)
  2. 界面
  3. 抽象类

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

[编辑] 我将通过说当您控制架构时我看不到它的好用例来限定此声明。当然,当使用期望它的 API 时,whaddaya 会做什么?但我不明白这些 API 所做的设计选择。调用类应该总是能够根据依赖倒置原则声明和使用抽象。如果一个子类有额外的接口要使用,你要么必须违反 DIP 要么做一些丑陋的转换来获得这些接口。

于 2011-09-27T17:33:00.463 回答
2

一般来说,从抽象类继承比从具体类继承要好。一个具体的类必须为其数据表示提供定义,并且一些子类将需要不同的表示。由于抽象类不必提供数据表示,未来的子类可以使用任何表示,而不必担心与它们继承的表示冲突。即使我从来没有发现我觉得具体的继承是必要的。但是,当您为软件提供向后兼容性时,可能会出现一些具体继承的情况。在这种情况下,您可能已经专门化了 a class A,但您希望它具体化,因为您的旧应用程序可能正在使用它。

于 2011-09-21T09:03:19.107 回答
2
  1. 如果你想扩展侧库功能,继承具体类是唯一的选择。

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

于 2011-09-21T07:39:29.740 回答
2

我开始觉得从具体类继承几乎没有合适的情况。

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

这是来自小程序信息的示例。页

/* <!-- 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);
    }
}
于 2011-09-21T07:45:06.420 回答
1

来自gdata 项目

com.google.gdata.client.Service旨在充当可针对特定类型的 GData 服务进行定制的基类。

服务 javadoc:

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

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

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

扩展- 定义与服务关联的提要、条目和其他类型的预期扩展。

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

于 2011-09-21T07:45:00.333 回答
0

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

于 2011-09-21T14:23:41.660 回答
0

我发现 java 集合类是一个很好的例子。因此,您有一个 AbstractCollection,其中包含 AbstractList、AbstractSet、AbstractQueue 等子类……我认为这个层次结构设计得很好……为了确保没有爆炸,Collections 类及其所有内部静态类。

于 2011-09-21T07:55:14.033 回答
0

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

所以我的回答是:在所有你真的只想添加一些东西的情况下。也许这种情况并不经常发生。

于 2013-05-24T07:52:41.123 回答