ventajas tipos que puede mĂșltiple multiple herencia hacer ejercicios desventajas concepto java oop inheritance multiple-inheritance java-8

que - tipos de herencia en java



Uso de herencia mĂșltiple en Java 8 (1)

Primero, si funciona, y hace lo que usted quiere hacer, y no hay peligro de que se rompa algo en el futuro, no tiene sentido decir que lo está haciendo mal uso. Después de todo, hizo el trabajo, ¿verdad? Las características como los métodos predeterminados y los métodos estáticos se agregaron a las interfaces con objetivos particulares en mente, pero si lo ayudan a lograr otros objetivos, es el uso creativo de una nueva característica o un truco brutal y sucio. :-) Hasta cierto punto es cuestión de gustos.

Con esa perspectiva en mente, lo que busco en las API, y lo que trato de hacer al diseñar las API, es distinguir a los clientes de una API de los implementadores de una API. Un cliente o usuario típico de una API obtiene una referencia de algún tipo de interfaz de algún lugar y llama a los métodos para hacer que sucedan las cosas. Un implementador proporciona implementaciones para los métodos definidos en interfaces, reemplaza los métodos y (si se hace una subclase) llama a los métodos de superclase. A menudo, los métodos destinados a ser llamados por los clientes son diferentes de aquellos destinados a ser llamados desde subclases.

Me parece que estos conceptos se están mezclando en la interfaz Drawable . Ciertamente, los clientes de un Drawable harán cosas como llamar los métodos draw o drawDepthPass en ellos. Estupendo. Pero al observar la implementación predeterminada de drawDepthPass , obtiene cierta información utilizando los métodos isTessellated y isInstanced , y luego los utiliza para elegir un programa y llamar a los métodos de una manera particular. Está bien que estos bits de lógica se encapsulen dentro de un método, pero para que se realice de forma predeterminada , los captadores deben ser forzados a ingresar en la interfaz pública.

Puede que esté equivocado acerca de su modelo, por supuesto, pero me parece que este tipo de lógica es más adecuada para una relación abstracta de superclase y subclase. La superclase abstracta implementa cierta lógica que maneja todos los Drawable, pero negocia con las implementaciones Drawable particulares con métodos como está isTesselated con el isTesselated o isInstanced . En una superclase abstracta, estos serían métodos protegidos que las subclases deben implementar. Al poner esta lógica en los métodos predeterminados de una interfaz, todos estos deben ser públicos, lo que desordena la interfaz del cliente. Los otros métodos que parecen similares son getDataMode , isShadowReceiver y isShadowCaster . ¿Se espera que los clientes llamen a estos, o son lógicamente internos a la implementación?

Lo que se destaca es que, a pesar de la adición de métodos predeterminados y métodos estáticos, las interfaces aún están orientadas hacia los clientes y menos hacia las subclases de soporte. Las razones son las siguientes:

  • Las interfaces solo tienen miembros públicos.
  • Las clases abstractas pueden tener métodos protegidos para que las subclases anulen o llamen.
  • Las clases abstractas pueden tener métodos privados para permitir la implementación compartida.
  • Las clases abstractas pueden tener campos (estado) que pueden protegerse para compartir el estado con subclases, o por lo general son privadas.
  • Las clases abstractas pueden tener métodos finales que imponen ciertas políticas de comportamiento en las subclases.

Otro problema que observo con la familia de interfaces Drawable es que utiliza la capacidad de los métodos predeterminados para anularse mutuamente para permitir algunos mixins simples a las clases de implementación como Box . ¡Es isTesselated bueno que solo puedas decir implements TessellatedDrawable y evitar la isTesselated anulación del método isTesselated ! El problema es que esto ahora se convierte en parte del tipo de clase de implementación. ¿Es útil para el cliente saber que una Box también es un TessellatedDrawable ? ¿O es solo un esquema para hacer que la implementación interna sea más limpia? Si es lo último, podría ser preferible que estas interfaces mixtas como TessellatedDrawable e InstancedDrawable no sean interfaces públicas (es decir, paquete privado).

Tenga en cuenta también que este enfoque desordena la jerarquía de tipos, lo que puede hacer que el código sea más confuso para navegar. Normalmente, un nuevo tipo es un concepto nuevo, pero parece pesado tener interfaces que simplemente definen métodos predeterminados que devuelven constantes booleanas.

Un punto más en este sentido. Nuevamente, no conozco tu modelo, pero las características que se mezclan aquí son muy simples: son solo constantes booleanas. Si alguna vez hay una implementación Drawable que, por ejemplo, comienza sin ser instanciada y luego puede ser instanciada, no puede usar estas interfaces de mezcla. Las implementaciones por defecto son realmente bastante restringidas en lo que pueden hacer. No pueden llamar a métodos privados ni inspeccionar campos de una clase de implementación, por lo que su uso es bastante limitado. Usar interfaces de esta manera es casi como usarlas como interfaces de marcadores, con una pequeña adición de poder llamar a un método para obtener la característica, en lugar de usar instanceof . No parece haber mucho uso más allá de esto.

