Yaymak için Desenler bir nesne modelini değiştirir ..?


22

İşte başa çıkmam için her zaman sinir bozucu olan ortak bir senaryo.

Üst nesneye sahip bir nesne modelim var. Ebeveyn bazı alt nesneler içeriyor. Bunun gibi bir şey.

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

Her alt nesnenin çeşitli veri ve yöntemleri vardır.

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

    public void MakeMess()
    {
        ...
    }
}

Çocuk değiştiğinde, bu durumda MakeMess yöntemi çağrıldığında, üst öğedeki bazı değerlerin güncellenmesi gerekir. Diyelim ki, belirli bir Hayvan eşiği bir karmaşa yaptığında, hayvanat bahçesinin IsDirty bayrağının ayarlanması gerekiyor.

Bu senaryo ile başa çıkmanın birkaç yolu var (bildiğim kadarıyla).

1) Her Hayvan, değişiklikleri iletmek için bir hayvanat bahçesi referansına sahip olabilir.

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

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

Bu, Animal'ı üst nesnesine bağladığı için en kötü seçenek gibi geliyor. Ya bir evde yaşayan bir hayvanı istersem?

2) Başka bir seçenek, etkinlikleri destekleyen bir dil kullanıyorsanız (C # gibi) ebeveynin etkinlikleri değiştirmek için abone olmasını sağlamaktır.

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)
    {
        ...
    }
}

Bu iş gibi görünüyor ama deneyimlerden elde etmek zorlaşıyor. Hayvanlar, Hayvanat Bahçesini değiştirirse, eski Hayvanat Bahçesindeki etkinlikleri iptal etmeli ve yeni Hayvanat Bahçesinde yeniden abone olmalısınız. Bu sadece kompozisyon ağacı derinleştikçe kötüleşir.

3) Diğer seçenek, mantığı üste taşımaktır.

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

Bu çok doğal görünmüyor ve mantığın tekrarlanmasına neden oluyor. Örneğin, herhangi bir miras ebeveyni Zoo ile paylaşmayan bir House nesnesi olsaydı ..

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

Bu durumlarla baş etmek için henüz iyi bir strateji bulamadım. Başka ne var? Bu nasıl daha basit hale getirilebilir?


# 1'in kötü olması konusunda haklısın ve ben de # 2'ye meraklı değilim; Genellikle yan etkilerden kaçınmak istersiniz ve bunun yerine onları arttırırsınız. Seçenek # 3 ile ilgili olarak neden AnimalMakeMess'i tüm sınıfların çağırabileceği statik bir metoda dahil edemiyorsunuz?
Doval,

4
Bu özel Üst sınıf yerine bir arabirim (IAnimalObserver) aracılığıyla iletişim kurarsanız # 1 mutlaka fena değildir.
coredump

Yanıtlar:


11

Bununla birkaç kez uğraşmak zorunda kaldım. İlk kez seçenek 2'yi kullandım (olaylar) ve dediğiniz gibi gerçekten karmaşıklaştı. Bu rotaya giderseniz, olayların doğru yapıldığından ve sarkan referanslardan ayrılmadığınızdan emin olmak için çok ayrıntılı birim testlerine ihtiyacınız olduğunu, aksi halde hata ayıklamanın gerçekten büyük bir acı olduğunu öneriyorum.

