Pola untuk menyebarkan perubahan pada model objek ..?

22

Berikut adalah skenario umum yang selalu membuat saya frustrasi untuk berurusan.

Saya memiliki model objek dengan objek induk. Induk berisi beberapa objek anak. Sesuatu seperti ini.

public class Zoo
{
    public List<Animal> Animals { get; set; }
    public bool IsDirty { get; set; }
}

Setiap objek anak memiliki berbagai data dan metode

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void MakeMess()
    {
        ...
    }
}

Ketika anak berubah, dalam hal ini ketika metode MakeMess dipanggil, beberapa nilai dalam orangtua perlu diperbarui. Katakanlah ketika ambang Hewan tertentu telah membuat kekacauan, maka bendera IsDirty Kebun Binatang perlu ditetapkan.

Ada beberapa cara untuk menangani skenario ini (yang saya tahu).

1) Setiap Hewan dapat memiliki referensi Kebun Binatang induk untuk mengkomunikasikan perubahan.

public class Animal
{
    public Zoo Parent { get; set; }
    ...

    public void MakeMess()
    {
        Parent.OnAnimalMadeMess();
    }
}

Ini terasa seperti pilihan terburuk karena ia memasangkan Animal ke objek induknya. Bagaimana jika saya menginginkan binatang yang tinggal di rumah?

2) Pilihan lain, jika Anda menggunakan bahasa yang mendukung acara (seperti C #) adalah membuat orang tua berlangganan untuk mengubah acara.

public class Animal
{
    public event OnMakeMessDelegate OnMakeMess;

    public void MakeMess()
    {
        OnMakeMess();
    }
}

public class Zoo
{
    ...

    public void SubscribeToChanges()
    {
        foreach (var animal in Animals)
        {
            animal.OnMakeMess += new OnMakeMessDelegate(OnMakeMessHandler);
        }
    }

    public void OnMakeMessHandler(object sender, EventArgs e)
    {
        ...
    }
}

Ini tampaknya berhasil tetapi dari pengalaman semakin sulit untuk dipertahankan. Jika Hewan pernah mengubah Kebun Binatang, Anda harus berhenti berlangganan acara di Kebun Binatang lama dan berlangganan di Kebun Binatang baru. Ini hanya menjadi lebih buruk karena pohon komposisi semakin dalam.

3) Pilihan lainnya adalah memindahkan logika ke induk.

public class Zoo
{
    public void AnimalMakesMess(Animal animal)
    {
        ...
    }
}

Ini tampaknya sangat tidak wajar dan menyebabkan duplikasi logika. Misalnya, jika saya memiliki objek House yang tidak membagikan warisan pewarisan bersama dengan Zoo ..

public class House
{
    // Now I have to duplicate this logic
    public void AnimalMakesMess(Animal animal)
    {
        ...
    }
}

Saya belum menemukan strategi yang baik untuk menghadapi situasi ini. Apa lagi yang tersedia? Bagaimana ini bisa dibuat lebih sederhana?

ConditionRacer
sumber
Anda benar tentang # 1 menjadi buruk, dan saya juga tidak tertarik pada # 2; umumnya Anda ingin menghindari efek samping, dan justru meningkatkannya. Mengenai opsi # 3, mengapa Anda tidak bisa memasukkan AnimalMakeMess menjadi metode statis yang dapat dipanggil oleh semua kelas?
Doval
4
# 1 tidak terlalu buruk jika Anda berkomunikasi melalui antarmuka (IAnimalObserver) alih-alih kelas Induk tertentu.
coredump

Jawaban:

11

Saya harus berurusan dengan ini beberapa kali. Pertama kali saya menggunakan opsi 2 (peristiwa) dan seperti yang Anda katakan itu menjadi sangat rumit. Jika Anda menggunakan rute itu, saya sangat menyarankan Anda perlu unit test yang sangat teliti untuk memastikan acara tersebut dilakukan dengan benar dan Anda tidak meninggalkan referensi yang menggantung, jika tidak, akan sangat sulit untuk melakukan debug.