Los métodos estáticos en la interfaz Drawable parecen en su mayoría razonables. Son utilidades que parecen orientadas al cliente, y proporcionan agregaciones razonables de la lógica proporcionada por los métodos de instancia pública.

Finalmente, hay algunos puntos sobre el modelo que parecen extraños, aunque no están directamente relacionados con el uso de métodos predeterminados y estáticos.

Parece un Drawable has-a Program , ya que hay métodos de instancia compileProgram , getProgram y delete . Sin embargo, el drawDepthPass y otros métodos similares requieren que el cliente pase dos programas, uno de los cuales se selecciona según el resultado de los captadores booleanos. No me queda claro dónde se supone que la persona que llama elige los Programas correctos.

Algo similar está sucediendo con los métodos drawAll y el valor de offset . Parece que en una lista de Drawables, tienen que ser dibujados usando compensaciones particulares basadas en el tamaño de datos de cada Drawable. Sin embargo, lo que aparentemente es el método más fundamental, draw , requiere que la persona que llama pase una compensación. Esto parece ser una gran responsabilidad empujar a la persona que llama. Así que tal vez las cosas de compensación realmente pertenecen también a la implementación.

Hay un par de métodos que toman una Lista de elementos dibujables y una stream() llamadas stream() y luego forEach() o forEachOrdered() . Esto no es necesario, ya que List tiene un método forEach , heredado de Iterable .

Creo que es genial explorar cómo se pueden usar estas cosas nuevas. Es lo suficientemente nuevo como para que todavía no haya surgido un estilo comúnmente aceptado. Experimentos como este, y esta discusión, ayudan a desarrollar ese estilo. Por otro lado, también debemos tener cuidado de no usar estas nuevas características brillantes solo porque son nuevas y brillantes.

¿Estoy usando una característica de Java 8 o la estoy usando mal ?

Consulte el código y la explicación a continuación para saber por qué se eligió para ser así.

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(); } }

Primero los pasos sobre cómo llegué a este código:

  1. Comencé con la interfaz Drawable y cada implementación tenía sus propios drawDepthPass , draw y delete .

  2. Refactorizar delete a un método default fue fácil, trivial y no debería estar equivocado.

  3. Sin embargo, para poder refactorizar drawDepthPass y draw , necesitaba acceso para saber si un Drawable estaba tesselated y / o instanciado, así que agregué los métodos públicos ( no predeterminados ) isTessellated() , isInstanced() y getInstancesCount() .

  4. Luego me di cuenta de que sería un poco engorroso, ya que los programadores somos perezosos, implementarlos en cada Drawable .

  5. Como consecuencia, agregué los métodos default a Drawable , dando el comportamiento del Drawable más básico.

  6. Luego me di cuenta de que todavía soy perezoso y no quiero implementarlo manualmente para las variantes instanciadas y en mosaico eithere.

  7. Así que creé TessellatedDrawable e InstancedDrawable que proporcionan isTessellated() default isTessellated() e isInstanced() respectivamente. Y en InstancedDrawable revocé la implementación default de getInstancesCount() .

Como resultado puedo tener lo siguiente:

  • Drawable normal: public class A implements Drawable
  • Drawable Tessellated: public class A implements TessellatedDrawable
  • Drawable instancias: public class A implements InstancedDrawable
  • Drawable mosaico e instanciable: public class A implements InstancedDrawable, TessellatedDrawable .

Solo para asegurarse de que todo esto se compile y funcione bien, los implements InstancedDrawable, TessellatedDrawable se manejan perfectamente con Java 8, ya que no hay ambigüedad alguna desde la interfaz de la cual debe provenir la funcionalidad.

Ahora en mi propia pequeña evaluación de diseño OOP:

  • Cada Drawable es de hecho un Drawable , por lo que la Collection<Drawable> no se romperá.
  • Es posible agrupar todos los TessellatedDrawable y / o InstancedDrawable , independientemente de cómo se implementa exactamente.

Otros pensamientos que tuve:

  • Utilice un enfoque en capas más convencional, sin embargo, no hice caso de eso ya que terminaría en:

  • abstract class AbstractDrawable

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

También he considerado un patrón de creación, sin embargo, es un patrón que se utiliza cuando se crean muchas instancias únicas de un determinado objeto, y eso no es lo que estamos haciendo aquí, ni se trata del constructor del objeto.

Entonces, la primera y última pregunta fue: ¿Estoy usando una función de Java 8 o estoy haciendo un mal uso de ella?