在 Java 8 中使用多重继承

我是在使用 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. 我从接口开始,每个实现都有自己的、和方法。DrawabledrawDepthPassdrawdelete

  2. 重构到一个方法很容易,微不足道,不应该是错误的。deletedefault

  3. 但是,为了能够重构,我需要访问a是否被镶嵌和/或实例化,所以我添加了公共(非默认)方法,和。drawDepthPassdrawDrawableisTessellated()isInstanced()getInstancesCount()

  4. 然后我发现这会有点麻烦,因为我们程序员很懒惰,在每个.Drawable

  5. 因此,我将方法添加到 中,给出了最基本的行为。defaultDrawableDrawable

  6. 然后我想我仍然很懒惰,不想为曲面细分和实例化变体手动实现它。

  7. 所以我创造了和那个提供和分别。并在我撤销了.TessellatedDrawableInstancedDrawabledefaultisTessellated()isInstanced()InstancedDrawabledefaultgetInstancesCount()

因此,我可以拥有以下内容:

  • 正常: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设计评估:

  • 每一个其实都是一个,所以不会断裂。DrawableDrawableCollection<Drawable>
  • 可以对所有和/或 进行分组,而与它的确切实现方式无关。TessellatedDrawableInstancedDrawable

我有其他想法:

  • 使用更传统的分层方法,但是我忽略了这一点,因为它最终会变成:

  • abstract class AbstractDrawable

  • class Drawable extends AbstractDrawable
  • class TessellatedDrawable extends AbstractDrawable
  • class InstancedDrawable extends AbstractDrawable
  • class InstancedTessellatedDrawable extends AbstractDrawable

我也考虑过生成器模式,但是当您创建某个对象的大量唯一实例时,这是一种要使用的模式,这不是我们在这里所做的,也不是关于对象的构造函数。

因此,第一个也是最后一个问题是:我是在使用Java 8的功能还是滥用它?


答案 1

首先,如果它有效,并且它做了你想做的事情,并且将来没有坏掉的危险,那么说你滥用它是没有意义的。毕竟,它完成了工作,对吧?像默认方法和静态方法这样的功能被添加到具有特定目标的接口中,但是如果它们可以帮助您实现其他目标,那么要么是创造性地使用新功能,要么是粗略而肮脏的黑客攻击。:-)在某种程度上,这是一个品味问题。

考虑到这一点,我在API中寻找的,以及我在设计API时试图做的,是将API的客户端与API的实现者区分开来。API的典型客户端或用户从某个地方获取某种接口类型的引用,并在其上调用方法以使事情发生。实现器为接口中定义的方法提供实现,重写方法,并(如果是子类)调用超类方法。通常,客户端调用的方法与从子类调用的方法不同。

在我看来,这些概念在界面中混合在一起。当然,a 的客户端会执行一些操作,例如调用 or 方法。伟大。但是看看 的默认实现,它获取一些使用和方法的信息,然后使用这些方法来选择一个程序,并以特定的方式调用它的方法。将这些逻辑位封装在方法中是可以的,但是为了在默认方法中完成它,必须强制将 getters 插入公共接口。DrawableDrawabledrawdrawDepthPassdrawDepthPassisTessellatedisInstanced

当然,我对你的模型的看法可能是错的,但在我看来,这种逻辑更适合抽象的超类和子类关系。抽象超类实现了一些处理所有可绘制对象的逻辑,但它使用 or 等方法与特定的 Drawable 实现进行协商。在抽象超类中,这些将是子类需要实现的受保护方法。通过将此逻辑放入接口的默认方法中,所有这些方法都必须是公共的,这会使客户端接口变得混乱。其他看起来相似的方法是 、 和 。客户端是应该调用这些,还是它们在实现的逻辑内部?isTesselatedisInstancedgetDataModeisShadowReceiverisShadowCaster

这突出的是,尽管添加了默认方法和静态方法,但接口仍然面向客户端,而不是支持子类。原因如下:

  • 接口只有公共成员。
  • 抽象类可以具有受保护的方法,供子类重写或调用。
  • 抽象类可以具有私有方法来启用实现共享。
  • 抽象类可以具有字段(状态),这些字段(状态)可以受到保护以与子类共享状态,或者通常具有私有状态。
  • 抽象类可以具有在子类上强制实施某些行为策略的最终方法。

我注意到接口系列的另一个问题是,它使用默认方法相互覆盖的能力,以允许一些简单的mixins到实现类,如.你可以说并避免讨厌的方法覆盖,这有点整洁!问题是,这现在成为实现类类型的一部分。对于客户端来说,知道 a 也是一个 有用吗?或者这只是一个使内部实现更清洁的方案?如果是后者,则可能最好这些mixin接口,而不是公共接口(即包私有)。DrawableBoximplements TessellatedDrawableisTesselatedBoxTessellatedDrawableTessellatedDrawableInstancedDrawable

另请注意,此方法会使类型层次结构变得混乱,这可能会使代码在导航时更加混乱。通常,新类型是一个新概念,但是拥有仅定义返回布尔常量的默认方法的接口似乎重量级。

这是另一点。同样,我不知道你的模型,但这里混合的特征非常简单:它们只是布尔常量。如果有一个实现,比如说,一开始没有被实例化,后来可以成为实例化,它不能使用这些mixin接口。默认实现在它们能做什么方面确实受到了很大的限制。它们不能调用私有方法或检查实现类的字段,因此它们的使用非常有限。以这种方式使用接口几乎就像将它们用作标记接口一样,只需添加一个微小的就可以调用方法来获取特征,而不是使用 。除此之外似乎没有多大用处。Drawableinstanceof

界面中的静态方法似乎大多是合理的。它们是看似面向客户端的实用程序,它们提供了由公共实例方法提供的逻辑的合理聚合。Drawable

最后,关于模型的一些观点似乎很奇怪,尽管它们与默认和静态方法的使用没有直接关系。

它看起来像一个 has-a ,因为有实例方法 、 、 和 。然而,和类似的方法要求客户端传入两个程序,其中一个程序是根据布尔 getter 的结果选择的。我不清楚呼叫者应该在哪里选择正确的程序。DrawableProgramcompileProgramgetProgramdeletedrawDepthPass

方法和值也发生了类似的事情。似乎在可绘制对象列表中,必须根据每个可绘制对象的数据大小使用特定的偏移量来绘制它们。然而,这显然是最基本的方法,要求调用方传入偏移量。这似乎是推动呼叫者的一大责任。因此,也许偏移量的东西也确实属于实现。drawAlloffsetdraw

有几个方法可以采用可绘制对象列表并调用,然后或 。这不是必需的,因为上面有一个从 继承自 的方法。stream()forEach()forEachOrdered()ListforEachIterable

我认为探索如何使用这种新东西真是太好了。它足够新,以至于一种普遍接受的风格还没有出现。像这样的实验,以及这个讨论,有助于发展这种风格。另一方面,我们还需要小心,不要仅仅因为它们是新的和闪亮的而使用这些闪亮的新功能。


答案 2