İkinci kez, ana mülkiyeti çocukların bir işlevi olarak uyguladım, bu yüzden Dirtyher hayvanın mülkünü tut ve Animal.IsDirtygeri dön this.Animals.Any(x => x.IsDirty). Bu modeldeydi. Modelin üzerinde bir Denetleyici vardı ve denetleyicinin görevi, modeli değiştirdikten sonra (modeldeki tüm eylemlerin denetleyiciden geçirildiğini ve böylece bir şeyin değiştiğini bildiğini) bilmesiydi; yeniden değerlendirme fonksiyonu, ZooMaintenancedepartmanın Zootekrar kirli olup olmadığını kontrol etmek için tetiklemek gibi . Alternatif olarak, ZooMaintenanceçekleri daha sonra belli bir zamana kadar (her 100 msn'de, 1 saniyede, 2 dakikada, 24 saatte bir gerekliyse) çekebilirim .

İkincisinin bakımı çok daha kolay olduğunu ve performans sorunlarından duyduğum korkuların hiçbir zaman gerçekleşemediğini gördüm.

Düzenle

Bununla başa çıkmanın başka bir yolu da Mesaj Veriyolu modelidir. ControllerBenim örneğimde benzerini kullanmak yerine , her nesneyi bir IMessageBushizmete enjekte ettiniz . AnimalSınıf ardından "Mess Malı" gibi bir mesaj yayınlayabilir ve Zoosınıf "Mess Yapımı" mesajı abone olabilirsiniz. Mesaj veri yolu servisi, Zooherhangi bir hayvanın bu mesajlardan birini ne zaman yayınladığını bildirmekle ilgilenir ve IsDirtymülkünü yeniden değerlendirebilir .

Bunun Animalsartık bir Zooreferansa Zooihtiyaç duymadığı ve her birinden etkinliklere abone olma veya abonelikten çıkma konusunda endişelenmenize gerek yok Animal. Ceza, bu Zoomesaja abone olan tüm sınıfların, hayvanlarından biri olmasa bile özelliklerini yeniden değerlendirmek zorunda kalacağıdır. Bu büyük bir anlaşma olabilir veya olmayabilir. Sadece bir veya iki Zooörnek varsa, muhtemelen iyidir.

Düzenle 2

Seçenek 1'in sadeliğini azaltmayın. Kodu tekrar ziyaret eden hiç kimsenin onu anlamada fazla bir sorunu olmaz. Bu gören bir kullanıcı için bariz olacak Animalsınıfına zaman o MakeMessbunun için mesaj kadar yayar olduğunu denir Zoove açıkça olacak Zooo içindeki mesajları alır sınıfında. Nesne yönelimli programlamada, kullanılan bir yöntem çağrısının "mesaj" olarak adlandırıldığını unutmayın. Aslında, seçenek 1'den ayrılmanın çok anlamlı olduğu tek şey, eğer bir karışıklık yaratırsa Zoo, sadece bildirilmekten daha fazlası olması gerektiğidir Animal. Bildirilmesi gereken daha fazla nesne olsaydı, muhtemelen bir mesaj yoluna ya da kontrol cihazına giderdim.


5

Etki alanınızı tanımlayan basit bir sınıf şeması hazırladım: görüntü tanımını buraya girin

Her Animal birinin bir Habitat işi vardır .

Habitat(Bu temelde bu durumda değil mi tarif Tasarımınızın parçası olmadığı sürece) içinde olanı veya kaç hayvan ilgilenmez.

Ama Animalumrunda, çünkü her birinde farklı davranacak Habitat.

Bu diyagram, strateji tasarım modelinin UML Diyagramına benzer , ancak farklı şekilde kullanacağız.

İşte Java'da bazı kod örnekleri (C # özel hata yapmak istemiyorum).

Tabii ki bu tasarım, dil ve gereksinimler için kendi ince ayarlarınızı yapabilirsiniz.

Bu Strateji arayüzüdür:

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

    public float getCleanliness();
}

Somut bir örnek Habitat. Elbette her bir Habitatalt sınıf bu yöntemleri farklı şekilde uygulayabilir.

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

    public float getCleanliness() {
        return cleanliness;
    }

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

Elbette, her birinin farklı şekilde karıştırdığı birden fazla hayvan alt sınıfına sahip olabilirsiniz:

public class Animel {
    private Habitat habitat;

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

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

Bu müşteri sınıfıdır, temel olarak bu tasarımı nasıl kullanabileceğinizi açıklar.

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");
        }
    }
}

Tabii ki gerçek uygulamanızda Habitat, Animalihtiyacınız olup olmadığını bilmenizi ve yönetmenizi sağlayabilirsiniz .


3

Daha önce 2. seçeneğiniz gibi mimarilerde oldukça başarılı oldum. Bu en genel seçenektir ve en yüksek esnekliğe izin verecektir. Ancak, dinleyicileriniz üzerinde denetiminiz varsa ve çok sayıda abonelik türü yönetemiyorsanız, bir arayüz oluşturarak etkinliklere daha kolay abone olabilirsiniz.

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

class MessEvent
{
  String DetailsOrWhatever;
}

Arabirim seçeneği, seçenek 1'iniz kadar basit olma avantajına sahiptir, ancak aynı zamanda hayvanları bir Houseveya daha fazla zahmetsizce barındırmanıza izin verir FairlyLand.


3
  • Seçenek 1 aslında oldukça basittir. Bu sadece bir geri referans. Ancak denilen arayüzle genelleştirin Dwellingve MakeMessüzerinde bir yöntem sağlayın . Bu döngüsel bağımlılığı kırar. Sonra hayvan bir karmaşa yaptığında, o da çağırır dwelling.MakeMess().

Lex Parsimoniae'nin ruhuyla , bununla devam edeceğim, muhtemelen aşağıdaki zincir çözümü kullanıp, beni tanıyorum. (Bu, @Benjamin Albert'in önerdiği modelin aynısıdır.)

İlişkisel veritabanı tablolarını modelliyorsanız, ilişkinin diğer yoldan gideceğini unutmayın: Hayvan, Hayvanat Bahçesine bir atıfta bulunacak ve Hayvanat Bahçesi için Hayvan toplama işleminin bir sorgu sonucu olacağını unutmayın.

  • Bu fikri daha ileri götürmek için zincirleme bir mimari kullanabilirsiniz. Diğer bir deyişle, bir arayüz oluşturun Messableve her mesajlaşılabilir maddede bir referans ekleyin next. Bir karışıklık oluşturduktan sonra MakeMess, bir sonraki öğeyi arayın .

