【问题标题】:Abstract Base Class vs. Concrete Class as a SuperType抽象基类与作为超类型的具体类
【发布时间】:2011-03-09 06:23:42
【问题描述】:

在阅读了最优秀的书《Head First Design Patterns》之后,我开始向同事宣传模式和设计原则的好处。在赞美我最喜欢的模式——策略模式——的优点时,我被问到一个让我停顿的问题。当然,策略使用继承和组合,当一位同事问“为什么使用抽象基类而不是具体类?”时,我正在谈论“程序到接口(或超类型)而不是实现”的长篇大论。 .
我只能想出“好吧,你强制你的子类实现抽象方法并阻止它们实例化 ABC”。但老实说,这个问题让我措手不及。这些是在我的层次结构顶部使用抽象基类而不是具体类的唯一好处吗?

【问题讨论】:

  • 在我看来是的。但我认为强制从这个类继承的人实现一个方法是一种语言的一个非常重要的“特性”。有时您需要创建一个通用类,但无法实现所有功能,因为它太具体了。

标签: java c# design-patterns architecture abstract-base-class


【解决方案1】:

在以下场景中首选抽象基类:

  1. 没有子类 1234562 就不能存在基类 => 基类只是抽象的,它不能被实例化。
  2. 基类不能有方法的完整或具体实现 => 方法的实现是基类不完整,只有子类可以提供完整的实现。
  3. 基类提供了方法实现的模板,但还是依赖Concrete类来完成方法实现-Template_method_pattern

一个简单的例子来说明以上几点

Shape 是抽象的,如果没有像Rectangle 这样的具体形状,它就无法存在。绘制Shape 不能在Shape 类中实现,因为不同的形状有不同的公式。处理场景的最佳选择:将draw() 实现留给子类

abstract class Shape{
    int x;
    int y;
    public Shape(int x,int y){
        this.x = x;
        this.y = y;
    }
    public abstract void draw();
}
class Rectangle extends Shape{
    public Rectangle(int x,int y){
        super(x,y);
    }
    public void draw(){
        //Draw Rectangle using x and y : length * width
        System.out.println("draw Rectangle with area:"+ (x * y));
    }
}
class Triangle extends Shape{
    public Triangle(int x,int y){
        super(x,y);
    }
    public void draw(){
        //Draw Triangle using x and y : base * height /2
        System.out.println("draw Triangle with area:"+ (x * y) / 2);
    }
}
class Circle extends Shape{
    public Circle(int x,int y){
        super(x,y);
    }
    public void draw(){
        //Draw Circle using x as radius ( PI * radius * radius
        System.out.println("draw Circle with area:"+ ( 3.14 * x * x ));
    }
}

public class AbstractBaseClass{
    public static void main(String args[]){
        Shape s = new Rectangle(5,10);
        s.draw();
        s = new Circle(5,10);
        s.draw();
        s = new Triangle(5,10);
        s.draw();
    }
}

输出:

draw Rectangle with area:50
draw Circle with area:78.5
draw Triangle with area:25

以上代码涵盖了第1点和第2点。如果基类有一些实现并调用子类方法完成draw()功能,您可以将draw()方法更改为模板方法。

现在与模板方法模式相同的示例:

abstract class Shape{
    int x;
    int y;
    public Shape(int x,int y){
        this.x = x;
        this.y = y;
    }
    public abstract void draw();

    // drawShape is template method
    public void drawShape(){
        System.out.println("Drawing shape from Base class begins");
        draw();
        System.out.println("Drawing shape from Base class ends");       
    }
}
class Rectangle extends Shape{
    public Rectangle(int x,int y){
        super(x,y);
    }
    public void draw(){
        //Draw Rectangle using x and y : length * width
        System.out.println("draw Rectangle with area:"+ (x * y));
    }
}
class Triangle extends Shape{
    public Triangle(int x,int y){
        super(x,y);
    }
    public void draw(){
        //Draw Triangle using x and y : base * height /2
        System.out.println("draw Triangle with area:"+ (x * y) / 2);
    }
}
class Circle extends Shape{
    public Circle(int x,int y){
        super(x,y);
    }
    public void draw(){
        //Draw Circle using x as radius ( PI * radius * radius
        System.out.println("draw Circle with area:"+ ( 3.14 * x * x ));
    }
}

public class AbstractBaseClass{
    public static void main(String args[]){
        Shape s = new Rectangle(5,10);
        s.drawShape();
        s = new Circle(5,10);
        s.drawShape();
        s = new Triangle(5,10);
        s.drawShape();
    }
}

输出:

Drawing shape from Base class begins
draw Rectangle with area:50
Drawing shape from Base class ends
Drawing shape from Base class begins
draw Circle with area:78.5
Drawing shape from Base class ends
Drawing shape from Base class begins
draw Triangle with area:25
Drawing shape from Base class ends

一旦您决定必须使用方法abstract,您有两个选择:用户interfaceabstract 类。您可以在interface 中声明您的方法,并将abstract 类定义为实现interface 的类。

