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


17

Виходячи з фону OOP (Java), я самостійно навчаюсь Scala. Хоча я легко бачу переваги використання незмінних об'єктів індивідуально, мені важко зрозуміти, як можна створити таке додаток. Наведу приклад:

Скажімо, у мене є предмети, які представляють "матеріали" та їх властивості (я конструюю гру, тому я справді маю цю проблему), як вода та лід. У мене був би "менеджер", який володіє усіма такими екземплярами матеріалів. Однією властивістю була б температура замерзання та плавлення, а також те, до чого матеріал замерзає або плавиться.

[EDIT] Усі матеріальні екземпляри є "однотонними", подібними до Java Enum.

Я хочу, щоб "вода" сказала, що вона замерзає до "ожеледиці" при 0 ° С, а "лід" - щоб вона танула до "води" при 1 ° С. Але якщо вода і лід незмінні, вони не можуть отримати посилання один на одного як параметри конструктора, тому що один з них повинен бути створений першим, і він не міг отримати посилання на ще не існуючий інший як параметр конструктора. Я міг би вирішити це, подавши їм обоє посилання на менеджера, щоб вони могли запитувати його, щоб знайти інший матеріальний екземпляр, який їм потрібен щоразу, коли їх запитують про їхні властивості заморожування / плавлення, але потім у мене виникає однакова проблема між менеджером і матеріали, що вони потребують посилання один на одного, але це може бути надано в конструкторі лише одному з них, тому ні керівник, ні матеріал не можуть бути непорушними.

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


2
для мене, як ви заявляєте, немає ні води, ні льоду. Є лише h2oматеріал
гнат

1
Я знаю, що це мало б більше "наукового сенсу", але в грі легше моделювати його як два різних матеріалу із "фіксованими" властивостями, ніж один матеріал із "змінними" властивостями залежно від контексту.
Себастьєн Діот

Сінглтон - дурна ідея.
DeadMG

@DeadMG Ну добре. Вони не є справжніми Java Singletons. Я просто маю на увазі, що немає сенсу створювати більше ніж один екземпляр кожного, оскільки вони незмінні і були б рівні між собою. Насправді у мене не буде реальних статичних примірників. Мої "кореневі" об'єкти - це сервіси OSGi.
Себастьєн Діот

Відповідь на це інше запитання, здається, підтверджує мою підозру, що речі дуже швидко ускладнюються незмінними: programmers.stackexchange.com/questions/68058/…
Себастьян Діот

Відповіді:


2

Рішення - трохи обдурити. Конкретно:

  • Створіть A, але залиште його посилання на B неініціалізованим (оскільки B ще не існує).

  • Створіть B і вкажіть його на А.

  • Оновіть A до пункту B. Не оновлюйте ні A, ні B після цього.

Це можна зробити явно (наприклад, на C ++):

struct List {
    int n;
    List *next;

    List(int n, List *next)
        : n(n), next(next);
};

// Return a list containing [0,1,0,1,...].
List *binary(void)
{
    List *a = new List(0, NULL);
    List *b = new List(1, a);
    a->next = b; // Evil, but necessary.
    return a;
}

або неявно (приклад у Haskell):

binary :: [Int]
binary = a where
    a = 0 : b
    b = 1 : a

Приклад Haskell використовує ледачу оцінку для досягнення ілюзії взаємно залежних незмінних значень. Значення починаються як:

a = 0 : <thunk>
b = 1 : a

aі bобидві дійсні головні нормальні форми незалежно. Кожен мінус може бути побудований без необхідності остаточного значення іншої змінної. Коли обчислювач буде оцінений, він вкаже на ті самі bточки даних .

Таким чином, якщо ви хочете, щоб два незмінні значення вказували один на одного, вам потрібно або оновити перше після побудови другого, або використовувати механізм вищого рівня, щоб зробити те саме.


У вашому конкретному прикладі я можу виразити це в Haskell як:

data Material = Water {temperature :: Double}
              | Ice   {temperature :: Double}

setTemperature :: Double -> Material -> Material
setTemperature newTemp (Water _) | newTemp <= 0.0 = Ice newTemp
                                 | otherwise      = Water newTemp
setTemperature newTemp (Ice _)   | newTemp >= 1.0 = Water newTemp
                                 | otherwise      = Ice newTemp

Однак я боково ступаю питання. Я б уявив, що в об'єктно-орієнтованому підході, де setTemperatureметод приєднаний до результату кожного Materialконструктора, вам доведеться, щоб конструктори вказували один на одного. Якщо конструктори трактуються як незмінні значення, можна використовувати підхід, викладений вище.


