Отдельный интерфейс для методов мутации

11

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

У меня есть интерфейс, Fooопределенный как

public interface Foo {

    int getX();

    int getY();

    int getZ();
}

И реализация как

public final class DefaultFoo implements Foo {

    public DefaultFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getZ() {
        return z;
    }

    private final int x;
    private final int y;
    private final int z;
}

У меня также есть интерфейс, MutableFooкоторый обеспечивает соответствующие мутаторы

/**
 * This class extends Foo, because a 'write-only' instance should not
 * be possible and a bit counter-intuitive.
 */
public interface MutableFoo extends Foo {

    void setX(int newX);

    void setY(int newY);

    void setZ(int newZ);
}

Есть несколько реализаций, MutableFooкоторые могут существовать (я еще не реализовал их). Один из них является

public final class DefaultMutableFoo implements MutableFoo {

    /**
     * A DefaultMutableFoo is not conceptually constructed 
     * without all values being set.
     */
    public DefaultMutableFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public void setX(int newX) {
        this.x = newX;
    }

    public int getY() {
        return y;
    }

    public void setY(int newY) {
        this.y = newY;
    }

    public int getZ() {
        return z;
    }

    public void setZ(int newZ) {
        this.z = newZ;
    }

    private int x;
    private int y;
    private int z;
}

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

Основной вариант использования, который у меня есть, - это интерфейс, StatSetкоторый представляет определенные детали боя для игры (точки попадания, атака, защита). Тем не менее, «эффективные» характеристики, или фактические характеристики, являются результатом базовых характеристик, которые никогда не могут быть изменены, и обученных характеристик, которые могут быть увеличены. Эти два связаны

/**
 * The EffectiveStats can never be modified independently of either the baseStats
 * or trained stats. As such, this StatSet must never provide mutators.
 */
public StatSet calculateEffectiveStats() {
    int effectiveHitpoints =
        baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    int effectiveAttack = 
        baseStats.getAttack() + (trainedStats.getAttack() / 4);
    int effectiveDefense = 
        baseStats.getDefense() + (trainedStats.getDefense() / 4);

    return StatSetFactory.createImmutableStatSet(effectiveHitpoints, effectiveAttack, effectiveDefense);
}

обученная статистика увеличивается после каждой битвы, как

public void grantExperience() {
    int hitpointsReward = 0;
    int attackReward = 0;
    int defenseReward = 0;

    final StatSet enemyStats = enemy.getEffectiveStats();
    final StatSet currentStats = player.getEffectiveStats();
    if (enemyStats.getHitpoints() >= currentStats.getHitpoints()) {
        hitpointsReward++;
    }
    if (enemyStats.getAttack() >= currentStats.getAttack()) {
        attackReward++;
    }
    if (enemyStats.getDefense() >= currentStats.getDefense()) {
        defenseReward++;
    }

    final MutableStatSet trainedStats = player.getTrainedStats();
    trainedStats.increaseHitpoints(hitpointsReward);
    trainedStats.increaseAttack(attackReward);
    trainedStats.increaseDefense(defenseReward);
}

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

Теперь для моих вопросов:

  1. Есть ли название для разделения интерфейсов аксессорами и мутаторами на отдельные интерфейсы?
  2. Является ли их разделение таким образом «правильным» подходом, если они одинаково вероятны для использования, или есть другой, более приемлемый шаблон, который я должен использовать вместо этого (например, Foo foo = FooFactory.createImmutableFoo();который может возвращаться DefaultFooили DefaultMutableFooскрыт, потому что createImmutableFooвозвращается Foo)?
  3. Есть ли какие-либо непосредственные предсказуемые недостатки использования этого шаблона, за исключением усложнения иерархии интерфейса?

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

Создание нового класса для EffectiveStatSetне имеет особого смысла, поскольку мы никоим образом не расширяем функциональность. Мы могли бы изменить реализацию и составить EffectiveStatSetкомпозицию из двух разных StatSets, но я чувствую, что это не правильное решение;

public class EffectiveStatSet implements StatSet {

    public EffectiveStatSet(StatSet baseStats, StatSet trainedStats) {
        // ...
    }