Bu yüzden Zoo burada karışıklık yaratıyor, çünkü dağınıklık da oluyor. Var:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

Şimdi bir karmaşa yaratıldığını bildiren bir mesaj zinciri var.

  • Seçenek 2, yayınlama / abone olma modeli burada çalışabilir, ancak gerçekten ağır hissediyor. Nesne ve konteynerin bilinen bir ilişkisi var, bu yüzden ondan daha genel bir şey kullanmak biraz zor görünüyor.

  • Seçenek 3: Bu özel durumda, arama Zoo.MakeMess(animal)veya House.MakeMess(animal)aslında kötü bir seçenek değildir, çünkü bir evin bir hayvanat bahçesinden daha dağınık olması için farklı semantikleri olabilir.

Zincir yolunda ilerlemeseniz bile, burada iki sorun var gibi geliyor: 1) sorun bir nesneden kabına bir değişiklik yaymakla ilgili, 2) Bir arayüzü kesmek istediğiniz gibi geliyor Hayvanların yaşayabileceği soyut kap.

...

Birinci sınıf işlevleriniz varsa, bir karmaşa yaptıktan sonra aramak için bir işlevi (veya temsilci) Hayvan'a geçirebilirsiniz. Arayüz yerine bir fonksiyon dışında, zincir fikir gibi bir şey.

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

Hayvan hareket ettiğinde, yeni bir temsilci ayarlayın.

  • Aşırı ele alındığında, Aspect Oriented Programming'i (AOP) MakeMess'in "after" önerisiyle kullanabilirsiniz.

2

1 ile giderdim, ancak ebeveyn-çocuk ilişkisini bildirim mantığı ile ayrı bir paketleyiciye dönüştürürüm. Bu, Hayvanların Hayvanat Bahçesine bağımlılığını ortadan kaldırır ve ebeveyn-çocuk ilişkilerinin otomatik yönetimine izin verir. Ancak bu, hiyerarşideki nesneleri ilk önce arayüzlere / soyut sınıflara dönüştürmenizi ve her arayüz için özel bir sarıcı yazmanızı gerektirir. Ancak bu kod üretimi kullanılarak kaldırılabilir.

Gibi bir şey :

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; }
}

Bu aslında bazı ORM'lerin varlıklar üzerindeki değişim takibini nasıl yaptıklarıdır. Varlıkların etrafında sarıcılar yaratırlar ve onlarla çalışmanızı sağlarlar. Bu sarmalayıcılar genellikle yansıma ve dinamik kod oluşturma kullanılarak yapılır.


1

Sık kullandığım iki seçenek. İkinci yaklaşımı kullanabilir ve olayı koleksiyonun kendisinde ebeveyne bağlamak için mantığı kullanabilirsiniz.

Alternatif bir yaklaşım (aslında üç seçenekten herhangi biriyle birlikte kullanılabilir) çevreleme kullanmaktır. Evde ya da hayvanat bahçesinde ya da başka bir yerde yaşayabilecek bir AnimalContainer yapın (ya da bir koleksiyon haline getirin). Hayvanlarla ilişkili izleme işlevselliğini sağlar, ancak ihtiyacı olan herhangi bir nesneye dahil edilebildiğinden, miras sorunlarını önler.


0

Temel bir başarısızlıkla başlıyorsunuz: çocuk nesneler ebeveynleri hakkında bilgi sahibi olmamalı.

Dizeler listede olduklarını biliyor mu? Hayır. Tarihler bir takvimde olduklarını biliyor mu? Yok hayır.

En iyi seçenek, tasarımınızı bu tür senaryoların olmaması için değiştirmektir.

Bundan sonra, kontrolün tersini düşünün. Bunun yerine MakeMessilgili Animalbir yan etki ya da olay ile, geçiş Zooyönteme. AnimalHer zaman bir yerde yaşaması gereken değişmezleri korumanız gerekirse Seçenek 1 gayet iyi . Bu bir ebeveyn değil, o zaman akran birliği.

Bazen 2 ve 3 düzelecek, ancak takip edilecek temel mimari ilke, çocukların ebeveynleri hakkında bilgi sahibi olmamasıdır.


Bunun bir formdaki bir gönderme butonu gibi bir listedeki bir string'den daha fazla olduğundan şüpheleniyorum.
svidgen

1
@svidgen - Sonra bir geri arama iletin. Bir olaya göre daha aldatmasız, akla gelmesi daha kolay ve bilmediği şeylere yaramaz referanslar vermez.
Telastyn
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.