(припустимо, що я зрозумів синтаксис Хаскелла) Я думаю, що моє поточне рішення насправді дуже схоже, але мені було цікаво, чи це "правильний", чи існує щось краще. Спочатку я створюю "ручку" (посилання) для кожного (ще не створеного) об'єкта, потім створюю всі об'єкти, даючи їм потрібні їм ручки і, нарешті, ініціалізує ручку на об'єкти. Об'єкти самі по собі незмінні, але не ручки.
Себастьєн Діот

6

У вашому прикладі ви застосовуєте перетворення до об'єкта, щоб я використав щось на зразок ApplyTransform()методу, який повертає, BlockBaseа не намагався змінити поточний об'єкт.

Наприклад, щоб змінити IceBlock на WaterBlock, застосувавши трохи тепла, я б назвав щось подібне

BlockBase currentBlock = new IceBlock();
currentBlock = currentBlock.ApplyTemperature(1); 
// currentBlock is now a WaterBlock 

і IceBlock.ApplyTemperature()метод виглядав би приблизно так:

public class IceBlock() : BlockBase
{
    public BlockBase ApplyTemperature(int temp)
    {
        return (temp > 0 ? new WaterBlock((BlockBase)this) : this);
    }
}

Це хороша відповідь, але, на жаль, лише тому, що я не зміг зазначити, що мої "матеріали", справді мої "блоки", є однотонними, тому новий WaterBlock () просто не є варіантом. У цьому головна користь непорушних, ви можете використовувати їх нескінченно. Замість того, щоб мати 500 000 блоків у барані, я маю 500 000 посилань на 100 блоків. Набагато дешевше!
Себастьєн Діот

То як щодо повернення BlockList.WaterBlockзамість створення нового блоку?
Рейчел

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

@Sebastien Я думаю, що BlockListце просто staticклас, який відповідає за окремі екземпляри кожного блоку, тому вам не потрібно створювати екземпляр BlockList(я звик до C #)
Рейчел

@Sebastien: Якщо ви використовуєте Singeltons, ви платите ціну.
DeadMG

6

Ще один спосіб розірвати цикл - це розділити проблеми матеріалу та трансмутації на деяких складених мовах:

water = new Block("water");
ice = new Block("ice");

transitions = new Transitions([
    new transitions.temperature.Below(0.0, water, ice),
    new transitions.temperature.Above(0.0, ice, water),
]);

Так, мені важко було прочитати це спочатку, але я думаю, що це по суті той самий підхід, який я обстоював.
Айдан Каллі

1

Якщо ви збираєтесь використовувати функціональну мову і хочете усвідомити переваги незмінності, то вам слід підійти до проблеми з цим на увазі. Ви намагаєтеся визначити тип об'єкта "лід" або "вода", який може підтримувати діапазон температур - щоб підтримати незмінність, тоді вам потрібно буде створювати новий об'єкт щоразу, коли температура змінюється, а це марно. Тому спробуйте зробити поняття блокового типу та температури більш незалежними. Я не знаю Scala (це в моєму списку навчальних записів :-)), але, запозичивши Joey Adams Answer у Haskell , я пропоную щось на зразок:

data Material = Water | Ice

blockForTemperature :: Double -> Material
blockForTemperature x = 
  if x < 0 then Ice else Water

або можливо:

transitionForTemperature :: Material -> Double -> Material
transitionForTemperature oldMaterial newTemp = 
  case (oldMaterial, newTemp) of
    (Ice, _) | newTemp > 0 -> Water
    (Water, _) | newTemp <= 0 -> Ice

(Примітка: я не намагався це запустити, і мій Haskell трохи іржавий.) Тепер логіка переходу відокремлена від типу матеріалу, тому вона не витрачає стільки пам’яті, і (на мій погляд) це цілком трохи більш функціонально орієнтований.


Я насправді не намагаюся використовувати "функціональну мову", тому що просто не розумію! Єдине, що я зазвичай затримую від будь-якого нетривіального прикладу функціонального програмування: "Чорт, я, який я був розумніший!" Це просто поза мною, як це може мати сенс для будь-кого. З моїх студентських днів я пам’ятаю, що Prolog (заснований на логіці), Occam (все працює в паралелі за замовчуванням) і навіть асемблер мали сенс, але Лісп був просто божевільним. Але я отримую точку переміщення коду, що викликає "зміну стану" поза "станом".
Себастьєн Діот
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.