Apakah ada strategi desain khusus yang dapat diterapkan untuk menyelesaikan sebagian besar masalah ayam dan telur saat menggunakan objek yang tidak dapat diubah?

17

Berasal dari latar belakang OOP (Jawa), saya belajar Scala sendiri. Sementara saya dapat dengan mudah melihat keuntungan dari menggunakan objek yang tidak dapat diubah secara individual, saya mengalami kesulitan melihat bagaimana seseorang dapat merancang seluruh aplikasi seperti itu. Saya akan memberi contoh:

Katakanlah saya memiliki objek yang mewakili "bahan" dan sifat-sifatnya (saya merancang permainan, jadi saya benar-benar memiliki masalah itu), seperti air dan es. Saya akan memiliki "manajer" yang memiliki semua contoh materi seperti itu. Satu properti akan menjadi titik beku dan leleh, dan bahan itu membeku atau meleleh.

[EDIT] Semua instance materi adalah "singleton", semacam Java Enum.

Saya ingin "air" untuk mengatakan itu membeku menjadi "es" pada 0C, dan "es" untuk mengatakan itu mencair menjadi "air" pada 1C. Tetapi jika air dan es tidak dapat diubah, mereka tidak dapat mendapatkan referensi satu sama lain sebagai parameter konstruktor, karena salah satunya harus dibuat terlebih dahulu, dan bahwa seseorang tidak bisa mendapatkan referensi ke yang lain yang belum ada sebagai parameter konstruktor. Saya bisa menyelesaikan ini dengan memberi mereka berdua referensi ke manajer sehingga mereka dapat meminta itu untuk menemukan contoh material lain yang mereka butuhkan setiap kali mereka diminta untuk pembekuan / sifat lelehnya, tetapi kemudian saya mendapatkan masalah yang sama antara manajer dan bahan-bahan, yang mereka butuhkan referensi satu sama lain, tetapi hanya dapat disediakan di konstruktor untuk salah satu dari mereka, sehingga manajer atau bahan tidak dapat berubah.

Apakah tidak ada jalan keluar untuk masalah ini, atau apakah saya perlu menggunakan teknik pemrograman "fungsional", atau beberapa pola lain untuk menyelesaikannya?

Sebastien Diot
sumber
2
bagi saya, cara Anda menyatakan, tidak ada air dan es. Hanya ada h2obahan
nyamuk
1
Saya tahu ini akan lebih masuk akal "ilmiah", tetapi dalam permainan lebih mudah untuk memodelkannya sebagai dua bahan yang berbeda dengan sifat "tetap", daripada satu bahan dengan sifat "variabel" tergantung pada konteksnya.
Sebastien Diot
Singleton adalah ide bodoh.
DeadMG
@DeadMG Baiklah, oke. Mereka bukan orang Jawa asli. Maksud saya, tidak ada gunanya untuk membuat lebih dari satu contoh masing-masing, karena mereka tidak berubah dan akan sama satu sama lain. Sebenarnya, saya tidak akan memiliki contoh statis nyata. Objek "root" saya adalah layanan OSGi.
Sebastien Diot
Jawaban untuk pertanyaan lain ini tampaknya mengkonfirmasi kecurigaan saya bahwa segala sesuatunya menjadi sangat rumit dengan kekal: programmer
.stackexchange.com/questions/68058/…

Jawaban:

2

Solusinya adalah sedikit curang. Secara khusus:

  • Buat A, tetapi biarkan referensi ke B tidak diinisialisasi (karena B belum ada).

  • Buat B, dan arahkan ke A.

  • Perbarui A ke titik B. Jangan memperbarui A atau B setelah ini.

Ini dapat dilakukan secara eksplisit (contoh dalam 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;
}

atau secara implisit (contoh dalam Haskell):

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

Contoh Haskell menggunakan evaluasi malas untuk mencapai ilusi nilai-nilai abadi yang saling bergantung. Nilai dimulai sebagai:

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

adan bkeduanya merupakan bentuk normal kepala yang valid secara independen. Setiap kontra dapat dibangun tanpa memerlukan nilai akhir dari variabel lain. Ketika thunk dievaluasi, maka akan menunjuk ke titik data yang sama b.

Jadi, jika Anda ingin dua nilai yang tidak dapat diubah untuk menunjuk satu sama lain, Anda harus memperbarui yang pertama setelah membangun yang kedua, atau menggunakan mekanisme tingkat yang lebih tinggi untuk melakukan hal yang sama.


Dalam contoh khusus Anda, saya dapat mengungkapkannya di Haskell sebagai:

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

Namun, saya melangkahi masalah ini. Saya akan membayangkan itu dalam pendekatan berorientasi objek, di mana sebuah setTemperaturemetode melekat pada hasil masing-masingMaterial konstruktor, Anda harus memiliki titik konstruktor satu sama lain. Jika konstruktor diperlakukan sebagai nilai yang tidak dapat diubah, Anda dapat menggunakan pendekatan yang diuraikan di atas.

