Java 8中多继承的用法

26

我是在使用Java 8的特性,还是滥用它?

请参考下面的代码和解释来了解为什么选择这样做。

public interface Drawable {
    public void compileProgram();

    public Program getProgram();

    default public boolean isTessellated() {
        return false;
    }

    default public boolean isInstanced() {
        return false;
    }

    default public int getInstancesCount() {
        return 0;
    }

    public int getDataSize();

    public FloatBuffer putData(final FloatBuffer dataBuffer);

    public int getDataMode();

    public boolean isShadowReceiver();

    public boolean isShadowCaster();    //TODO use for AABB calculations

    default public void drawDepthPass(final int offset, final Program depthNormalProgram, final Program depthTessellationProgram) {
        Program depthProgram = (isTessellated()) ? depthTessellationProgram : depthNormalProgram;
        if (isInstanced()) {
            depthProgram.use().drawArraysInstanced(getDataMode(), offset, getDataSize(), getInstancesCount());
        }
        else {
            depthProgram.use().drawArrays(getDataMode(), offset, getDataSize());
        }
    }

    default public void draw(final int offset) {
        if (isInstanced()) {
            getProgram().use().drawArraysInstanced(getDataMode(), offset, getDataSize(), getInstancesCount());
        }
        else {
            getProgram().use().drawArrays(getDataMode(), offset, getDataSize());
        }
    }

    default public void delete() {
        getProgram().delete();
    }

    public static int countDataSize(final Collection<Drawable> drawables) {
        return drawables.stream()
                .mapToInt(Drawable::getDataSize)
                .sum();
    }

    public static FloatBuffer putAllData(final List<Drawable> drawables) {
        FloatBuffer dataBuffer = BufferUtils.createFloatBuffer(countDataSize(drawables) * 3);
        drawables.stream().forEachOrdered(drawable -> drawable.putData(dataBuffer));
        return (FloatBuffer)dataBuffer.clear();
    }

    public static void drawAllDepthPass(final List<Drawable> drawables, final Program depthNormalProgram, final Program depthTessellationProgram) {
        int offset = 0;
        for (Drawable drawable : drawables) {
            if (drawable.isShadowReceiver()) {
                drawable.drawDepthPass(offset, depthNormalProgram, depthTessellationProgram);
            }
            offset += drawable.getDataSize();   //TODO count offset only if not shadow receiver?
        }
    }

    public static void drawAll(final List<Drawable> drawables) {
        int offset = 0;
        for (Drawable drawable : drawables) {
            drawable.draw(offset);
            offset += drawable.getDataSize();
        }
    }

    public static void deleteAll(final List<Drawable> drawables) {
        drawables.stream().forEach(Drawable::delete);
    }
}

public interface TessellatedDrawable extends Drawable {
    @Override
    default public boolean isTessellated() {
        return true;
    }
}

public interface InstancedDrawable extends Drawable {
    @Override
    default public boolean isInstanced() {
        return true;
    }

    @Override
    public int getInstancesCount();
}

public class Box implements TessellatedDrawable, InstancedDrawable {
    //<editor-fold defaultstate="collapsed" desc="keep-imports">
    static {
        int KEEP_LWJGL_IMPORTS = GL_2_BYTES | GL_ALIASED_LINE_WIDTH_RANGE | GL_ACTIVE_TEXTURE | GL_BLEND_COLOR | GL_ARRAY_BUFFER | GL_ACTIVE_ATTRIBUTE_MAX_LENGTH | GL_COMPRESSED_SLUMINANCE | GL_ALPHA_INTEGER | GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH | GL_ALREADY_SIGNALED | GL_ANY_SAMPLES_PASSED | GL_ACTIVE_SUBROUTINE_UNIFORM_MAX_LENGTH | GL_ACTIVE_PROGRAM | GL_ACTIVE_ATOMIC_COUNTER_BUFFERS | GL_ACTIVE_RESOURCES | GL_BUFFER_IMMUTABLE_STORAGE;
        int KEEP_OWN_IMPORTS = UNIFORM_PROJECTION_MATRIX.getLocation() | VS_POSITION.getLocation();
    }
//</editor-fold>
    private FloatBuffer data;
    private Program program;

    private final float width, height, depth;

    public Box(final float width, final float height, final float depth) {
        this.width = width;
        this.height = height;
        this.depth = depth;
        data = generateBox();
        data.clear();
    }

    @Override
    public void compileProgram() {
        program = new Program(
                new VertexShader("data/shaders/box.vs.glsl").compile(),
                new FragmentShader("data/shaders/box.fs.glsl").compile()
        ).compile().usingUniforms(
                        UNIFORM_MODEL_MATRIX,
                        UNIFORM_VIEW_MATRIX,
                        UNIFORM_PROJECTION_MATRIX,
                        UNIFORM_SHADOW_MATRIX
                        );
    }

