Apa cara terbaik untuk menginisialisasi referensi anak ke orang tuanya?

35

Saya mengembangkan model objek yang memiliki banyak kelas orangtua / anak yang berbeda. Setiap objek anak memiliki referensi ke objek induknya. Saya dapat memikirkan (dan telah mencoba) beberapa cara untuk menginisialisasi referensi orang tua, tetapi saya menemukan kelemahan yang signifikan untuk setiap pendekatan. Mengingat pendekatan yang dijelaskan di bawah ini mana yang terbaik ... atau apa yang lebih baik.

Saya tidak akan memastikan kode di bawah ini dikompilasi jadi silakan coba lihat maksud saya jika kode tidak benar secara sintaksis.

Perhatikan bahwa beberapa konstruktor kelas anak saya mengambil parameter (selain orang tua) meskipun saya tidak selalu menampilkannya.

  1. Penelepon bertanggung jawab untuk mengatur orang tua dan menambahkan ke orang tua yang sama.

    class Child {
      public Child(Parent parent) {Parent=parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      public Child Child {get; set;}
      //children
      private List<Child> _children = new List<Child>();
      public List<Child> Children { get {return _children;} }
    }
    

    Kelemahan: pengaturan induk adalah proses dua langkah untuk konsumen.

    var child = new Child(parent);
    parent.Children.Add(child);
    

    Kelemahan: rawan kesalahan. Penelepon dapat menambahkan anak ke orangtua yang berbeda dari yang digunakan untuk menginisialisasi anak.

    var child = new Child(parent1);
    parent2.Children.Add(child);
  2. Induk memverifikasi bahwa pemanggil menambahkan anak ke induk yang diinisialisasi.

    class Child {
      public Child(Parent parent) {Parent = parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      private Child _child;
      public Child Child {
        get {return _child;}
        set {
          if (value.Parent != this) throw new Exception();
          _child=value;
        }
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public void AddChild(Child child) {
        if (child.Parent != this) throw new Exception();
        _children.Add(child);
      }
    }

    Kelemahan: Pemanggil masih memiliki proses dua langkah untuk pengaturan induk.

    Kelemahan: pengecekan runtime - mengurangi kinerja dan menambahkan kode ke setiap add / setter.

  3. Orang tua menetapkan referensi orang tua anak (untuk dirinya sendiri) ketika anak ditambahkan / ditugaskan ke orang tua. Setter induk adalah internal.

    class Child {
      public Parent Parent {get; internal set;}
    }
    class Parent {
      // singleton child
      private Child _child;
      public Child Child {
        get {return _child;}
        set {
          value.Parent = this;
          _child = value;
        }
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public void AddChild(Child child) {
        child.Parent = this;
        _children.Add(child);
      }
    }

    Kelemahan: Anak dibuat tanpa referensi orang tua. Terkadang inisialisasi / validasi memerlukan induk yang berarti beberapa inisialisasi / validasi harus dilakukan di setter induk anak. Kode dapat menjadi rumit. Akan jauh lebih mudah untuk menerapkan anak jika selalu memiliki referensi orang tuanya.

  4. Orang tua memperlihatkan metode penambahan pabrik sehingga seorang anak selalu memiliki referensi orang tua. Aktor anak adalah internal. Setter induk bersifat pribadi.

    class Child {
      internal Child(Parent parent, init-params) {Parent = parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      public Child Child {get; private set;}
      public void CreateChild(init-params) {
          var child = new Child(this, init-params);
          Child = value;
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public Child AddChild(init-params) {
        var child = new Child(this, init-params);
        _children.Add(child);
        return child;
      }
    }

    Kelemahan: Tidak dapat menggunakan sintaks inisialisasi seperti new Child(){prop = value}. Sebaliknya harus dilakukan:

    var c = parent.AddChild(); 
    c.prop = value;

    Kelemahan: Harus menduplikasi parameter konstruktor anak dalam metode pabrik tambahan.

    Kelemahan: Tidak dapat menggunakan properti setter untuk anak tunggal. Tampaknya lumpuh bahwa saya memerlukan metode untuk mengatur nilai tetapi memberikan akses baca melalui pengambil properti. Itu miring.

  5. Child menambahkan dirinya ke orang tua yang dirujuk dalam konstruktornya. Ctor anak adalah publik. Tidak ada publik menambahkan akses dari orang tua.

    //singleton
    class Child{
      public Child(ParentWithChild parent) {
        Parent = parent;
        Parent.Child = this;
      }
      public ParentWithChild Parent {get; private set;}
    }
    class ParentWithChild {
      public Child Child {get; internal set;}
    }
    
    //children
    class Child {
      public Child(ParentWithChildren parent) {
        Parent = parent;
        Parent._children.Add(this);
      }
      public ParentWithChildren Parent {get; private set;}
    }
    class ParentWithChildren {
      internal List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
    }

    Kelemahan: sintaks panggilan tidak bagus. Biasanya seseorang memanggil addmetode pada orang tua alih-alih hanya membuat objek seperti ini:

    var parent = new ParentWithChildren();
    new Child(parent); //adds child to parent
    new Child(parent);
    new Child(parent);

    Dan menetapkan properti daripada hanya membuat objek seperti ini:

    var parent = new ParentWithChild();
    new Child(parent); // sets parent.Child

...

Saya baru tahu bahwa SE tidak mengizinkan beberapa pertanyaan subjektif dan jelas ini adalah pertanyaan subjektif. Tapi, mungkin itu adalah pertanyaan subjektif yang bagus.

Steven Broshar
sumber
14
Praktik terbaik adalah bahwa anak-anak tidak boleh tahu tentang orang tua mereka.
Telastyn
2
@ Telastyn Saya tidak bisa tidak membaca itu sebagai lidah di pipi, dan itu lucu. Juga benar-benar mati berdarah akurat. Steven, istilah untuk melihat adalah "asiklik" karena ada banyak literatur di luar sana tentang mengapa Anda harus membuat grafik asiklik jika memungkinkan.
Jimmy Hoffa
10
@Telastyn Anda harus mencoba menggunakan komentar itu di parenting.stackexchange
Fabio Marcolini
2
Hmm. Tidak tahu cara memindahkan pos (tidak melihat kontrol bendera). Saya diposting ulang ke programmer karena seseorang mengatakan kepada saya itu milik di sana.
Steven Broshar

Jawaban:

19

Saya akan menjauh dari skenario apa pun yang mengharuskan anak untuk tahu tentang orang tua.

Ada cara menyampaikan pesan dari anak ke orang tua melalui acara. Dengan cara ini orang tua, setelah menambahkan, cukup mendaftar ke acara yang dipicu anak, tanpa anak harus tahu tentang orang tua secara langsung. Lagipula, ini kemungkinan adalah penggunaan yang dimaksudkan dari seorang anak yang mengetahui tentang orang tuanya, agar dapat menggunakan orang tuanya untuk beberapa efek. Kecuali Anda tidak ingin anak melakukan pekerjaan orang tua, jadi sebenarnya yang ingin Anda lakukan hanyalah memberi tahu orang tua bahwa sesuatu telah terjadi. Karena itu, apa yang perlu Anda tangani adalah peristiwa pada anak, yang dapat dimanfaatkan orang tua.

Pola ini juga berskala sangat baik jika acara ini bermanfaat bagi kelas-kelas lain. Mungkin ini sedikit berlebihan, tetapi juga mencegah Anda menembak diri sendiri nanti, karena tergoda untuk ingin menggunakan orang tua di kelas anak Anda yang hanya menyandingkan dua kelas bahkan lebih. Refactoring kelas-kelas semacam itu sesudahnya memakan waktu dan dapat dengan mudah membuat bug di program Anda.

Semoga itu bisa membantu!

Neil
sumber
6
Menjauhlah dari setiap skenario yang mengharuskan anak untuk tahu tentang orang tua ” - mengapa? Jawaban Anda bergantung pada asumsi bahwa grafik objek lingkaran adalah ide yang buruk. Meskipun ini kadang-kadang terjadi (misalnya ketika mengelola memori melalui penghitungan ulang yang naif - bukan kasus di C #), itu bukan hal yang buruk secara umum. Khususnya, Pola Pengamat (yang sering digunakan untuk mengirimkan peristiwa) melibatkan yang diamati ( Child) mempertahankan seperangkat pengamat ( Parent), yang memperkenalkan kembali sirkularitas dengan cara mundur (dan memperkenalkan sejumlah masalah sendiri).
amon
1
Karena dependensi siklik berarti menyusun kode Anda sedemikian rupa sehingga Anda tidak dapat memilikinya tanpa yang lain. Berdasarkan sifat hubungan orangtua-anak, mereka haruslah entitas yang terpisah atau Anda berisiko memiliki dua kelas yang digabungkan secara ketat yang mungkin juga merupakan kelas raksasa tunggal dengan daftar untuk semua perawatan cermat yang dimasukkan ke dalam desainnya. Saya gagal melihat bagaimana pola Observer sama dengan orangtua-anak selain fakta bahwa satu kelas memiliki referensi ke beberapa yang lain. Bagi saya orangtua-anak adalah orangtua yang memiliki ketergantungan kuat pada anak, tetapi bukan kebalikannya.
Neil
Saya setuju. Acara adalah cara terbaik untuk menangani hubungan orangtua-anak dalam kasus ini. Ini adalah pola yang saya gunakan sangat sering, dan itu membuat kode sangat mudah untuk dipelihara, daripada harus khawatir tentang apa yang dilakukan kelas anak kepada orang tua melalui referensi.
Eternal21
@ Neil: Ketergantungan bersama dari instance objek membentuk bagian alami dari banyak model data. Dalam simulasi mekanis mobil, bagian-bagian mobil yang berbeda perlu mengirimkan kekuatan satu sama lain; ini biasanya lebih baik ditangani dengan membuat semua bagian mobil menganggap mesin simulasi itu sendiri sebagai objek "induk", daripada dengan memiliki ketergantungan siklik di antara semua komponen, tetapi jika komponen mampu merespons rangsangan apa pun dari luar induk. mereka akan membutuhkan cara untuk memberi tahu orang tua jika rangsangan tersebut memiliki efek yang perlu diketahui orang tua.
supercat
2
@Neil: Jika domain menyertakan objek hutan dengan dependensi data siklik yang tidak dapat direduksi, model domain apa pun akan melakukannya juga. Dalam banyak kasus, ini akan lebih jauh menyiratkan bahwa hutan akan berperilaku sebagai objek raksasa tunggal apakah orang menginginkannya atau tidak . Pola agregat berfungsi untuk memusatkan kompleksitas hutan menjadi objek kelas tunggal yang disebut Root Agregat. Bergantung pada kompleksitas domain yang dimodelkan, bahwa agregat root mungkin menjadi agak besar dan sulit, tetapi jika kompleksitas tidak dapat dihindari (seperti halnya dengan beberapa domain) lebih baik ...
supercat
10

Saya pikir pilihan Anda 3 mungkin yang paling bersih. Kau menulis.

Kelemahan: Anak dibuat tanpa referensi orang tua.

Saya tidak melihat itu sebagai downside. Bahkan, desain program Anda dapat mengambil manfaat dari objek anak yang dapat dibuat tanpa orang tua terlebih dahulu. Misalnya, ini dapat membuat pengujian anak-anak dalam isolasi jauh lebih mudah. Jika pengguna model Anda lupa untuk menambahkan anak ke induknya dan memanggil metode yang mengharapkan properti induk diinisialisasi di kelas anak Anda, maka ia mendapat pengecualian ref kosong - yang persis apa yang Anda inginkan: crash awal untuk kesalahan pemakaian.

Dan jika Anda berpikir seorang anak membutuhkan atribut induknya untuk diinisialisasi dalam konstruktor dalam semua keadaan karena alasan teknis, gunakan sesuatu seperti "objek orangtua nol" sebagai nilai default (meskipun ini memiliki risiko kesalahan masking).

Doc Brown
sumber
Jika objek anak tidak dapat melakukan sesuatu yang berguna tanpa orangtua, memulai objek anak tanpa orangtua akan mengharuskannya untuk memiliki SetParentmetode yang baik perlu mendukung mengubah asal usul anak yang ada (yang mungkin sulit dilakukan dan / atau tidak masuk akal) atau hanya akan dipanggil sekali. Membuat model situasi seperti kelompok agregasi (seperti yang dikemukakan Mike Brown) bisa jauh lebih baik daripada memiliki anak yang mulai tanpa orangtua.
supercat
Jika use case membutuhkan sesuatu sekali saja, desain harus memungkinkannya selalu. Kemudian, membatasi kemampuan untuk keadaan khusus itu mudah. Namun menambahkan kemampuan seperti itu biasanya tidak mungkin. Solusi terbaik adalah Opsi 3. Mungkin dengan objek ketiga, "Hubungan" antara orang tua dan anak sehingga [Orang Tua] ---> [Hubungan (Orangtua memiliki Anak)] <--- [Anak]. Ini juga memungkinkan beberapa instance [Hubungan] seperti [Anak] ---> [Hubungan (Anak dimiliki oleh Orang Tua)] <--- [Orang Tua].
DocSalvager
3

Tidak ada yang mencegah kohesi tinggi antara dua kelas yang biasanya digunakan bersama-sama (mis. Orde dan LineItem umumnya akan saling referensi). Namun, dalam kasus ini, saya cenderung mematuhi aturan Desain Berbasis Domain dan memodelkannya sebagai Agregat dengan Orang Tua sebagai Agregat Root. Ini memberi tahu kita bahwa AR bertanggung jawab atas masa pakai semua objek dalam agregatnya.

Jadi akan seperti skenario empat Anda di mana orang tua memperlihatkan metode untuk membuat anak-anaknya menerima parameter yang diperlukan untuk menginisialisasi anak-anak dengan benar dan menambahkannya ke koleksi.

Michael Brown
sumber
1
Saya akan menggunakan definisi agregat yang sedikit lebih longgar, yang akan memungkinkan adanya referensi luar ke dalam bagian agregat selain dari akar, asalkan - dari sudut pandang pengamat luar - perilaku akan konsisten dengan setiap bagian dari agregat hanya memiliki referensi ke root dan tidak ke bagian lain. Menurut saya, kuncinya adalah bahwa setiap objek yang bisa berubah harus memiliki satu pemilik ; agregat adalah kumpulan objek yang semuanya dimiliki oleh objek tunggal ("akar agregat"), yang harus mengetahui semua referensi yang ada pada bagian-bagiannya.
supercat
3

Saya akan menyarankan memiliki objek "pabrik anak" yang dilewatkan ke metode induk yang menciptakan anak (menggunakan objek "pabrik anak"), melampirkannya, dan mengembalikan tampilan. Objek anak itu sendiri tidak akan pernah terpapar di luar orangtua. Pendekatan ini dapat bekerja dengan baik untuk hal-hal seperti simulasi. Dalam simulasi elektronik, satu objek "pabrik anak" mungkin mewakili spesifikasi untuk beberapa jenis transistor; yang lain mungkin mewakili spesifikasi untuk resistor; sirkuit yang membutuhkan dua transistor dan empat resistor dapat dibuat dengan kode seperti:

var q2N3904 = new TransistorSpec(TransistorType.NPN, 0.691, 40);
var idealResistor4K7 = new IdealResistorSpec(4700.0);
var idealResistor47K = new IdealResistorSpec(47000.0);

var Q1 = Circuit.AddComponent(q2N3904);
var Q2 = Circuit.AddComponent(q2N3904);
var R1 = Circuit.AddComponent(idealResistor4K7);
var R2 = Circuit.AddComponent(idealResistor4K7);
var R3 = Circuit.AddComponent(idealResistor47K);
var R4 = Circuit.AddComponent(idealResistor47K);

Perhatikan bahwa simulator tidak perlu mempertahankan referensi apa pun ke objek pencipta anak, dan AddComponenttidak akan mengembalikan referensi ke objek yang dibuat dan disimpan oleh simulator, melainkan objek yang mewakili tampilan. Jika AddComponentmetode ini generik, objek tampilan dapat menyertakan fungsi spesifik komponen tetapi tidak akan mengekspos anggota yang digunakan orang tua untuk mengelola lampiran.

supercat
sumber
2

Daftar hebat. Saya tidak tahu metode mana yang "terbaik" tetapi di sini ada satu untuk menemukan metode yang paling ekspresif.

Mulailah dengan kelas orangtua dan anak yang paling sederhana. Tulis kode Anda dengan itu. Setelah Anda melihat duplikasi kode yang dapat dinamai Anda memasukkannya ke dalam metode.

Mungkin Anda mengerti addChild(). Mungkin Anda mendapatkan sesuatu seperti addChildren(List<Child>)atau addChildrenNamed(List<String>)atau loadChildrenFrom(String)atau newTwins(String, String)atau Child.replicate(int).

Jika masalah Anda adalah tentang memaksakan hubungan satu-ke-banyak, mungkin Anda harus melakukannya

  • memaksakannya di setter yang dapat menyebabkan kebingungan atau klausa lemparan
  • Anda menghapus setter dan membuat salinan khusus atau memindahkan metode - yang ekspresif dan dapat dimengerti

Ini bukan jawaban tapi saya harap Anda menemukannya saat membaca ini.

Pengguna
sumber
0

Saya menghargai bahwa memiliki tautan dari anak ke orang tua memiliki kelemahan seperti yang disebutkan di atas.

Namun untuk banyak skenario, penyelesaian dengan peristiwa dan mekanisme "terputus" lainnya juga membawa kerumitan mereka sendiri dan garis kode tambahan.

Misalnya mengangkat suatu acara dari Anak untuk diterima oleh Orang Tua itu akan mengikat keduanya bersama-sama meskipun dalam mode kopling longgar.

Mungkin untuk banyak skenario, jelas bagi semua pengembang apa arti properti Child.Parent. Untuk sebagian besar sistem yang saya kerjakan ini bekerja dengan baik. Over engineering bisa memakan waktu dan…. Membingungkan!

Have a Parent.AttachChild () metode yang melakukan semua pekerjaan yang diperlukan untuk mengikat Anak ke orang tuanya. Semua orang jelas tentang apa artinya "ini"

Rax
sumber