    public int getHitpoints() {
        return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    }
}
Zymus
источник
1
Я думаю, проблема в том, что ваш объект изменчив, даже если вы обращаетесь к нему через мой «неизменяемый» интерфейс. Лучше иметь изменяемый объект с Player.TrainStats ()
Эван
@gnat у тебя есть какие-нибудь ссылки, где я могу узнать об этом больше? Основываясь на отредактированном вопросе, я не уверен, как или где я мог бы применить это.
Зимус
@gnat: ваши ссылки (и ссылки второй и третьей степени) очень полезны, но броские фразы мудрости мало помогают. Это только привлекает недопонимание и презрение.
Руонг

Ответы:

6

Мне кажется, у вас есть решение в поисках проблемы.

Есть ли название для разделения интерфейсов аксессорами и мутаторами на отдельные интерфейсы?

Это может быть немного провоцирующим, но на самом деле я бы назвал это «чрезмерно усложняющими вещами» или «слишком усложняющими вещами». Предлагая изменчивый и неизменный вариант одного и того же класса, вы предлагаете два функционально эквивалентных решения для одной и той же проблемы, которые отличаются только нефункциональными аспектами, такими как поведение производительности, API и защита от побочных эффектов. Я полагаю, это потому, что вы боитесь принять решение, какой из них выбрать, или потому, что вы пытаетесь реализовать функцию const в C ++ в C #. Я предполагаю, что в 99% случаев не будет большой разницы, если пользователь выбирает изменяемый или неизменяемый вариант, он может решить свои проблемы с тем или другим. Таким образом "вероятность использования класса"

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

Делает ли их таким образом «правильный» подход, если они одинаково вероятны для использования, или существует другая, более приемлемая модель

«Более приемлемый шаблон» называется KISS - будь проще и глупее. Примите решение за или против изменчивости для определенного класса / интерфейса в вашей библиотеке. Например, если ваш «StatSet» имеет десяток или более атрибутов, и они в основном изменяются индивидуально, я бы предпочел изменяемый вариант и просто не изменять базовую статистику, где их не следует изменять. Для чего-то вроде Fooкласса с атрибутами X, Y, Z (трехмерный вектор) я бы, вероятно, предпочел неизменный вариант.

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

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

Док Браун
источник
1
Я думаю, что вы имеете в виду «ПОЦЕЛУЙ
будь проще
2

Есть ли название для разделения интерфейсов аксессорами и мутаторами на отдельные интерфейсы?

Для этого может быть название, если это разделение полезно и дает выгоду, которую я не вижу . Если разделение не дает выгоды, два других вопроса не имеют смысла.

Можете ли вы рассказать нам о каком-либо случае использования в бизнесе, когда два отдельных интерфейса дают нам преимущество, или это вопрос академического характера (YAGNI)?

Я могу придумать магазин с изменяемой корзиной (вы можете добавить больше товаров), которая может стать заказом, в котором товары не могут быть изменены покупателем. Статус заказа все еще изменчив.

Реализации, которые я видел, не разделяют интерфейсы

  • нет интерфейса ReadOnlyList в Java

Версия ReadOnly использует «WritableInterface» и выдает исключение, если используется метод записи

k3b
источник
Я изменил ОП, чтобы объяснить основной вариант использования.
Зимус
2
«Нет интерфейса ReadOnlyList в java» - честно говоря, так и должно быть. Если у вас есть фрагмент кода, который принимает List <T> в качестве параметра, вы не можете легко определить, будет ли он работать с неизменяемым списком. Код, возвращающий списки, не позволяет узнать, можно ли их безопасно изменить. Единственный вариант - либо полагаться на потенциально неполную или неточную документацию, либо на всякий случай сделать защитную копию. Доступный только для чтения тип коллекции сделает это намного проще.
Жюль
@Jules: действительно, способ Java, кажется, все выше: убедиться, что документация полная и точная, а также сделать защитную копию на всякий случай. Это, безусловно, масштабируется до очень крупных корпоративных проектов.
Rwong
1

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