    @Override
    public int getInstancesCount() {
        return 100;
    }

    @Override
    public Program getProgram() {
        return program;
    }

    @Override
    public int getDataSize() {
        return 6 * 6;
    }

    @Override
    public FloatBuffer putData(final FloatBuffer dataBuffer) {
        FloatBuffer returnData = dataBuffer.put(data);
        data.clear();   //clear to reset data state
        return returnData;
    }

    @Override
    public int getDataMode() {
        return GL_TRIANGLES;
    }

    @Override
    public boolean isShadowReceiver() {
        return true;
    }

    @Override
    public boolean isShadowCaster() {
        return true;
    }

    private FloatBuffer generateBox() {
        FloatBuffer boxData = BufferUtils.createFloatBuffer(6 * 6 * 3);

        //put data into boxData

        return (FloatBuffer)boxData.clear();
    }
}

首先,我是如何得到这段代码的:

  1. 我从 Drawable 接口开始,每个实现都有自己的 drawDepthPassdrawdelete 方法。

  2. delete 重构为默认方法很容易,微不足道,并且不应该出错。

  3. 但是,为了能够重构 drawDepthPassdraw,我需要访问一个 Drawable 是否被细分和/或实例化的信息,因此我添加了公共(非默认)方法 isTessellated()isInstanced()getInstancesCount()

  4. 然后我发现,对于每个 Drawable 实现来说,手动实现这些方法可能稍微有些麻烦,因为我们程序员懒惰。

  5. 因此,我在 Drawable 中添加了默认方法,给出了最基本的 Drawable 行为。

  6. 接着,我意识到我还是有点懒,不想为细分和实例化变体手动实现它们。

  7. 因此,我创建了 TessellatedDrawableInstancedDrawable,它们分别提供默认的 isTessellated()isInstanced()。在 InstancedDrawable 中,我撤销了 getInstancesCount() 的默认实现。

因此,可以得到以下结果:

  • 普通的 Drawablepublic class A implements Drawable
  • 细分的 Drawablepublic class A implements TessellatedDrawable
  • 实例化的 Drawablepublic class A implements InstancedDrawable
  • 细分和实例化的 Drawablepublic class A implements InstancedDrawable, TessellatedDrawable

只是为了确保你,这一切都编译并运行良好。在 Java 8 中,“implements InstancedDrawable, TessellatedDrawable”被完美地处理,因为从哪个接口获取功能从来没有歧义。

现在进入我的一个小 OOP 设计评估:

  • 每个Drawable实际上都是一个Drawable,因此Collection<Drawable>将不会出错。
  • 可以将所有TessellatedDrawable和/或InstancedDrawable分组,无论其实现方式如何。

我还考虑过使用建造者模式,但这是一种用于创建大量唯一实例的对象的模式,而这不是我们在这里做的事情,也不涉及对象的构造函数。

所以第一个和最终的问题是:我是在使用Java 8的一个特性,还是误用了它?


哇,非常详细的问题,谢谢。 - Caffeinated
这是一个很好的问题,但也许更适合在 Code Review 而不是 Stack Overflow 上提问,因为这涉及到风格判断而非正确性。从我个人的直觉来看,除非真正需要,否则应避免使用多重继承,因为在其他某些语言中,它可能会导致混乱…而且我不太喜欢现在就将自己局限于 Java 8 的想法。在你的设计中,我没有看到明显的错误,但我会犹豫是否要选择这条路。 - keshlam
6
这是一个有趣的问题,因为它探索了新语言功能的维度。不过,这是一个非常冗长的问题(抱歉)。能不能精简一下,只留下必要的部分?另外,您这里有两个问题:1)这是一种使用/滥用语言特征吗?2)您会如何处理这个设计空间?最好将它们分解成单独的、更易回答的问题! - David Bullock
@DavidBullock 我认为阅读整个故事至少和问题本身同样重要。 - skiwi
1个回答

