【问题标题】:Ways to circumvent lack of multiple inheritance acceptably in Java在 Java 中可以接受地规避缺乏多重继承的方法
【发布时间】:2014-01-11 22:50:19
【问题描述】:

我正在制作游戏。我有一个实体类,它是一对 x,y 坐标和宽度和高度。

我想将此实体类子类化为许多新类,以添加功能组的组合。我想添加几个“组”功能:

Moveable {
    public float getVelocityX();
    public void setVelocityX(float velocityX);
    public float getVelocityY();
    public void setVelocityY(float velocityY);
    public void moveRelative(float dx, float dy);
    public void setPosX(float posX);
}

Rotatable {
    public void setAngularVelocity(float degreesPerSecond);
    public void setAbsoluteAngle(float angle);
}

Resizable {
    void setAbsoluteSize(int width, int height);
    void setRelativeSize(int widthGrowFactor, int heightGrowFactor);

    public void setWidth(float width);
    public void setHeight(float height);
}

...以及更多(可更新、可绘制、可碰撞)。

一些实体可以旋转/被绘制但不能移动/碰撞/更新/调整大小:“静态”不可触摸的东西,例如背景动画(云、太阳、星星)。

一些实体可以移动/旋转/被绘制/更新/碰撞/调整大小:玩家角色和怪物。

某些实体只能被绘制:游戏的 HUD。

一些实体可以移动/被绘制/更新/碰撞,但不能旋转/调整大小:例如枪声。

如您所见,我需要上述功能的组合,并且可以通过一些多重继承轻松解决。

为什么不用你问的接口来做呢?因为那会让我为所有方法复制/粘贴大量代码,并且创建新类型的实体类将永远引入复制/粘贴。

您可能会问,为什么我不只是创建一个可以完成所有工作的子类,然后覆盖不应与空方法一起使用的方法,或者在那些无效的方法中抛出 UnsupportedOperationException?即使每个方法中只有一行,这仍然是复制/粘贴。一个只能 move() 的实体可能会以 50 个方法结束,并带有“throw new UnsupportedOperationException();”。这是不可接受的。

如果你建议适配器模式,我希望你能发布一个适配器模式如何做类似事情的示例(我已经对此进行了辩论)。

如果您建议作文,请说明我将如何访问受保护的字段。

谢谢!

【问题讨论】:

  • 您所描述的可以使用接口而不是子类轻松实现。另一种方法是使用对象组合而不是继承。如果您有多个使用相同“代码”的接口实现,则可以将代码重构为 Action f.e.然后将实现传递给具体操作(类似于命令模式)
  • 随意展示如何。
  • 看起来使用“has-a”关系(has-a Location,has-a Size)比尝试使用继承要容易得多。
  • 这样的问题会使所有实例都具有相同的方法,并且在那些云不可碰撞的实例中,您建议我在这些方法中做什么?抛出不受支持的异常?我已经在我的问题中讨论过这个解决方案。
  • 您可能有兴趣知道使用 java 8(我认为下个月发布)您可以实现安全的多重继承形式,因为接口被允许default methods 从而接口可以具有方法主体除非实现类提供自己的实现

标签: java oop inheritance multiple-inheritance


【解决方案1】:

正如罗曼在评论中所说:

Java 确实允许interfaces 的多重继承。如果您只需要继承 is-a 关系和一组有保证的方法来与对象通信,那么这非常有效。

只能继承一个class(抽象与否)(以及需要的许多接口)。这是为了避免“钻石继承”的复杂性——网络搜索该短语以了解更多信息。如果您确实需要重用来自多个来源的逻辑,通常的解决方法是在实用程序类中定义其他共享行为,并让您的类委托给该实例。是的,这是一个 has-a 而不是 is-a 关系,但是由于 is-a 可以抽象为一个接口,您可以获得组合行为的所需最终外观。 (与菱形继承不同,这迫使您明确决定要调用哪个实现。)

【讨论】:

  • 我的标题应该告诉你我知道多重继承是不允许的。我还写了关于使用多个接口及其问题的文章。
  • 实用程序课程可以解决您的反对意见,我相信。另一种选择是离开 Java。
  • @keshlam 或等待几个月的 Java 8 和 interface default methods
  • 太棒了(他讽刺地说)。钻石传承,我们来了。好的,它有一个特定的解析规则,但这并不能阻止人们绝望地混淆自己。
  • @kesham 我可以看到它被广泛滥用,但使用得当我期待它
【解决方案2】:

根据合同,您必须实现接口中定义的方法。但是,您可以在这些方法中做任何您喜欢的事情。在这个简单的示例中,为每个实例创建了MoveableRotatable 的进一步实现类。对相应操作的请求被转发到实现。

通过这种方式,您可以与多个对象共享相同的实现。操作本身甚至能够管理其状态而不是委托类。

public SomeClass implements Moveable, Rotatable
{
    // the implementation provide a concrete implementation
    // you can even exchange the implementation with new ones
    private Moveable movaAction = new MoveImpl();
    private Rotatable rotateAction = new RotateImpl();

    public float getVelocityX()
    {
        return moveAction.getVelocityX();
    }

    public void setVelocityX(float velocityX)
    {
        moveAction.getVelocityX(velocityX);
    }

    ...

    public void setAngularVelocity(float degreesPerSecond)
    {
        rotateAction.setAngularVelocity(degreesPerSecond);
    }
    ...
}

如果您进一步定义 f.e. registerNewMove(Moveable action) 如果需要,您甚至可以在运行时交换具体的实现(策略模式)。

【讨论】:

    【解决方案3】:

    如果所有子类的代码都相同,那么它可能在超类中。你可以这样做:

    public final move() {
        if (!isMovable()) {
            throw new UnsupportedOperationException("This entity can't be moved");
        }
        // TODO implement the actual move
    }
    
    public abstract boolean isMovable();
    

    如果你想有接口来判断一个实体是否可移动,你甚至可以在超类中实现 isMovable() 方法:

    public final boolean isMovable() {
        return this instanceof Movable;
    }
    

    【讨论】:

    • 这将在所有 6 个接口的所有方法中添加 3 行带有“throw new”的行,目前在“设计阶段”有 19 个方法。我预计它会增长很多。
    • 那又怎样?他们在一个班级。如果你想要一个有 19 个方法的类,你不应该害怕有 19 * 3 行的琐碎代码。您确定所有这些代码都应该在实体本身中,而不是在 EntityMover 类中吗?你确定你真的想要子类吗?如果 Sun 与 Star 的区别仅在于其属性值,为什么不将所有事物都设为具有不同状态的 Entity 实例?
    • BackgroundEntity sun = new BackgroundEntity(xxx);背景实体星 = 新背景实体(xxx);它们将是同一类,具有相同的功能集,但外观不同。他们在阶级方面没有区别。他们可以can rotate/be drawn but not move/collide/update/resize。用于这两个的类与用于实例化项目符号的类不同,因为项目符号具有不同的功能集。
    【解决方案4】:

    也许从完全不同的角度看待您的问题会很有用。您是否考虑过,不是将功能构建到对象的层次结构中,而是将功能构建到对象的初始化中。

    尝试这样的开始:

    static class Thing {
        int x;
        int y;
        float w;
        float h;
        float angle;
    
    }
    
    enum Op {
        Translate{
            @Override
            void op (Thing it, Thing op) {
                it.x += op.x;
                it.y += op.y;
            }
        },
        Rotate{
            @Override
            void op (Thing it, Thing op) {
                it.angle += op.angle;
            }
        },
        Scale{
            @Override
            void op (Thing it, Thing op) {
                it.w *= op.w;
                it.h *= op.h;
            }
        };
    
        abstract void op(Thing it, Thing op);
    }
    

    现在你有了一个可以操作的基类Thing。构建合法操作现在变成了数据处理问题,而不是层次结构问题。

    【讨论】:

    • 假设需要更改的数据已经存在于原始对象中。它不是。如果它不是可绘制的,它就没有纹理,因此 setTexture()/getTexture()/draw() 方法不能依赖已经存在的成员字段,因为扩展应该添加它们。您的解决方案要求我将所有可能的字段放入我可能需要的基类中(您在示例中放入 x,y,h,w,angle)。如果我这样做,我也可以为它们添加方法。进一步的例子:一个 Collideable 对象有一个碰撞检测器(一个粗略或精确的算法 impl),而 non-collis 没有。
    • @Xabster - 我想这个想法的本质是指出您实际上不需要将 所有 功能放入类中 - 您可以使用 机械例如这样。精心设计的机器和接口驱动的继承功能的混合通常可以产生更直观的 API。请记住,您应该根据对象而不是所做来设计对象。
    猜你喜欢
    • 1970-01-01
    • 2017-06-04
    • 1970-01-01
    • 2013-01-07
    • 2011-09-16
    • 2019-12-05
    • 1970-01-01
    • 2012-03-17
    • 2020-02-04
    相关资源
    最近更新 更多