불변 개체를 사용하는 동안 대부분의 닭과 계란 문제를 해결하기 위해 적용 할 수있는 특정 설계 전략이 있습니까? 어는점과 녹는 점, 재료가 얼거나

OOP 배경 (자바)에서 온 저는 스칼라를 스스로 배우고 있습니다. 불변 개체를 개별적으로 사용하는 이점을 쉽게 볼 수는 있지만, 전체 응용 프로그램을 어떻게 디자인 할 수 있는지 보는 데 어려움을 겪고 있습니다. 예를 들어 보겠습니다.

물과 얼음과 같은 “재료”와 그 속성 (게임을 디자인하고 있으므로 실제로 문제가 있습니다)을 나타내는 개체가 있다고 가정 해 봅시다. 그러한 모든 재료 인스턴스를 소유하는 “관리자”가 있습니다. 한 가지 특성은 어는점과 녹는 점, 재료가 얼거나 녹는 것입니다.

[편집] 모든 머티리얼 인스턴스는 Java Enum과 같은 “단일”입니다.

나는 “물”이 0C에서 “얼음”으로 얼 었다고 말하고 “얼음”은 1C에서 “물”로 녹인다 고 말하고 싶습니다. 그러나 물과 얼음이 불변이라면, 그것들 중 하나가 먼저 생성되어야하고, 아직 존재하지 않는 다른 생성자 매개 변수에 대한 참조를 얻을 수 없기 때문에 생성자 매개 변수로서 서로에 대한 참조를 얻을 수 없습니다. 동결 / 용해 속성을 요청할 때마다 필요한 다른 재료 인스턴스를 찾기 위해 쿼리 할 수 ​​있도록 관리자에 대한 참조를 제공 하여이 문제를 해결할 수 있지만 관리자간에 동일한 문제가 발생합니다. 그리고 자료는 서로에 대한 참조가 필요하지만 그 중 하나에 대해서만 생성자에 제공 될 수 있으므로 관리자 나 자료는 변경할 수 없습니다.

이 문제를 해결할 방법이 없습니까? 아니면 “기능적”프로그래밍 기술이나 다른 패턴을 사용하여 해결해야합니까?



답변

해결책은 약간의 속임수입니다. 구체적으로 특별히:

  • A를 작성하되 B에 대한 참조는 아직 초기화되지 않은 상태로 두십시오 (B는 아직 존재하지 않음).

  • B를 작성하고 A를 가리 키도록하십시오.

  • B를 가리 키도록 A를 업데이트하십시오.이 후에 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생성자 의 결과에 첨부 되는 객체 지향 접근 방식에서는 생성자가 서로를 가리켜 야한다고 생각합니다. 생성자가 변경 불가능한 값으로 처리되는 경우 위에서 설명한 방법을 사용할 수 있습니다.


답변

귀하의 예에서는 객체에 변형을 적용 하여 현재 객체를 변경하려고 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);
    }
}


답변

주기를 깨는 또 다른 방법은 일부 언어로 재료와 변형에 대한 우려를 분리하는 것입니다.

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),
]);


답변

기능적 언어를 사용하려고하고 불변성의 이점을 실현하려면 문제를 염두에 두어야합니다. 다양한 온도를 지원할 수있는 “얼음”또는 “물”이라는 객체 유형을 정의하려고합니다. 불변성을 지원하려면 온도가 변할 때마다 새 객체를 만들어야하므로 낭비입니다. 따라서 블록 유형과 온도 개념을보다 독립적으로 만드십시오. 나는 Scala를 알지 못하지만 (내 학습 목록에 있습니다 :-)) Haskell의 Joey Adams Answer에서 빌리면 다음과 같이 제안합니다.

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은 약간 녹슬 었습니다.) 이제 전환 논리는 재료 유형과 분리되어 있으므로 많은 메모리를 낭비하지 않으며 (제 의견으로는) 상당히 좋습니다. 좀 더 기능 지향적입니다.


답변