Окремий інтерфейс для методів мутації


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


1
Я думаю, що проблема полягає в тому, що ваш об'єкт є змінним, навіть якщо ви отримуєте доступ до нього в моєму "незмінному" інтерфейсі. Краще мати змінний об’єкт із програмою Player.TrainStats ()
Ewan

@gnat у вас є посилання на те, де я можу дізнатися більше про це? Виходячи з відредагованого питання, я не впевнений, як і де я міг би це застосувати.
Зимус

@gnat: ваші посилання (і його посилання другого ступеня та третього ступеня) дуже корисні, але влучні фрази мудрості мало допомагають. Це тільки приваблює нерозуміння і зневага.
rwong

Відповіді:


6

Мені здається, ти маєш рішення шукати проблему.

Чи існує назва поділу інтерфейсів за допомогою аксесуарів та мутаторів на окремі інтерфейси?

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

Виняток - коли ви розробляєте нову мову програмування або багатоцільовий фреймворк, який буде використовуватися десь десятьма тисячами програмістів або більше. Тоді це дійсно може бути краще, коли ви пропонуєте незмінні та незмінні варіанти типів даних загального призначення. Але це ситуація, коли у вас буде тисячі різних сценаріїв використання - напевно, це не проблема, з якою ви стикаєтесь?

Розбиває їх таким чином «правильний» підхід, якщо вони однаковою мірою можуть бути використані або є інша, більш прийнята модель

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

Чи є негайно передбачувані мінуси використання цього шаблону, окрім ускладнення ієрархії інтерфейсу?

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


1
Я думаю, ти маєш на увазі "KISS - Тримай просто,
глупо

2

Чи існує назва поділу інтерфейсів за допомогою аксесуарів та мутаторів на окремі інтерфейси?

Для цього може бути назва, якщо ця відокремленість є корисною і дає користь, яку я не бачу . Якщо сепарати не приносять користі, інші два питання не мають сенсу.

Чи можете ви сказати нам про будь-який випадок використання бізнесу, коли два окремі інтерфейси дають нам користь чи це питання є академічною проблемою (YAGNI)?

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

Реалізації, які я бачив, не розділяють інтерфейси

  • в Java немає інтерфейсу ReadOnlyList

Версія ReadOnly використовує "WritableInterface" і видає виняток, якщо використовується метод запису


Я змінив ОП, щоб пояснити основний випадок використання.
Зимус

2
"в Java немає інтерфейсу ReadOnlyList" - відверто кажучи, має бути. Якщо у вас є фрагмент коду, який приймає список <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 або будь-які прості об'єкти передачі даних, які несуть інформацію, яка використовується для виконання збільшення / оновлення.


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(...)

Бачите різницю? Об'єкт статистики є абсолютно незмінним, але поточна статистика гравця - це змінна посилання на незмінний об'єкт. Не потрібно робити два інтерфейси взагалі - просто зробіть структуру даних повністю непорушною і керуйте змінною посиланням на неї там, де ви, звичайно, використовуєте її для зберігання стану.

Це означає, що інші об’єкти у вашій системі не можуть покладатися на посилання на об’єкт статистики - об’єкт статистики, який вони не матимуть, не буде тим самим об’єктом статистики, який має гравець після оновлення гравця своєї статистики.

Це має сенс у будь-якому випадку, оскільки концептуально це не насправді статистика, яка змінюється, це поточна статистика гравця . Не об’єкт статистики. Посилання на статистику об'єкта об'єкт гравця. Отже, в інших частинах вашої системи, залежно від цього посилання, слід чітко посилатися player.currentStats()або щось подібне, а не отримувати об'єкт статистики гравця, зберігати його десь і покладатися на оновлення за допомогою мутацій.


1

Нічого ... це насправді повертає мене назад. Я спробував цю саму ідею кілька разів. Я був надто впертий, щоб відмовитись від цього, тому що думав, що є чомусь хорошому навчитися. Я бачив, як інші пробують це також у коді. Більшість реалізацій я бачив під назвою Read-Only інтерфейси , як ваша FooViewerабо ReadOnlyFooй інтерфейс Write-тільки FooEditorабо WriteableFooабо в Java я думаю , що я бачив FooMutatorодин раз. Я не впевнений, що існує офіційний або навіть загальний словник для того, щоб робити такі дії.

Це ніколи не робило нічого корисного для мене в місцях, де я це спробував. Я б уникну цього взагалі. Я би зробив так, як пропонують інші, і зробити крок назад і подумати, чи справді вам потрібна ця ідея у вашій більшій ідеї. Я не впевнений, що є правильний спосіб зробити це, оскільки я ніколи фактично не зберігав коду, який я створив під час спроби цього. Кожного разу я відступав після значних зусиль і спрощував речі. І під цим я маю на увазі щось, що сказали інші про YAGNI та KISS та DRY.

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

Особисто мені не подобається створювати інтерфейси для речей, які також мають лише одну реалізацію. Це означає, що коли я подорожую в коді, я не можу просто перейти до єдиної реалізації безпосередньо, мені доведеться перейти до інтерфейсу, а потім до реалізації інтерфейсу або хоча б натиснути кілька додаткових клавіш, щоб потрапити туди. Ці речі складаються.

І тут є ускладнення, про яке ви згадали. Я сумніваюся, що коли-небудь знову пройдуся саме цим кодом зі своїм кодом. Навіть для місця, яке найкраще вписується в мій загальний план коду (ORM, який я побудував), я витягнув це і замінив його чимось легшим для кодування.

Я б дійсно більше задумався над згаданою вами композицією. Мені цікаво, чому ви вважаєте, що це буде поганою ідеєю. Я б очікував EffectiveStatsскласти та непорушний, Statsі щось подібне, StatModifiersщо надалі складе якийсь набір StatModifiers, який представляє все, що може змінити статистику (тимчасові ефекти, елементи, розташування в якійсь області покращення, втома), але що вам EffectiveStatsне потрібно буде розуміти, тому StatModifiersщо керуйте, які ці речі були і який ефект і який би вони мали на якій статистиці. TheStatModifierможе бути інтерфейсом для різних речей і міг би знати такі речі, як "я в зоні", "коли ліки зношуються" тощо. Потрібно було б лише сказати, яка статистика та наскільки модифікована вона була зараз. Ще краще StatModifierпросто розкрити спосіб отримання нового незмінного, Statsзаснованого на іншому, Statsякий би відрізнявся, оскільки він був належним чином змінений. Тоді ви могли б зробити щось на кшталт, currentStats = statModifier2.modify(statModifier1.modify(baseStats))і все це Statsможе бути незмінним. Я б навіть не кодував це безпосередньо, я, мабуть, перегляне всі модифікатори і застосує кожен до результату попередніх модифікаторів, починаючи з baseStats.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.