Joey Adams
sumber
(dengan asumsi saya mengerti sintaksis Haskell) Saya pikir solusi saya saat ini sebenarnya sangat mirip, tetapi saya bertanya-tanya apakah itu "yang benar", atau apakah ada sesuatu yang lebih baik. Pertama saya membuat "handle" (referensi) untuk setiap objek (yang belum dibuat), kemudian membuat semua objek, memberi mereka pegangan yang mereka butuhkan, dan akhirnya menginisialisasi pegangan ke objek. Benda-benda itu sendiri tidak berubah, tetapi bukan pegangannya.
Sebastien Diot
6

Dalam contoh Anda, Anda menerapkan transformasi ke objek jadi saya akan menggunakan sesuatu seperti ApplyTransform() metode yang mengembalikan BlockBasedaripada mencoba mengubah objek saat ini.

Misalnya, untuk mengubah IceBlock ke WaterBlock dengan menerapkan panas, saya akan memanggil sesuatu seperti

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

dan IceBlock.ApplyTemperature()metodenya akan terlihat seperti ini:

public class IceBlock() : BlockBase
{
    public BlockBase ApplyTemperature(int temp)
    {
        return (temp > 0 ? new WaterBlock((BlockBase)this) : this);
    }
}
Rachel
sumber
Ini adalah jawaban yang bagus, tetapi sayangnya hanya karena saya gagal menyebutkan bahwa "materi" saya, memang "blok" saya, semuanya singleton, sehingga WaterBlock baru () bukan merupakan pilihan. Itu adalah manfaat utama dari kekekalan, Anda dapat menggunakannya kembali secara tak terbatas. Alih-alih memiliki 500.000 blok di ram, saya memiliki 500.000 referensi ke 100 blok. Lebih murah!
Sebastien Diot
Jadi bagaimana dengan kembali BlockList.WaterBlockdaripada membuat blok baru?
Rachel
Ya, ini yang saya lakukan, tetapi bagaimana cara mendapatkan daftar blokir? Jelas, blok harus dibuat sebelum daftar blok, dan oleh karena itu, jika blok benar-benar tidak dapat diubah, itu tidak dapat menerima daftar blok sebagai parameter. Jadi dari mana ia mendapatkan daftar itu? Poin umum saya adalah, dengan membuat kode lebih berbelit-belit, Anda memecahkan masalah ayam-dan-telur di satu tingkat, hanya untuk mendapatkannya lagi di tingkat berikutnya. Pada dasarnya, saya tidak melihat cara untuk membuat aplikasi secara keseluruhan berdasarkan pada immutability. Tampaknya hanya berlaku untuk "benda kecil", tetapi tidak untuk wadah.
Sebastien Diot
@Sebastien Saya berpikir itu BlockListhanya statickelas yang bertanggung jawab untuk instance tunggal dari setiap blok, jadi Anda tidak perlu membuat instance dari BlockList(Saya sudah terbiasa dengan C #)
Rachel
@Sebastien: Jika Anda menggunakan Singeltons, Anda membayar harganya.
DeadMG
6

Cara lain untuk memutus siklus adalah dengan memisahkan masalah materi dan transmutasi, dalam beberapa bahasa yang dibuat-buat:

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
sumber
Huh, sulit bagi saya untuk membaca yang ini pada awalnya, tapi saya pikir itu pada dasarnya pendekatan yang sama yang saya anjurkan.
Aidan Cully
1

Jika Anda akan menggunakan bahasa fungsional, dan Anda ingin menyadari manfaat dari keabadian, maka Anda harus mendekati masalah dengan itu dalam pikiran. Anda sedang mencoba untuk menentukan jenis objek "es" atau "air" yang dapat mendukung berbagai suhu - untuk mendukung kekekalan, Anda kemudian perlu membuat objek baru setiap kali perubahan suhu, yang boros. Jadi cobalah membuat konsep tipe blok dan suhu lebih mandiri. Saya tidak tahu Scala (ada di daftar to-learn saya :-)), tetapi meminjam dari Joey Adams Answer di Haskell , saya menyarankan sesuatu seperti:

data Material = Water | Ice

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

atau mungkin:

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

(Catatan: Saya belum mencoba menjalankan ini, dan Haskell saya agak berkarat.) Sekarang, logika transisi dipisahkan dari jenis material, sehingga tidak membuang banyak memori, dan (menurut saya) cukup sedikit lebih berorientasi fungsional.

Aidan Cully
sumber
Saya sebenarnya tidak mencoba menggunakan "bahasa fungsional" karena saya tidak mengerti! Satu-satunya hal yang biasanya saya pertahankan dari contoh pemrograman fungsional non-sepele adalah: "Sial, saya yang saya lebih pintar!" Hanya di luar saya bagaimana ini bisa masuk akal bagi siapa pun. Dari masa murid-murid saya, saya ingat bahwa Prolog (berbasis logika), Occam (semuanya berjalan secara paralel-secara-default) dan bahkan assembler masuk akal, tetapi Lisp hanyalah hal yang gila. Tapi saya ingin Anda memindahkan kode yang menyebabkan "perubahan negara" di luar "keadaan".
Sebastien Diot