Kedua kalinya, saya baru saja menerapkan properti induk sebagai fungsi anak-anak, jadi simpan Dirtyproperti pada setiap hewan, dan biarkan Animal.IsDirtykembali this.Animals.Any(x => x.IsDirty). Itu ada dalam model. Di atas model ada Pengendali, dan tugas pengontrol adalah untuk mengetahui bahwa setelah saya mengubah model (semua tindakan pada model dilewatkan melalui pengontrol sehingga tahu bahwa ada sesuatu yang berubah), maka ia tahu harus memanggil kembali -Fungsi evaluasi, seperti memicu ZooMaintenancedepartemen untuk memeriksa apakah Zookotor lagi. Atau saya bisa menekan ZooMaintenancecek sampai beberapa waktu kemudian dijadwalkan (setiap 100 ms, 1 detik, 2 menit, 24 jam, apa pun yang diperlukan).

Saya menemukan bahwa yang terakhir jauh lebih mudah untuk dipertahankan, dan ketakutan saya terhadap masalah kinerja tidak pernah terwujud.

Edit

Cara lain untuk mengatasi ini adalah pola Bus Pesan . Daripada menggunakan Controllerseperti dalam contoh saya, Anda menyuntikkan setiap objek dengan IMessageBuslayanan. The Animalkelas maka dapat mempublikasikan pesan, seperti "Mess Terbuat" dan Anda Zookelas dapat berlangganan pesan "Mess Terbuat". Layanan bus pesan akan memberitahukan Zookapan ada hewan yang mempublikasikan salah satu pesan itu, dan ia dapat mengevaluasi kembali IsDirtypropertinya.

Ini memiliki keuntungan yang Animalstidak lagi membutuhkan Zooreferensi, dan Zootidak perlu khawatir tentang berlangganan dan berhenti berlangganan dari setiap acara Animal. Hukumannya adalah bahwa semua Zookelas yang berlangganan pesan tersebut harus mengevaluasi kembali properti mereka, bahkan jika itu bukan salah satu dari hewan-hewannya. Itu mungkin atau mungkin bukan masalah besar. Jika hanya ada satu atau dua Zoocontoh, itu mungkin baik-baik saja.

Edit 2

Jangan mengabaikan kesederhanaan opsi 1. Siapa pun yang mengunjungi kembali kode tidak akan kesulitan memahaminya. Ini akan menjadi jelas bagi seseorang yang melihat Animalkelas bahwa ketika MakeMessdipanggil akan menyebarkan pesan ke kelas Zoodan itu akan menjadi jelas bagi Zookelas dari mana pesan itu berasal. Ingat bahwa dalam pemrograman berorientasi objek, panggilan metode dulu disebut "pesan". Bahkan, satu-satunya waktu yang masuk akal untuk keluar dari opsi 1 adalah jika lebih dari sekadar Zooharus diberitahukan jika Animalmembuat berantakan. Jika ada lebih banyak objek yang perlu diberitahukan, maka saya mungkin akan pindah ke bus pesan atau pengontrol.

Scott Whitlock
sumber
5

Saya telah membuat diagram kelas sederhana yang menggambarkan domain Anda: masukkan deskripsi gambar di sini

Masing-masing Animal memiliki Habitat itu kacau.

Tidak Habitatpeduli apa atau berapa banyak hewan yang dimilikinya (kecuali jika secara fundamental bagian dari desain Anda yang dalam hal ini Anda uraikan bukan).

Tetapi yang Animalpeduli, karena akan berperilaku berbeda di setiap Habitat.

Diagram ini mirip dengan Diagram UML dari pola desain strategi , tetapi kami akan menggunakannya secara berbeda.

