【问题标题】:Usage of multiple inheritance in Java 8Java 8 中多重继承的使用
【发布时间】:2014-03-29 22:14:51
【问题描述】:

我是使用 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 重构为default 方法很简单,很简单,应该不会出错。

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

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

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

  6. 然后我发现我仍然很懒惰,不想为镶嵌和实例化的变体手动实现它。

  7. 所以我创建了TessellatedDrawableInstancedDrawable,它们分别提供default isTessellated()isInstanced()。在InstancedDrawable 中,我撤销了getInstancesCount()default 实现。

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

  • 正常Drawable:public class A implements Drawable
  • 镶嵌Drawable: public class A implements TessellatedDrawable
  • 实例化Drawable:public class A implements InstancedDrawable
  • 镶嵌和实例化Drawable: public class A implements InstancedDrawable, TessellatedDrawable

为了确保您可以正常编译和运行,implements InstancedDrawable, TessellatedDrawable 由 Java 8 完美处理,因为功能应该来自哪个接口没有任何歧义。

现在开始我自己的小 OOP 设计评估:

  • 每个Drawable 实际上都是Drawable,所以Collection&lt;Drawable&gt; 不会中断。
  • 可以对所有 TessellatedDrawable 和/或 InstancedDrawable 进行分组,与具体实现方式无关。

我的其他想法:

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

  • abstract class AbstractDrawable

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

我也考虑过 Builder 模式,但是当您创建某个对象的许多唯一实例时要使用这种模式,这不是我们在这里所做的,这也不是关于对象。

所以第一个也是最后一个问题是:我是使用 Java 8 的一个特性还是滥用它?

【问题讨论】:

  • 哇,问题透彻,谢谢
  • 这是一个很好的问题,但对于 Code Review 来说可能比 Stack Overflow 更好,因为这是一个风格判断而不是正确性的问题。我个人的直觉仍然是避免多重继承,除非确实有必要,因为看到它在其他一些语言中造成混乱......而且我还不喜欢将自己锁定在 Java 8 中的想法。我在您的设计中没有发现任何明显错误的地方,但我会犹豫是否走这条路。
  • 这是一个有趣的问题,因为它正在探索一种新语言特征的维度。但是,这是一个非常冗长的问题(对不起)。是否有可能将其缩减为最基本的必需品?此外,您似乎在这里有两个问题:1)这是对语言功能的使用/滥用吗? 2)你将如何处理这个设计空间?最好将这些问题分解成单独的、更容易回答的问题!
  • @DavidBullock 我认为阅读整个故事至少与问题本身一样重要。

标签: java oop inheritance multiple-inheritance java-8


【解决方案1】:

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

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

在我看来,这些概念在Drawable 界面中混杂在一起。当然,Drawable 的客户会做一些事情,比如在他们身上调用 drawdrawDepthPass 方法。伟大的。但是查看drawDepthPass 的默认实现,它使用isTessellatedisInstanced 方法获取一些信息,然后使用它们选择一个程序并以特定方式调用它的方法。将这些逻辑位封装在一个方法中是可以的,但为了在 default 方法中完成,必须强制 getter 进入公共接口。

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

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

  • 接口只有公共成员。
  • 抽象类可以具有供子类覆盖或调用的受保护方法。
  • 抽象类可以有私有方法来实现实现共享。
  • 抽象类可以具有字段(状态),这些字段可以受到保护以与子类共享状态,或者通常是私有的。
  • 抽象类可以有 final 方法对子类强制执行某些行为策略。

我注意到Drawable 接口系列的另一个问题是,它使用默认方法相互覆盖的能力,以允许一些简单的混入实现类,如Box。您可以只说 implements TessellatedDrawable 并避免讨厌的覆盖 isTesselated 方法,这有点简洁!问题是这现在成为实现类类型的一部分。客户知道Box 也是TessellatedDrawable 是否有用?或者这只是一个使内部实现更清洁的方案?如果是后者,最好不要像 TessellatedDrawableInstancedDrawable 这样的 mixin 接口是公共接口(即包私有)。

另请注意,这种方法会使类型层次结构混乱,这会使代码更难以导航。通常一个新类型是一个新概念,但拥有只定义返回布尔常量的默认方法的接口似乎是重量级的。

这方面的另一点。同样,我不知道您的模型,但这里混合的特征非常简单:它们只是布尔常量。如果曾经有一个Drawable 实现,比如说,一开始没有被实例化,后来可以被实例化,它就不能使用这些mixin 接口。默认实现在他们能做的事情上真的很受限制。它们不能调用私有方法或检查实现类的字段,因此它们的使用非常有限。以这种方式使用接口几乎就像将它们用作标记接口一样,只是能够调用一个方法来获取特性,而不是使用instanceof。除此之外似乎没什么用处。

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

最后,关于模型有几点看起来很奇怪,尽管它们与默认和静态方法的使用没有直接关系。

这似乎是一个Drawable 有一个Program,因为有实例方法compileProgramgetProgramdelete。然而drawDepthPass 和类似的方法需要客户端传入两个程序,其中一个是根据布尔 getter 的结果选择的。我不清楚调用者应该在哪里选择正确的程序。

drawAll 方法和 offset 值也发生了类似的情况。似乎在一个 Drawable 列表中,必须根据每个 Drawable 的数据大小使用特定的偏移量来绘制它们。然而,显然最基本的方法draw 要求调用者传入一个偏移量。这似乎是推给调用者的一项重大责任。所以也许偏移的东西真的也属于实现。

有几种方法可以获取可绘制列表并调用stream(),然后调用forEach()forEachOrdered()。这不是必需的,因为List 上有一个forEach 方法,继承自Iterable

我认为探索如何使用这些新东西非常棒。它足够新,尚未出现一种普遍接受的风格。像这样的实验,以及这样的讨论,有助于发展这种风格。另一方面,我们也需要注意不要仅仅因为它们是新的和闪亮的而使用这些闪亮的新功能。

【讨论】:

  • 不错的答案!接口应该只暴露真正的功能这一事实确实是一个很好的评价,也许我的设计在这方面失败了......
  • 不错的答案!这将是我们在Code Review 上给出的答案,如果您喜欢写这个答案,请随时加入我们:)
  • 我会重新设计其中的一些,然后我想它已经为 CR 做好了准备,因为我所做的似乎是允许的,如果情况符合要求的话。
猜你喜欢
  • 2014-12-03
  • 1970-01-01
  • 2014-03-16
  • 2016-08-24
  • 2020-04-14
  • 2015-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多