Обратите внимание на несколько слов: «контракт только для чтения» и «сбор».

Контракт только для чтения означает, что класс Aпредоставляет классу Bдоступ только для чтения, но не подразумевает, что базовый объект коллекции фактически неизменен. Неизменность означает, что она никогда не изменится в будущем, каким бы то ни было агентом. Договор только для чтения говорит только о том, что получателю не разрешено изменять его; кто-то другой (в частности, класс A) может изменить его.

Чтобы сделать объект неизменным, он должен быть действительно неизменным - он должен запрещать попытки изменить свои данные независимо от того, какой агент их запрашивает.

Скорее всего, паттерн наблюдается на объектах, представляющих набор данных - списки, последовательности, файлы (потоки) и т. Д.


Слово «неизменный» становится причудами, но концепция не нова. И есть много способов использования неизменяемости, а также много способов достижения лучшего дизайна с использованием чего-то другого (то есть его конкурентов).


Вот мой подход (не основанный на неизменности).

  1. Определите DTO (объект передачи данных), также известный как кортеж значения.
    • Это DTO будет содержать три поля: hitpoints, attack, defense.
    • Просто поля: общедоступные, любой может писать, без защиты.
    • Однако DTO должен быть одноразовым объектом: если классу Aнужно передать DTO классу B, он делает его копию и передает копию. Таким образом, Bможно использовать DTO так, как ему нравится (писать в него), не влияя на DTO, на котором Aдержится.
  2. grantExperienceФункция , которая будет разбита на две части :
    • calculateNewStats
    • increaseStats
  3. calculateNewStats примет входные данные от двух DTO, один представляет статистику игрока, а другой представляет статистику противника, и выполнит вычисления.
    • Для ввода звонящий должен выбирать между базовой, обученной или эффективной статистикой, в соответствии с вашими потребностями.
    • Результат будет новый DTO , где каждое поле ( hitpoints, attack, defense) хранит сумму , которая будет увеличиваться для этой способности.
    • Новый DTO «сумма для приращения» не касается верхнего предела (установленного максимального ограничения) для этих значений.
  4. increaseStats это метод на игроке (не на DTO), который принимает DTO «на сумму приращения» и применяет это увеличение на DTO, которое принадлежит игроку и представляет собой обучаемое DTO игрока.
    • Если существуют соответствующие максимальные значения для этой статистики, они применяются здесь.

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

Если calculateNewStatsобнаружено, что объект полностью зависит от игрока и вражеских объектов (то есть объекты будущего игрока и врага могут иметь новые свойства и calculateNewStatsдолжны быть обновлены, чтобы использовать как можно больше своих новых свойств), то calculateNewStatsдолжны принять эти два объекты, а не просто DTO. Тем не менее, результатом его вычисления по-прежнему будет приращение DTO или любые другие простые объекты передачи данных, которые несут информацию, используемую для выполнения приращения / обновления.

rwong
источник
1

Здесь есть большая проблема: только то, что структура данных является неизменной, не означает, что нам не нужны ее модифицированные версии . Реальная неизменная версия структуры данных не предусматривает setX, setYи setZметоды - они просто возвращают новую структуру , а не изменять объект, называемый их.

// Mutates mutableFoo
mutableFoo.setX(...)
// Creates a new updated immutableFoo, existing immutableFoo is unchanged
newFoo = immutableFoo.setX(...)

Итак, как вы даете одной части вашей системы возможность изменить ее, одновременно ограничивая другие части? С изменяемым объектом, содержащим ссылку на экземпляр неизменяемой структуры. В принципе, вместо того , чтобы ваш класс игрока в состоянии мутировать свой объект статистика, давая любой другому класс неизменного вид его статистики, ее статистики являются незыблемыми и игрок изменчиво. Вместо:

// Stats are mutable, mutates self.stats
self.stats.setX(...)

Вы бы имели:

// Stats are immutable, mutates self, setX() returns new updated stats
self.stats = self.stats.setX(...)