Berikut adalah beberapa contoh kode di Java (Saya tidak ingin membuat kesalahan spesifik C #).

Tentu saja Anda dapat membuat tweak sendiri untuk desain, bahasa, dan persyaratan ini.

Ini adalah antarmuka Strategi:

public interface Habitat {
    public void messUp(float magnitude);

    public float getCleanliness();
}

Contoh beton Habitat. tentu saja setiap Habitatsubclass dapat mengimplementasikan metode ini secara berbeda.

public class Zoo implements Habitat {
    public float cleanliness = 1;

    public float getCleanliness() {
        return cleanliness;
    }

    public void messUp(float magnitude) {
        cleanliness -= magnitude;
    }
}

Tentu saja Anda dapat memiliki beberapa subkelas hewan, di mana masing-masing mengacaukannya secara berbeda:

public class Animel {
    private Habitat habitat;

    public void makeMess() {
        habitat.messUp(.05f);
    }

    public Animel addTo(Habitat habitat) {
        this.habitat = habitat;
        return this;
    }
}

Ini adalah kelas klien, ini pada dasarnya menjelaskan bagaimana Anda dapat menggunakan desain ini.

public class ZooKeeper {
    public Habitat zoo = new Zoo();

    public ZooKeeper() {
        new Animal()
            .addTo( zoo )
            .makeMess();

        if (zoo.getCleanliness() < 0.5f) {
            System.out.println("The zoo is really messy");
        } else {
            System.out.println("The zoo looks clean");
        }
    }
}

Tentu saja dalam aplikasi nyata Anda, Anda dapat memberi Habitattahu dan mengelola Animaljika Anda membutuhkannya.

Benjamin Albert
sumber
3

Saya sudah cukup sukses dengan arsitektur seperti pilihan Anda 2 di masa lalu. Ini adalah opsi paling umum dan akan memberikan fleksibilitas terbesar. Tetapi, jika Anda memiliki kendali atas pendengar Anda dan tidak mengelola banyak jenis langganan, Anda dapat berlangganan acara dengan lebih mudah dengan membuat antarmuka.

interface MessablePlace
{
  void OnMess(object sender, MessEvent e);
}

class MessEvent
{
  String DetailsOrWhatever;
}

Opsi antarmuka memiliki keuntungan yang hampir sesederhana opsi 1 Anda, tetapi juga memungkinkan Anda untuk menempatkan hewan dengan mudah di dalam Houseatau FairlyLand.

svidgen
sumber
3
  • Opsi 1 sebenarnya cukup mudah. Itu hanya referensi kembali. Tetapi menggeneralisasikannya dengan antarmuka yang disebut Dwellingdan menyediakan MakeMessmetode di atasnya. Itu memutus ketergantungan sirkuler. Lalu ketika hewan itu membuat berantakan, ia dwelling.MakeMess()juga memanggil .

Dalam semangat lex parsimoniae , saya akan memilih yang ini, walaupun saya mungkin akan menggunakan solusi rantai di bawah ini, mengenal saya. (Ini hanya model yang sama dengan yang disarankan @Benjamin Albert.)

Perhatikan bahwa jika Anda memodelkan tabel basis data relasional, relasinya akan menuju ke arah lain: Hewan akan memiliki referensi ke Kebun Binatang, dan koleksi Hewan untuk Kebun Binatang akan menjadi hasil permintaan.

  • Mengambil gagasan itu lebih jauh, Anda bisa menggunakan arsitektur berantai. Yaitu, buat antarmuka Messable, dan di setiap item yang bisa dikirim, sertakan referensi next. Setelah membuat kekacauan, panggil MakeMessitem berikutnya.

Jadi Zoo di sini terlibat dalam membuat kekacauan, karena menjadi berantakan juga. Memiliki:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

Jadi sekarang Anda memiliki rantai hal-hal yang mendapatkan pesan bahwa kekacauan telah dibuat.

  • Opsi 2, model terbitkan / berlangganan dapat bekerja di sini, tetapi terasa sangat berat. Objek dan wadah memiliki hubungan yang diketahui, sehingga tampaknya sedikit berat menggunakan sesuatu yang lebih umum dari itu.

  • Opsi 3: Dalam kasus khusus ini, menelepon Zoo.MakeMess(animal)atau House.MakeMess(animal)sebenarnya bukan pilihan yang buruk, karena sebuah rumah mungkin memiliki semantik berbeda untuk menjadi berantakan daripada Kebun Binatang.

Bahkan jika Anda tidak melalui rute rantai, sepertinya ada dua masalah di sini: 1) masalahnya adalah tentang menyebarkan perubahan dari suatu objek ke wadahnya, 2) Kedengarannya seperti Anda ingin mematikan antarmuka untuk wadah untuk abstrak di mana binatang bisa hidup.

...

Jika Anda memiliki fungsi kelas satu, Anda dapat meneruskan suatu fungsi (atau mendelegasikan) ke Animal untuk dipanggil setelah itu membuat kekacauan. Itu sedikit seperti ide berantai, kecuali dengan fungsi bukan antarmuka.

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

Saat hewan bergerak, cukup tentukan delegasi baru.

  • Dibawa ke ekstrem, Anda bisa menggunakan Pemrograman Berorientasi Aspek (AOP) dengan saran "setelah" pada MakeMess.
rampok
sumber
2

