Подтвердить что ты не робот

Использование множественного наследования в 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();
    }
}

Сначала выполните шаги по тому, как я пришел к этому коду:

  • Я начал с интерфейса Drawable, и каждая реализация имела свои собственные методы drawDepthPass, draw и delete.

  • Рефакторинг delete к методу default был простым, тривиальным и не должен быть неправильным.

  • Однако, чтобы иметь возможность рефакторирования drawDepthPass и draw, мне нужен был доступ к тому, был ли тег Drawable тесселирован и/или установлен, поэтому я добавил публикацию (не по умолчанию) методы isTessellated(), isInstanced() и getInstancesCount().

  • Затем я понял, что это будет немного громоздко, поскольку мы, программисты, ленивы, чтобы реализовать их в каждом Drawable.

  • Как следствие, я добавил методы default в Drawable, давая поведение самого базового Drawable.

  • Затем я подумал, что я все еще ленив и не хочу вручную реализовывать его для тесселлированных и инстантивных вариантов.

  • Итак, я создал TessellatedDrawable и InstancedDrawable, которые обеспечивают default isTessellated() и isInstanced() соответственно. И в InstancedDrawable я отменил реализацию default getInstancesCount().

В результате я могу иметь следующее:

  • Обычный Drawable: public class A implements Drawable
  • Тесселированный Drawable: public class A implements TessellatedDrawable
  • Экранный Drawable: public class A implements InstancedDrawable
  • Tessellated и instanced Drawable: public class A implements InstancedDrawable, TessellatedDrawable.

Просто, чтобы убедиться, что все это компилируется и работает отлично, implements InstancedDrawable, TessellatedDrawable отлично обрабатывается Java 8, поскольку в какой-то степени не существует двусмысленности, с которой интерфейс должен функционировать.

Теперь на мою собственную оценку дизайна ООП:

  • Каждый Drawable на самом деле является Drawable, поэтому Collection<Drawable> не сломается.
  • Можно группировать все TessellatedDrawable и/или InstancedDrawable, не имеющие отношения к тому, как именно оно реализуется.

Другие мысли, которые у меня были:

  • Используйте более обычный, многоуровневый подход, однако я проигнорировал это, так как это закончилось бы:

  • abstract class AbstractDrawable

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

Я также рассмотрел шаблон Builder, однако это шаблон, который будет использоваться, когда вы создаете множество уникальных экземпляров определенного объекта, и это не то, что мы здесь делаем, и это не о конструкторе объект.

Итак, первый и последний вопрос: я использую функцию Java 8 или неправильно ее использую?

4b9b3361

Ответ 1

Во-первых, если он работает, и он делает то, что вы хотите сделать, и нет никакой опасности чего-то взломать в будущем, не имеет смысла говорить, что вы неправильно его используете. В конце концов, он выполнил свою работу, не так ли? Такие функции, как методы по умолчанию и статические методы, были добавлены к интерфейсам с конкретными целями, но если они помогут вам достичь других целей, либо это творческое использование новой функции, либо грубый и грязный взломать.:-) В определенной степени это вопрос вкуса.

Учитывая эту перспективу, то, что я ищу в API, и что я пытаюсь сделать при разработке API, заключается в том, чтобы отличать клиентов API от разработчиков API. Типичный клиент или пользователь API получает ссылку на некоторый тип интерфейса откуда-то и называет методы на нем, чтобы заставить вещи произойти. Разработчик предоставляет реализации для методов, определенных в интерфейсах, переопределяет методы и (если подклассы) вызывает методы суперкласса. Часто методы, предназначенные для вызова клиентами, отличаются от методов, предназначенных для вызова из подклассов.

Мне кажется, что эти понятия смешиваются в интерфейсе Drawable. Конечно, клиенты Drawable будут делать такие вещи, как называть методы draw или drawDepthPass на них. Отлично. Но, глядя на реализацию по умолчанию drawDepthPass, он получает некоторую информацию с использованием методов isTessellated и isInstanced, а затем использует их для выбора программы и методов вызова на ней определенным образом. Это прекрасно для того, чтобы эти биты логики были инкапсулированы внутри метода, но для того, чтобы это было сделано по методу по умолчанию, геттеры должны быть принудительно введены в открытый интерфейс.