Увидеть разницу? Объект stats является полностью неизменным, но текущая статистика игрока является изменяемой ссылкой на неизменяемый объект. Нет необходимости создавать два интерфейса вообще - просто сделайте структуру данных полностью неизменной и управляйте изменяемой ссылкой на нее, когда вы используете ее для хранения состояния.

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

В любом случае это имеет больше смысла, так как концептуально меняется не статистика , а текущая статистика игрока . Не объект статистики. Ссылка на статистику объекта объект игрока. Таким образом, другие части вашей системы, зависящие от этой ссылки, должны явно ссылаться player.currentStats()или что-то подобное, вместо того, чтобы овладеть базовым объектом статистики игрока, хранить его где-то и полагаться на его обновление посредством мутаций.

Джек
источник
1

Вау ... это действительно забирает меня обратно. Я пробовал эту же идею несколько раз. Я был слишком упрям, чтобы расстаться с этим, потому что думал, что можно чему-то научиться. Я видел, как другие тоже пробовали это в коде. Большинство реализаций, которые я видел, называются интерфейсами «Только чтение», такими как ваш FooViewerили ReadOnlyFoo«Интерфейс только для записи», FooEditorили « WriteableFooили» в Java FooMutator. Я не уверен, что есть официальный или даже общий словарь для таких вещей.

Это никогда не приносило мне пользы в тех местах, где я это пробовал. Я бы вообще этого избегал. Я бы поступил так, как предлагают другие, и сделал бы шаг назад и подумал, действительно ли вам нужно это понятие в вашей большой идее. Я не уверен, что есть правильный способ сделать это, так как я никогда не сохранял код, созданный при попытке этого. Каждый раз я отступал после значительных усилий и упрощал вещи. И под этим я имею в виду то, что говорили другие о YAGNI, KISS и DRY.

Один возможный недостаток: повторение. Возможно, вам придется создать эти интерфейсы для большого количества классов, и даже для одного класса вы должны назвать каждый метод и описать каждую сигнатуру как минимум дважды. Из всех вещей, которые сжигают меня в кодировании, необходимость изменить несколько файлов таким образом доставляет мне больше всего неприятностей. В конце концов я забываю вносить изменения в одном или другом месте. Если у вас есть класс с именем Foo и интерфейсы с именами FooViewer и FooEditor, если вы решите, что было бы лучше, если бы вы назвали его Bar, вместо этого вам придется трижды реорганизовать-> переименовать, если у вас нет действительно удивительно умной IDE. Я даже обнаружил, что это тот случай, когда у меня было значительное количество кода, поступающего из генератора кода.

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

И затем есть осложнение, которое вы упомянули. Я сомневаюсь, что когда-нибудь снова пойду этим путем с моим кодом. Даже для места, которое лучше всего вписывается в мой общий план для кода (ORM, который я построил), я вытащил это и заменил на что-то более простое для программирования.

Я бы действительно больше думал о композиции, которую вы упомянули. Мне любопытно, почему вы чувствуете, что это плохая идея. Я бы ожидал иметь состав EffectiveStatsи неизменность Statsи что-то подобное, StatModifiersчто в дальнейшем будет составлять некоторый набор StatModifiers, которые представляют все, что может изменить статистику (временные эффекты, предметы, расположение в некоторой области улучшения, усталость), но что вам EffectiveStatsне нужно будет понимать, потому что StatModifiersбудет управлять, что это были за вещи и как они повлияют на статистику. StatModifierбыл бы интерфейсом для различных вещей и мог бы знать такие вещи, как «я в зоне», «когда истощается лекарство» и т. д., но не должен был бы даже позволить другим объектам знать такие вещи. Нужно только сказать, какой стат и как он изменился. Еще лучше, если StatModifierбы можно было просто раскрыть метод создания нового неизменяемого Statsна основе другого, Statsкоторый был бы другим, потому что он был соответствующим образом изменен. Затем вы можете сделать что-то вроде currentStats = statModifier2.modify(statModifier1.modify(baseStats))и все Statsможет быть неизменным. Я бы даже не кодировал это напрямую, я, вероятно, перебрал бы все модификаторы и применил бы каждый к результату предыдущих модификаторов, начиная с baseStats.

Джейсон Келл
источник