Saya akan menggunakan 1, tapi saya akan membuat hubungan orangtua-anak bersama dengan notifikasi logika menjadi bungkus terpisah. Ini menghilangkan ketergantungan Animal on Zoo dan memungkinkan manajemen otomatis hubungan orangtua-anak. Tapi ini mengharuskan Anda untuk membuat ulang objek dalam hierarki menjadi antarmuka / kelas abstrak terlebih dahulu dan menulis pembungkus spesifik untuk setiap antarmuka. Tapi itu bisa dihapus menggunakan pembuatan kode.

Sesuatu seperti :

public interface IAnimal
{
    string Name { get; set; }
    int Age { get; set; }

    void MakeMess();
}

public class Animal : IAnimal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void MakeMess()
    {
        // makes mess
    }
}

public class ZooAnimals
{
    class AnimalInZoo : IAnimal
    {
        public IAnimal _animal;
        public ZooAnimals _zoo;

        public AnimalInZoo(IAnimal animal, ZooAnimals zoo)
        {
            _animal = animal;
            _zoo = zoo;
        }

        public string Name { get { return _animal.Name; } set { _animal.Name = value; } }
        public int Age { get { return _animal.Age; } set { _animal.Age = value; } }

        public void MakeMess()
        {
            _animal.MakeMess();
            _zoo.IsDirty = true;
        }
    }

    private Collection<AnimalInZoo> animals = new Collection<AnimalInZoo>();

    public IAnimal Add(IAnimal animal)
    {
        if (animal is AnimalInZoo)
        {
            var inZoo = (AnimalInZoo)animal;
            if (inZoo._zoo != this)
            {
                // animal is in a different zoo, what to do ?
                // either move animal to this zoo
                // or throw an exception so caller is forced to remove the animal from previous zoo first
            }
        }

        var anim = new AnimalInZoo(animal, this);
        animals.Add(anim);
        return anim;
    }

    public IAnimal Remove(IAnimal animal)
    {
        if (!(animal is AnimalInZoo))
        {
            // animal is not in zoo, throw an exception?
        }
        var inZoo = (AnimalInZoo)animal;
        if (inZoo._zoo != this)
        {
            // animal is in a different zoo, throw an exception?
        }

        animals.Remove(inZoo);
        return inZoo._animal;
    }

    public bool IsDirty { get; set; }
}

Ini sebenarnya bagaimana beberapa ORM melakukan pelacakan perubahan pada entitas. Mereka membuat pembungkus di sekitar entitas dan membuat Anda bekerja dengan itu. Pembungkus tersebut biasanya dibuat menggunakan refleksi dan pembuatan kode dinamis.

Euforia
sumber
1

Dua opsi yang sering saya gunakan. Anda dapat menggunakan pendekatan kedua dan menempatkan logika untuk memasang acara di koleksi itu sendiri pada induknya.

Pendekatan alternatif (yang sebenarnya dapat digunakan dengan salah satu dari tiga opsi) adalah dengan menggunakan penahanan. Buat AnimalContainer (atau bahkan koleksi) yang dapat tinggal di rumah atau kebun binatang atau apa pun. Ini menyediakan fungsionalitas pelacakan yang terkait dengan hewan, tetapi menghindari masalah warisan karena dapat dimasukkan dalam objek apa pun yang membutuhkannya.

AJ Henderson
sumber
0

Anda mulai dengan kegagalan dasar: objek anak tidak seharusnya tahu tentang orang tua mereka.

Apakah string tahu bahwa mereka ada dalam daftar? Tidak. Apakah tanggal tahu mereka ada di kalender? Tidak.

Pilihan terbaik adalah mengubah desain Anda sehingga skenario semacam ini tidak ada.

Setelah daripada, pertimbangkan inversi kontrol. Alih-alih MakeMesspada Animaldengan efek samping atau acara, lulus Zooke metode. Opsi 1 baik-baik saja jika Anda perlu melindungi invarian yang Animalselalu perlu tinggal di suatu tempat. Itu bukan orangtua, tetapi asosiasi teman sebaya.

Kadang-kadang 2 dan 3 akan berjalan dengan baik, tetapi prinsip arsitektur utama yang harus diikuti adalah bahwa anak-anak tidak tahu tentang orang tua mereka.

Telastyn
sumber
Saya menduga ini lebih seperti tombol kirim dalam bentuk daripada string dalam daftar.
svidgen
1
@viden - Lalu kirimkan panggilan balik. Lebih mudah daripada acara, lebih mudah untuk dipikirkan, dan tidak ada referensi nakal untuk hal-hal yang tidak perlu diketahui.
Telastyn