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

17

Исходя из опыта ООП (Java), я изучаю Scala самостоятельно. Несмотря на то, что я легко вижу преимущества индивидуального использования неизменяемых объектов, мне трудно понять, как можно создать такое целое приложение. Я приведу пример:

Скажем, у меня есть объекты, которые представляют «материалы» и их свойства (я разрабатываю игру, поэтому у меня действительно есть эта проблема), например, вода и лед. У меня был бы «менеджер», которому принадлежат все такие экземпляры материалов. Одним из свойств будет точка замерзания и плавления, а также то, до чего материал замерзает или плавится.

[РЕДАКТИРОВАТЬ] Все экземпляры материала "singleton", вроде как Enum Java.

Я хочу, чтобы «вода» говорила, что она замерзает до «льда» при 0 ° С, а «лед» говорит, что она плавится до «воды» при 1 ° С. Но если вода и лед являются неизменными, они не могут получить ссылку друг на друга в качестве параметров конструктора, потому что один из них должен быть создан первым, а другой не может получить ссылку на еще не существующий другой в качестве параметра конструктора. Я мог бы решить эту проблему, дав им обоим ссылку на менеджера, чтобы они могли запросить его, чтобы найти другой экземпляр материала, который им нужен каждый раз, когда их спрашивают об их свойствах замораживания / плавления, но затем я получаю ту же проблему между менеджером и материалы, что им нужна ссылка друг на друга, но она может быть предоставлена ​​в конструкторе только для одного из них, поэтому менеджер или материал не могут быть неизменными.

Они просто не могут обойти эту проблему, или мне нужно использовать «функциональные» методы программирования или какой-то другой шаблон для ее решения?

Себастьян Диот
источник
2
для меня, как вы говорите, нет ни воды, ни льда. Там просто h2oматериал
комнат
1
Я знаю, что это будет иметь больше «научного смысла», но в игре легче моделировать его как два разных материала с «фиксированными» свойствами, а не как один материал с «переменными» свойствами в зависимости от контекста.
Себастьян Диот
Синглтон - глупая идея.
DeadMG
@DeadMG Хорошо, хорошо. Они не настоящие Java Singletons. Я просто имею в виду, что нет смысла создавать более одного экземпляра каждого, поскольку они неизменны и будут равны друг другу. На самом деле, у меня не будет реальных статических экземпляров. Мои "корневые" объекты - это сервисы OSGi.
Себастьян Диот
Ответ на этот другой вопрос, кажется, подтверждает мое подозрение, что с неизменяемыми
объектами

Ответы:

2

Решение состоит в том, чтобы немного обмануть. В частности:

  • Создайте A, но оставьте ссылку на B неинициализированной (так как B еще не существует).

  • Создайте B и укажите на A.

  • Обновите 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),
]);
SingleNegationElimination
источник
Да, мне было трудно сначала прочитать это, но я думаю, что по сути это тот же подход, который я защищал.
Эйдан Калли
1

Если вы собираетесь использовать функциональный язык и хотите реализовать преимущества неизменяемости, то вам следует подойти к проблеме с учетом этого. Вы пытаетесь определить тип объекта "лед" или "вода", который может поддерживать диапазон температур - чтобы поддерживать неизменность, вам нужно будет создавать новый объект каждый раз при изменении температуры, что бесполезно. Поэтому постарайтесь сделать понятия типа блока и температуры более независимыми. Я не знаю Scala (это в моем списке для изучения :-)), но, заимствуя у Джои Адамса Ответа в 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 (все работает параллельно) по умолчанию и даже ассемблер имели смысл, но Lisp был просто сумасшедшим. Но я хочу, чтобы вы переместили код, который вызывает «изменение состояния» за пределы «состояния».
Себастьян Диот