【讨论】:

    【解决方案2】:

    抽象基类通常用于设计人员想要强制一种架构模式的场景,其中某些任务将由所有类以相同的方式执行,而其他行为依赖于子类。 示例:

    public abstract class Animal{
    
    public void digest(){
    
    }
    
    public abstract void sound(){
    
    }
    }
    
    public class Dog extends Animal{
    public void sound(){
        System.out.println("bark");
    }
    }
    

    策略模式要求设计者使用组合行为来处理存在一系列行为的情况。

    【讨论】:

      【解决方案3】:

      基类应该是抽象的还是具体的问题很大程度上取决于恕我直言,一个只实现类中所有对象共有的行为的基类对象是否有用。考虑一个 WaitHandle。对其调用“等待”将导致代码阻塞,直到满足某些条件,但没有常见的方法告诉 WaitHandle 对象其条件得到满足。如果可以实例化“WaitHandle”,而不是只能实例化派生类型的实例,那么这样的对象将不得不永远等待,或者永远等待。后一种行为将毫无用处。前者可能很有用,但几乎可以通过静态分配的 ManualResetEvent 来实现(我认为后者会浪费一些资源,但如果是静态分配的,那么总的资源损失应该是微不足道的)。

      在许多情况下,我认为我的偏好是使用对接口的引用而不是对抽象基类的引用,但为接口提供一个提供“模型实现”的基类。因此,任何地方都会使用对 MyThing 的引用,人们会提供对“iMyThing”的引用。很可能 99%(甚至 100%)的 iMyThing 对象实际上是一个 MyThing,但如果有人需要一个从其他东西继承的 iMyThing 对象,可以这样做。

      【讨论】:

        【解决方案4】:

        如果客户端依赖于“隐含的行为契约”,​​它会针对实现和无保证的行为进行编程。在遵循合同的同时覆盖该方法只会暴露客户端中的错误,而不是导致它们。

        OTOH,如果所讨论的方法是非虚拟的,那么假设不存在的合同的错误不太可能导致问题 - 即,覆盖它不会导致问题,因为它不能被覆盖。只有改变原始方法的实现(同时仍然遵守约定)才能破坏客户端。

        【讨论】:

          【解决方案5】:

          首先,策略模式几乎不应该在现代 C# 中使用。它主要适用于不支持函数指针、委托或一等函数的 Java 等语言。您将在 IComparer 等接口的旧版 C# 中看到它。

          对于抽象基类与具体类,Java 中的答案始终是“在这种情况下什么效果更好?”如果你的策略可以共享代码,那么一定要让他们这样做。

          设计模式不是关于如何做某事的说明。它们是对我们已经完成的事情进行分类的方法。

          【讨论】:

            【解决方案6】:

            程序是接口,而不是实现与抽象类和具体类关系不大。还记得template method pattern 吗?类,无论是抽象的还是具体的,都是实现细节。

            使用抽象类而不是具体类的原因是您可以在不实现它们的情况下调用方法,而是将它们留给子类来实现。

            对接口编程是另一回事——它定义您的 API 做什么,而不是如何它是做什么的。这由接口表示。

            请注意一个关键区别 - 您可以拥有 protected abstract 方法,这意味着这是实现细节。但所有接口方法都是公开的 - API 的一部分。

            【讨论】:

            • 除此之外,“程序到实现”将做一些事情,比如使用引用来访问类的私有字段。
            【解决方案7】:

            如果您需要实现特定的方法,请使用接口。如果有可以拉出来的共享逻辑,就使用抽象基类。如果基本功能集本身是完整的,那么您可以使用 concreate 类作为基础。抽象基类和接口不能直接实例化,这是优点之一。如果你可以使用具体类型,那么你需要重写方法,这有一种“代码味道”。

            【讨论】:

            • 嗯?为什么方法会压倒气味?
            • 具有虚拟方法的代码在未来更难在不破坏某些隐含行为契约的情况下进行更改。所以在 .NET 中,他们决定让方法可覆盖必须是一个有意识的决定,并且应该仔细考虑。因此,任何没有实现者指令的可覆盖方法都是“代码味道”。
            • 如果你有一个类,并且你以某种方式对行为进行编码。然后,从该类继承,并决定要更改行为,IMO,那是有味道的。我认为开发人员应该重构行为契约(接口或抽象基类)并创建两个具有明确定义的存在原因的具体实现。拥有一个抽象基类或接口使扩展在如何更改或替换行为方面更加明确。覆盖感觉更像是蛮力。
            【解决方案8】:

            是的,尽管您也可以使用接口来强制类实现特定方法。

            使用抽象类而不是具体类的另一个原因是抽象类显然不能被实例化。有时您也不希望这种情况发生,因此最好使用抽象类。

            【讨论】:

              猜你喜欢
              • 2012-05-15
              • 1970-01-01
              • 1970-01-01
              • 2011-01-08
              • 1970-01-01
              • 2013-01-02
              • 2016-10-03
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多