Возможно, я ошибаюсь в вашей модели, но мне кажется, что такая логика более подходит для абстрактного отношения суперкласса и подкласса. Абстрактный суперкласс выполняет некоторую логику, которая обрабатывает все Drawables, но она ведет переговоры с конкретными реализациями Drawable с помощью таких методов, как isTesselated или isInstanced. В абстрактном суперклассе это будут защищенные методы, которые должны выполняться подклассами. Помещая эту логику в методы по умолчанию для интерфейса, все они должны быть общедоступными, что загромождает клиентский интерфейс. Другими методами, которые кажутся похожими, являются getDataMode, isShadowReceiver и isShadowCaster. Ожидаются ли клиенты называть их или они логически являются внутренними для реализации?

Это подчеркивает, что, несмотря на добавление методов по умолчанию и статических методов, интерфейсы по-прежнему ориентированы на клиентов и меньше на поддержку подклассов. Причины таковы:

  • Интерфейсы имеют только открытые элементы.
  • Абстрактные классы могут иметь защищенные методы для переопределения или вызова подклассов.
  • Абстрактные классы могут иметь частные методы, позволяющие совместное использование.
  • Абстрактные классы могут иметь поля (состояние), которые могут быть защищены для совместного использования состояний с подклассами или обычно приватными в противном случае.
  • Абстрактные классы могут иметь окончательные методы, которые обеспечивают соблюдение определенных правил поведения в подклассах.

Еще одна проблема, которую я отмечаю с семейством интерфейсов Drawable, заключается в том, что она использует способность методов по умолчанию переопределять друг друга, чтобы позволить некоторым простым миксинсам классам реализации, таким как Box. Это довольно аккуратно, что вы можете просто сказать implements TessellatedDrawable и избежать надоедливого переопределения метода isTesselated! Проблема в том, что теперь это становится частью класса класса реализации. Полезно ли клиенту знать, что a Box также является TessellatedDrawable? Или это просто схема для улучшения внутренней реализации? Если это последний, может быть предпочтительнее, чтобы эти интерфейсы mixin, такие как TessellatedDrawable и InstancedDrawable, не были общедоступными интерфейсами (т.е. Частным пакетом).

Обратите внимание также, что этот подход загромождает иерархию типов, что может сделать код более запутанным для навигации. Обычно новый тип - это новая концепция, но кажется, что супертяжелый имеет интерфейсы, которые просто определяют методы по умолчанию, возвращающие логические константы.

Еще одна точка в этом ключе. Опять же, я не знаю вашу модель, но характеристики, смешанные здесь, очень просты: они всего лишь логические константы. Если когда-либо реализована реализация Drawable, которая, скажем, начинается без инстанса, а позже может стать instanced, она не может использовать эти интерфейсы mixin. Реализации по умолчанию действительно ограничены в том, что они могут делать. Они не могут вызывать частные методы или проверять поля класса реализации, поэтому их использование довольно ограничено. Использование интерфейсов таким образом почти похоже на использование их в качестве интерфейсов маркеров, с крошечным добавлением возможности вызова метода для получения характеристики вместо использования instanceof. Похоже, что это не так много.

Статические методы в интерфейсе Drawable кажутся в основном разумными. Это утилиты, которые кажутся ориентированными на клиента, и обеспечивают разумную совокупность логики, предоставляемую методами публичных экземпляров.

Наконец, есть несколько моментов в модели, которые кажутся нечетными, хотя они не имеют прямого отношения к использованию стандартных и статических методов.

Кажется, что Drawable has-a Program, так как существуют методы экземпляра compileProgram, getProgram и delete. Тем не менее, drawDepthPass и подобные методы требуют, чтобы клиент передавал две программы, одна из которых выбирается в зависимости от результата булевых геттеров. Мне непонятно, где вызывающий должен выбрать правильные Программы.

Нечто похожее происходит с методами drawAll и значением offset. Похоже, что в списке Drawables они должны быть нарисованы с использованием отдельных смещений на основе каждого размера данных Drawable. Но, по-видимому, самый фундаментальный метод draw требует, чтобы вызывающий передал смещение. Это похоже на большую ответственность за то, чтобы нажать на вызывающего абонента. Поэтому, возможно, материал смещения действительно также входит в реализацию.

Есть несколько методов, которые берут список drawables и вызывают stream(), а затем forEach() или forEachOrdered(). Это необязательно, так как List имеет на нем метод forEach, унаследованный от Iterable.

Я думаю, что здорово исследовать, как можно использовать этот новый материал. Это достаточно новое, что общепринятый стиль еще не появился. Эксперименты, подобные этому, и это обсуждение, помогают развить этот стиль. С другой стороны, мы также должны быть осторожны, чтобы не использовать эти блестящие новые функции только потому, что они новые и блестящие.