12
首先,如果它能够正常工作并且实现了你想要的功能,未来没有出现破坏性问题,那么说你误用它是没有意义的。毕竟,它完成了任务,对吧?像默认方法和静态方法这样的特征是为了特定目的而添加到接口中的,但如果它们可以帮助你实现其他目标,那么要么这是对新特性创造性使用的结果,要么是粗暴和不干净的黑客行为 :-) 在某种程度上,这是一个品味问题。
有了这个角度,我在API中寻找的,并在设计API时尝试做的,是将API的客户端与API的实现者区分开来。 API的典型客户端或用户从某个地方获取某个接口类型的引用,并调用其中的方法来使一些事情发生。实现者为接口中定义的方法提供实现,重写方法,并(如果进行子类化)调用超类方法。通常,旨在由客户端调用的方法与旨在从子类调用的方法不同。
在我的看法中,在Drawable接口中混合了这些概念。当然,Drawable的客户端将执行诸如在其上调用draw或drawDepthPass方法之类的操作。非常好。但是查看drawDepthPass的默认实现时,它使用isTessellated和isInstanced方法获取某些信息,然后使用这些信息选择程序并按特定方式调用它的方法。封装这些逻辑在一个方法中是可以的,但是为了将其放入默认方法中,getter必须强制进入公共接口。
当然,我对您的模型可能是错误的,但在我看来,这种逻辑更适合抽象超类和子类关系。抽象超类实现一些处理所有Drawable的逻辑,但是它通过像isTesselated或isInstanced这样的方法与特定的Drawable实现进行协商。在抽象超类中,这些将是受保护的方法,需要子类实现。将这个逻辑放入接口的默认方法中,所有这些方法都必须是公共的,这会使客户端接口变得混乱。其他看起来相似的方法是getDataMode,isShadowReceiver和isShadowCaster。客户端是否应该调用这些方法,还是它们在逻辑上是内部实现?
这凸显出尽管添加了默认方法和静态方法,但接口仍然面向客户端,而不是支持子类。原因如下:
  • 接口只有公共成员。
  • 抽象类可以拥有受保护的方法,供子类覆盖或调用。
  • 抽象类可以有私有方法来实现代码共享。
  • 抽象类可以拥有字段(状态),可以是受保护的以与子类共享状态,否则通常为私有的。
  • 抽象类可以拥有最终方法,强制执行某些行为策略的规则。

我还注意到Drawable接口族中存在一个问题,就是它使用默认方法相互覆盖的能力,允许一些简单的混合到像Box这样的实现类中。可以通过简单地说implements TessellatedDrawable来避免繁琐的重写isTesselated方法!问题在于,这现在成为了实现类类型的一部分。让客户端知道Box也是TessellatedDrawable有用吗?还是这只是一种使内部实现更清晰的方案?如果是后者,那么最好将这些混合接口(如TessellatedDrawableInstancedDrawable)设置为非公共接口(即包私有)。

还要注意,这种方法会使类型层次结构变得混乱,可能会使代码更加令人困惑。通常来说,新的类型意味着一个新的概念,但似乎将接口定义为仅定义返回布尔常量的默认方法有些笨重。

此外,这个模型中另一点需要指出,被混合在一起的特征非常简单,它们只是布尔常量。如果有一个Drawable实现,例如开始时不是实例化的,后来可以变成实例化的,则不能使用这些混合接口。默认实现在可做的事情上真的非常受限制。它们无法调用实现类的私有方法或检查字段,因此它们的使用相当有限。以这种方式使用接口几乎就像使用它们作为标记接口,只是稍微增加了通过调用方法获取特征的能力,而不是使用instanceof。似乎除此之外没有太多用处。

Drawable接口中的静态方法大多数都是合理的。它们是客户端导向的实用程序,并提供了由公共实例方法提供的逻辑的合理聚合。

最后,还有一些关于模型的奇怪之处,虽然它们与默认和静态方法的使用无直接关系。

似乎Drawable拥有Program,因为存在实例方法compileProgramgetProgramdelete。然而,drawDepthPass等方法要求客户端传入两个程序,其中一个根据布尔值获取器的结果进行选择。我不清楚调用者应该在哪里选择正确的程序。

类似的问题也出现在关于 drawAll 方法和 offset 值的使用上。在 Drawable 的列表中,每个 Drawable 都需要根据其数据大小使用特定的偏移量进行绘制。然而最基本的方法 draw 看起来要求调用者传递一个偏移量,这似乎是将大责任推给调用者。因此,也许偏移量的处理应该在实现内部完成。
有一些方法接受 Drawable 列表,并调用 stream(),然后调用 forEach() 或 forEachOrdered()。这是不必要的,因为 List 有一个 forEach 方法,继承自 Iterable。
我认为探索如何使用这些新特性是非常好的。它们还很新,尚未出现通常被接受的风格。像这样的实验和讨论有助于发展这种风格。另一方面,我们也需要小心,不要仅仅因为它们是闪亮新功能就使用它们。

好答案!接口只应该公开真正的功能,这是一个非常好的观点,也许我的设计在这方面确实有所不足... - skiwi
5
好的回答!这就是我们在Code Review上提供的答案类型,如果你喜欢撰写这个答案,请加入我们吧 :) - Mathieu Guindon
我会重新设计一些内容,然后准备进行代码审查,如果情况符合要求的话,那么我所做的似乎是允许的。 - skiwi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接