不変オブジェクトを使用しながら、鶏と卵のほとんどの問題を解決するために適用できる特定の設計戦略はありますか?


17

OOPのバックグラウンド(Java)から来て、私は自分でScalaを学んでいます。不変オブジェクトを個別に使用することの利点はすぐにわかりますが、そのようなアプリケーション全体をどのように設計できるかを見るのは大変です。例を挙げます。

「マテリアル」とそれらのプロパティを表すオブジェクト(ゲームを設計しているので、実際にその問題を抱えている)があり、たとえば水や氷があるとします。このようなすべてのマテリアルインスタンスを所有する「マネージャー」がいます。1つの特性は、凝固点と融点、および材料が凍結または融解するものです。

[編集]すべてのマテリアルインスタンスは「シングルトン」で、Java Enumのようなものです。

「水」は0℃で「氷」に凍結し、「氷」は1℃で「水」に融解すると言います。しかし、水と氷が不変である場合、それらの1つを最初に作成する必要があり、コンストラクターパラメーターとしてまだ存在しない他のものへの参照を取得できなかったため、コンストラクターパラメーターとして相互参照を取得できません。両方のマネージャーへの参照を提供することでこれを解決することができますので、彼らは凍結/融解特性を求められるたびに必要な他の材料インスタンスを見つけるためにクエリすることができますが、マネージャー間で同じ問題が発生しますそして、それらはお互いへの参照を必要としますが、それらのうちの1つのコンストラクターでのみ提供できるため、マネージャーまたはマテリアルは不変にできません。

彼らはこの問題を回避する方法がないのですか、それとも「機能的な」プログラミング技術、またはそれを解決するための他のパタ​​ーンを使用する必要がありますか?


2
私にとって、あなたが述べているように、水も氷もありません。ちょうどh2o材料があります
-gnat

1
これはより「科学的な意味」をもたらすことはわかっていますが、ゲームでは、コンテキストに応じて「可変」プロパティを持つ1つのマテリアルではなく、「固定」プロパティを持つ2つの異なるマテリアルとしてモデル化する方が簡単です。
セバスチャン・ディオ

シングルトンは愚かなアイデアです。
-DeadMG

@DeadMGまあ、OK。それらは実際のJavaシングルトンではありません。それらは不変であり、互いに等しいため、それぞれのインスタンスを複数作成する意味がないことを意味します。実際、実際の静的インスタンスはありません。私の「ルート」オブジェクトはOSGiサービスです。
セバスチャン・ディオ

この他の質問への答えは、物事がimmutablesと本当にすぐ複雑になるというのが私の疑いを確認するように見えるprogrammers.stackexchange.com/questions/68058/...
セバスチャンDiot

回答:


2

解決策は少しチートすることです。具体的には:

  • 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ポイントを指します。

したがって、2つの不変の値が相互に指し示すようにする場合は、2番目の値を作成した後に最初の値を更新するか、より高いレベルのメカニズムを使用して同じことを行う必要があります。


あなたの特定の例では、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コンストラクターの結果にアタッチされるオブジェクト指向のアプローチでは、コンストラクターが互いにポイントする必要があると思います。コンストラクターが不変値として扱われる場合は、上記で概説したアプローチを使用できます。


(Haskellの構文を理解していると仮定して)私の現在のソリューションは実際には非常に似ていると思いますが、それが「正しいもの」であるのか、それとももっと良いものがあるのか​​疑問に思っていました。最初に、すべての(まだ作成されていない)オブジェクトの「ハンドル」(参照)を作成し、次にすべてのオブジェクトを作成して必要なハンドルを与え、最後にオブジェクトのハンドルを初期化します。オブジェクト自体は不変ですが、ハンドルではありません。
セバスチャンディオ

6

この例では、オブジェクトに変換を適用しているため、現在のオブジェクトを変更しようとApplyTransform()するのBlockBaseではなく、aを返すメソッドのようなものを使用します。

たとえば、いくらかの熱を加えることによって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()はオプションではありません。それが不変の主な利点であり、無限に再利用できます。RAMに500,000ブロックを使用する代わりに、100ブロックを500,000個参照します。もっと安い!
セバスチャン・ディオ

では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を知りません(私の学習リストにあります:-))、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は少し錆びています。)今、遷移ロジックはマテリアルタイプから分離されているので、それほど多くのメモリを無駄にしません(そして、私の意見では)もう少し機能指向です。


私は実際に「関数型言語」を使用しようとはしていません。自明でない関数型プログラミングの例から私が普段保持しているのは、「くそー、私はもっと賢かった!」これがどのように誰にとっても理にかなっているのか、私だけではありません。私の学生時代から、Prolog(論理ベース)、Occam(すべてがデフォルトで並列実行)、さらにはアセンブラーまでもが理にかなっていることを覚えていますが、Lispはただのクレイジーなものでした。しかし、「状態の変更」を引き起こすコードを「状態」の外に移動することのポイントを取得します。
セバスチャン・ディオ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.