在Java 8中使用多重inheritance
我使用 Java 8的function还是滥用它?
请参阅下面的代码和说明,了解为什么选择这样的代码。
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 drawables) { return drawables.stream() .mapToInt(Drawable::getDataSize) .sum(); } public static FloatBuffer putAllData(final List 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 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 drawables) { int offset = 0; for (Drawable drawable : drawables) { drawable.draw(offset); offset += drawable.getDataSize(); } } public static void deleteAll(final List 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 { // 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(); } // 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(); } }
首先介绍我如何使用此代码:
-
我从
Drawable
接口开始,每个实现都有自己的drawDepthPass
,draw
和delete
方法。 -
delete
重构为default
方法很容易,很简单,不应该是错误的。 -
但是为了能够重构
drawDepthPass
并draw
我需要访问Drawable
是否被tesselated和/或实例化,所以我添加了public( 非默认 )方法isTessellated()
,isInstanced()
和getInstancesCount()
。 -
然后我发现它会稍微麻烦,因为我们的程序员很懒,要在每个
Drawable
实现它们。 -
因此,我将
default
方法添加到Drawable
,给出了最基本的Drawable
的行为。 -
然后我想我仍然很懒,并且不想为tessellated和instanced变体手动实现它。
-
所以我创建了
TessellatedDrawable
和InstancedDrawable
,分别提供default
isTessellated()
和isInstanced()
。 在InstancedDrawable
我撤消了getInstancesCount()
的default
实现。
因此,我可以拥有以下内容:
- Normal
Drawable
:public class A implements Drawable
- Tessellated
Drawable
:public class A implements TessellatedDrawable
- Instanced
Drawable
:public class A implements InstancedDrawable
- Tessellated和instanced
Drawable
:public class A implements InstancedDrawable, TessellatedDrawable
。
为了确保您,这一切都可以编译并运行良好, implements InstancedDrawable, TessellatedDrawable
由Java 8完美处理,因为function应该来自哪个接口的任何地方都没有任何歧义。
现在进行我自己的小OOP设计评估:
- 每个
Drawable
实际上都是Drawable
,因此Collection
不会破坏。 - 可以对所有
TessellatedDrawable
和/或InstancedDrawable
进行分组,与其实现方式无关。
我有的其他想法:
-
使用更传统的分层方法,但我忽略了它最终会:
-
abstract class AbstractDrawable
-
class Drawable extends AbstractDrawable
-
class TessellatedDrawable extends AbstractDrawable
-
class InstancedDrawable extends AbstractDrawable
-
class InstancedTessellatedDrawable extends AbstractDrawable
我也考虑过一个Builder Pattern,但是当你创建一个特定对象的很多独特实例时,这是一个模式,这不是我们在这里做的,也不是关于对象的构造函数。
所以第一个也是最后一个问题是:我使用 Java 8的function还是滥用它?
首先,如果它有效,并且它做你想做的事情,并且将来没有任何破坏的危险,那么说你滥用它是没有意义的。 毕竟,它完成了工作,对吧? 默认方法和静态方法等function被添加到具有特定目标的接口中,但是如果它们帮助您实现其他目标,则可以是创造性地使用新function,也可以是粗暴和肮脏的黑客攻击。 :-)在某种程度上,这是一个品味问题。
考虑到这一点,我在API中寻找的内容以及我在设计API时尝试做的是区分API的客户端和API的实现者。 API的典型客户端或用户从某个地方获取某种接口类型的引用,并在其上调用方法以使事情发生。 实现者提供接口中定义的方法的实现,覆盖方法和(如果子类化)调用超类方法。 通常,客户端调用的方法与从子类调用的方法不同。
在我看来,这些概念在Drawable
界面中混合使用。 当然, Drawable
客户端会执行调用draw
或drawDepthPass
方法的操作。 大。 但是看一下drawDepthPass
的默认实现,它使用isTessellated
和isInstanced
方法获取一些信息,然后使用它们选择一个程序并以特定方式调用它。 将这些逻辑位封装在一个方法中是很好的,但为了在默认方法中完成,必须将getter强制进入公共接口。
当然,我对你的模型可能是错的,但在我看来,这种逻辑更适合抽象的超类和子类关系。 抽象超类实现了一些处理所有Drawable的逻辑,但它使用isTesselated
或isInstanced
等方法与特定的Drawable实现进行协商。 在抽象超类中,这些是受保护的方法,需要子类来实现。 通过将此逻辑放入接口的默认方法中,所有这些都必须是公共的,这会使客户端接口变得混乱。 其他类似的方法是getDataMode
, isShadowReceiver
和isShadowCaster
。 是否期望客户打电话给这些客户,或者他们是否在逻辑上内部实施?
这突出表明,尽管添加了默认方法和静态方法,但接口仍然面向客户端,而不是支持子类。 原因如下:
- 接口只有公共成员。
- 抽象类可以有子类的覆盖或调用的受保护方法。
- 抽象类可以使用私有方法来实现实现共享。
- 抽象类可以具有可以被保护以与子类共享状态的字段(状态),或者通常是私有的。
- 抽象类可以具有在子类上强制执行某些行为策略的最终方法。
我在Drawable
接口系列中注意到的另一个问题是它使用默认方法的能力来相互覆盖,以允许一些简单的mixins到像Box
这样的实现类。 你可以说implements TessellatedDrawable
并避免令人讨厌的覆盖isTesselated
方法,这isTesselated
! 问题是,这现在成为实现类的类型的一部分。 对于客户来说,知道Box
也是TessellatedDrawable
是否有用? 或者这只是一个使内部实施更清洁的方案? 如果是后者,那么像TessellatedDrawable
和InstancedDrawable
这样的mixin接口可能不是公共接口(即包私有)。
另请注意,此方法会混淆类型层次结构,这会使代码更难以导航。 通常一个新类型是一个新概念,但它似乎是重量级的接口只定义返回布尔常量的默认方法。
这方面的另一点。 同样,我不知道你的模型,但这里混合的特征非常简单:它们只是布尔常量。 如果有一个Drawable
实现,比如说,开始没有实例化,以后可以变为实例,它就不能使用这些mixin接口。 默认实现在他们能做的事情上非常受限制。 他们不能调用私有方法或检查实现类的字段,因此它们的使用非常有限。 以这种方式使用接口几乎就像使用它们作为标记接口一样,只需要调用方法来获取特性,而不是使用instanceof
。 除此之外似乎没有多大用处。
Drawable
界面中的静态方法似乎最合理。 它们是看起来面向客户端的实用程序,它们提供公共实例方法提供的合理逻辑集合。
最后,关于模型有一些看似奇怪的观点,尽管它们与默认和静态方法的使用没有直接关系。
看起来像Drawable
有一个Program
,因为有实例方法compileProgram
, getProgram
和delete
。 然而drawDepthPass
和类似方法要求客户端传入两个程序,其中一个程序根据布尔getter的结果进行选择。 我不清楚调用者应该选择正确的程序。
使用drawAll
方法和offset
值进行类似的drawAll
。 看起来像是在Drawables列表中,它们必须使用基于每个Drawable的数据大小的特定偏移来绘制。 然而,显然最基本的方法是draw
,需要调用者传递偏移量。 这似乎是推动呼叫者的重要责任。 所以也许偏移的东西也真的属于实现。
有几种方法可以获取drawables和调用stream()
,然后是forEach()
或forEachOrdered()
。 这不是必需的,因为List
有一个forEach
方法,inheritance自Iterable
。
我认为探索如何使用这些新东西真是太棒了。 它足够新,以至于尚未出现一种普遍接受的风格。 像这样的实验,以及这个讨论,有助于发展这种风格。 另一方面,我们还需要注意不要使用这些shiny的新function,因为它们是新的和有光泽的。