รูปแบบสำหรับการเผยแพร่การเปลี่ยนแปลงแบบจำลองวัตถุ .. ?


22

นี่เป็นสถานการณ์ทั่วไปที่ทำให้ฉันต้องจัดการ

ฉันมีโมเดลวัตถุที่มีวัตถุแม่ พาเรนต์มีวัตถุย่อยบางอย่าง บางสิ่งเช่นนี้

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

แต่ละวัตถุลูกมีข้อมูลและวิธีการต่าง ๆ

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

    public void MakeMess()
    {
        ...
    }
}

เมื่อลูกเปลี่ยนแปลงในกรณีนี้เมื่อเรียกเมธอด MakeMess ค่าบางอย่างในพาเรนต์จะต้องได้รับการอัปเดต สมมติว่าเมื่อขีด จำกัด ของ Animal ได้สร้างความยุ่งเหยิงดังนั้นธง IsDirty ของ Zoo จึงจำเป็นต้องตั้งค่า

มีสองสามวิธีในการจัดการสถานการณ์นี้ (ที่ฉันรู้)

1)สัตว์แต่ละตัวสามารถมีการอ้างอิงของ Zoo หลักเพื่อสื่อสารการเปลี่ยนแปลง

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

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

สิ่งนี้ให้ความรู้สึกว่าเป็นตัวเลือกที่เลวร้ายที่สุดเพราะมันจับคู่สัตว์กับวัตถุหลัก ถ้าฉันต้องการสัตว์ที่อาศัยอยู่ในบ้านล่ะ

2)ตัวเลือกอื่นหากคุณใช้ภาษาที่สนับสนุนกิจกรรม (เช่น C #) คือให้ผู้ปกครองสมัครสมาชิกเพื่อเปลี่ยนกิจกรรม

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

ดูเหมือนว่าจะทำงานได้ แต่จากประสบการณ์ยากที่จะรักษา หากสัตว์มีการเปลี่ยนแปลงของสวนสัตว์คุณต้องยกเลิกการเป็นสมาชิกที่สวนสัตว์เก่าและสมัครใหม่ที่สวนสัตว์ใหม่ สิ่งนี้จะยิ่งแย่ลงเมื่อทรีองค์ประกอบมีความลึกมากขึ้น

3)ตัวเลือกอื่นคือการย้ายลอจิกถึงแม่

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

ดูเหมือนว่าผิดธรรมชาติมากและทำให้เกิดความซ้ำซ้อนของตรรกะ ตัวอย่างเช่นถ้าฉันมีวัตถุเฮ้าส์ที่ไม่ได้แชร์มรดกทั่วไปกับ Zoo ..

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

ฉันยังไม่พบกลยุทธ์ที่ดีในการรับมือกับสถานการณ์เหล่านี้ มีอะไรอีกบ้าง? สิ่งนี้จะทำให้ง่ายขึ้นได้อย่างไร


คุณพูดถูก # 1 ไม่ดีและฉันก็ไม่กระตือรือร้นที่ # 2 เช่นกัน โดยทั่วไปคุณต้องการหลีกเลี่ยงผลข้างเคียงและคุณจะเพิ่มผลข้างเคียงแทน เกี่ยวกับตัวเลือก # 3 ทำไมคุณไม่แยก AnimalMakeMess เป็นวิธีคงที่ที่ทุกคลาสสามารถโทรได้
Doval

4
# 1 ไม่จำเป็นต้องแย่ถ้าคุณสื่อสารผ่านอินเทอร์เฟซ (IAnimalObserver) แทนคลาสแม่ที่เฉพาะเจาะจงนั้น
coredump

คำตอบ:


11

ฉันต้องจัดการกับเรื่องนี้สองสามครั้ง ครั้งแรกที่ฉันใช้ตัวเลือก 2 (เหตุการณ์) และตามที่คุณบอกว่ามันซับซ้อนจริงๆ ถ้าคุณไปเส้นทางนั้นฉันขอแนะนำให้คุณต้องทดสอบหน่วยอย่างละเอียดเพื่อให้แน่ใจว่าเหตุการณ์นั้นทำได้อย่างถูกต้องและคุณไม่ได้ทิ้งเอกสารอ้างอิงห้อยมิฉะนั้นมันจะเจ็บปวดมากในการแก้ปัญหา

ครั้งที่สองผมก็ดำเนินการสถานที่ให้บริการผู้ปกครองเป็นหน้าที่ของเด็กเพื่อให้Dirtyทรัพย์สินในสัตว์แต่ละและให้ผลตอบแทนAnimal.IsDirty this.Animals.Any(x => x.IsDirty)นั่นคือในรูปแบบ เหนือโมเดลมีคอนโทรลเลอร์และงานของคอนโทรลเลอร์คือรู้ว่าหลังจากฉันเปลี่ยนโมเดล (การกระทำทั้งหมดในโมเดลถูกส่งผ่านคอนโทรลเลอร์ดังนั้นจึงรู้ว่ามีบางสิ่งเปลี่ยนไป) จากนั้นก็รู้ว่าต้องโทรหาบางอย่าง ฟังก์ชั่นการประเมินค่าเช่นเรียกZooMaintenanceแผนกเพื่อตรวจสอบว่าZooสกปรกอีกครั้ง อีกทางหนึ่งฉันสามารถกดZooMaintenanceเช็คเอาท์จนกว่าจะถึงกำหนดเวลาในภายหลัง (ทุก ๆ 100 มิลลิวินาที, 1 วินาที, 2 นาที, 24 ชั่วโมงไม่ว่าอะไรก็ตามที่จำเป็น)

ฉันพบว่าหลังนั้นง่ายกว่ามากในการบำรุงรักษาและความกลัวของปัญหาด้านประสิทธิภาพไม่เคยเกิดขึ้นมาก่อน

แก้ไข

อีกวิธีในการจัดการกับสิ่งนี้คือรูปแบบMessage Bus แทนที่จะใช้สิ่งที่Controllerชอบในตัวอย่างของฉันคุณฉีดทุกสิ่งด้วยIMessageBusบริการ Animalระดับแล้วสามารถเผยแพร่ข้อความเช่น "Mess ทำ" ของคุณและZooระดับสามารถสมัครสมาชิกเพื่อข้อความ "Mess ทำ" บริการบัสข้อความจะดูแลการแจ้งเตือนZooเมื่อสัตว์ใดเผยแพร่ข้อความใดข้อความหนึ่งและสามารถประเมินIsDirtyทรัพย์สินของมันได้อีกครั้ง

นี่เป็นข้อได้เปรียบที่Animalsไม่จำเป็นต้องมีZooการอ้างอิงอีกต่อไปและZooไม่จำเป็นต้องกังวลเกี่ยวกับการสมัครและยกเลิกการเป็นสมาชิกจากเหตุการณ์จากทุกAnimalๆ บทลงโทษคือทุกZooชั้นเรียนที่สมัครรับข่าวสารนั้นจะต้องประเมินคุณสมบัติของพวกเขาอีกครั้งแม้ว่ามันจะไม่ใช่สัตว์ นั่นอาจจะใช่หรือไม่ใช่เรื่องใหญ่ หากมีเพียงหนึ่งหรือสองZooกรณีก็อาจเป็นไปได้

แก้ไข 2

อย่าลดความเรียบง่ายของตัวเลือก 1. ใครก็ตามที่มาถึงโค้ดจะไม่มีปัญหาในการทำความเข้าใจ จะเห็นได้ชัดว่ามีคนกำลังดูAnimalชั้นเรียนว่าเมื่อใดที่ MakeMessมีการเรียกว่ามันแพร่กระจายข้อความไปZooยังZooชั้นเรียนและจะมีความชัดเจนต่อชั้นเรียนที่ได้รับข้อความจาก โปรดจำไว้ว่าในการเขียนโปรแกรมเชิงวัตถุการเรียกใช้เมธอดจะเรียกว่า "ข้อความ" ในความเป็นจริงเพียงครั้งเดียวที่เหมาะสมที่จะหยุดพักจากตัวเลือกที่ 1 คือถ้ามากกว่าZooนั้นจะต้องได้รับการแจ้งเตือนหากผู้คนAnimalทำเลอะเทอะ หากมีวัตถุอื่น ๆ ที่จำเป็นต้องได้รับการแจ้งเตือนฉันอาจจะย้ายไปที่บัสข้อความหรือคอนโทรลเลอร์


5

ฉันสร้างไดอะแกรมคลาสอย่างง่ายที่อธิบายโดเมนของคุณ: ป้อนคำอธิบายรูปภาพที่นี่

แต่ละคนAnimal มีความ Habitatยุ่งเหยิง

สัตว์Habitatไม่สนใจว่าจะมีสัตว์จำนวนเท่าใดหรือมี (ยกเว้นว่ามันเป็นส่วนหนึ่งของการออกแบบของคุณซึ่งในกรณีนี้คุณไม่ได้อธิบาย)

แต่การAnimalดูแลนั้นเพราะมันจะมีพฤติกรรมที่แตกต่างกันในทุกHabitat

แผนภาพนี้คล้ายกับ UML Diagram ของรูปแบบการออกแบบกลยุทธ์แต่เราจะใช้มันแตกต่างกัน

ต่อไปนี้เป็นตัวอย่างโค้ดใน Java (ฉันไม่ต้องการทำข้อผิดพลาดเฉพาะ C #)

แน่นอนคุณสามารถปรับแต่งการออกแบบภาษาและความต้องการของคุณเอง

นี่คือส่วนต่อประสานกลยุทธ์:

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

    public float getCleanliness();
}

Habitatตัวอย่างของคอนกรีต แน่นอนแต่ละHabitatคลาสย่อยสามารถใช้วิธีการเหล่านี้แตกต่างกัน

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

    public float getCleanliness() {
        return cleanliness;
    }

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

แน่นอนว่าคุณสามารถมีคลาสย่อยของสัตว์หลายตัวซึ่งแต่ละตัวจะยุ่งเหยิงกัน:

public class Animel {
    private Habitat habitat;

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

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

นี่คือคลาสไคลเอนต์ซึ่งโดยทั่วไปอธิบายถึงวิธีการใช้การออกแบบนี้

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

แน่นอนในใบสมัครจริงของคุณคุณสามารถแจ้งให้HabitatทราบและจัดการAnimalหากคุณต้องการ


3

ฉันประสบความสำเร็จในระดับพอใช้กับสถาปัตยกรรมเช่นตัวเลือกของคุณ 2 ในอดีต มันเป็นตัวเลือกที่ธรรมดาที่สุดและจะให้ความยืดหยุ่นสูงสุด แต่ถ้าคุณสามารถควบคุมผู้ฟังของคุณและไม่ได้จัดการประเภทการสมัครสมาชิกจำนวนมากคุณสามารถสมัครสมาชิกกิจกรรมได้ง่ายขึ้นด้วยการสร้างอินเทอร์เฟซ

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

class MessEvent
{
  String DetailsOrWhatever;
}

ตัวเลือกที่อินเตอร์เฟซที่มีความได้เปรียบของการเป็นเกือบเป็นง่ายๆเป็นตัวเลือกที่ 1 แต่ยังช่วยให้คุณสัตว์บ้านสวยได้อย่างง่ายดายในหรือHouseFairlyLand


3
  • ตัวเลือก 1 ค่อนข้างตรงไปตรงมา นั่นเป็นเพียงการอ้างอิงย้อนกลับ แต่พูดคุยกับมันด้วยอินเทอร์เฟซที่เรียกว่าDwellingและให้MakeMessวิธีการกับมัน ที่แบ่งการพึ่งพาแบบวงกลม จากนั้นเมื่อสัตว์ทำให้รกมันก็จะเรียกdwelling.MakeMess()เช่นกัน

ในจิตวิญญาณของlex parsimoniaeฉันจะไปกับอันนี้แม้ว่าฉันอาจจะใช้วิธีการแก้ปัญหาด้านล่างรู้ว่าฉัน (นี่เป็นเพียงแบบเดียวกับที่ @Benjamin Albert แนะนำ)

โปรดทราบว่าหากคุณกำลังสร้างแบบจำลองตารางฐานข้อมูลเชิงสัมพันธ์ความสัมพันธ์จะเป็นไปในทางอื่น: สัตว์จะมีการอ้างอิงถึงสวนสัตว์และชุดสัตว์สำหรับสวนสัตว์จะเป็นผลลัพธ์แบบสอบถาม

  • เมื่อนำแนวคิดนั้นไปให้ไกลคุณสามารถใช้สถาปัตยกรรมที่ถูกโยงโซ่ได้ นั่นคือการสร้างอินเตอร์เฟซMessableและในแต่ละรายการ messable nextรวมถึงการอ้างอิงถึง หลังจากสร้างระเบียบโทรMakeMessในรายการถัดไป

สวนสัตว์ที่นี่มีส่วนเกี่ยวข้องในการทำอาหารเพราะมันยุ่งเกินไป มี:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

ดังนั้นตอนนี้คุณมีสิ่งที่ได้รับข้อความที่สร้างความยุ่งเหยิง

  • ตัวเลือกที่ 2 รูปแบบการเผยแพร่ / สมัครสมาชิกสามารถทำงานได้ที่นี่ แต่รู้สึกหนักมาก วัตถุและภาชนะบรรจุมีความสัมพันธ์ที่รู้จักกันดีดังนั้นจึงดูเหมือนว่ามือหนัก ๆ จะใช้สิ่งทั่วไปมากกว่านั้น

  • ตัวเลือก 3: ในกรณีนี้การโทรZoo.MakeMess(animal)หรือHouse.MakeMess(animal)ไม่ใช่ตัวเลือกที่ไม่ดีจริง ๆ เพราะบ้านอาจมีความหมายที่แตกต่างกันสำหรับการยุ่งเหยิงกว่าสวนสัตว์

แม้ว่าคุณจะไม่ได้ไปตามเส้นทางลูกโซ่ แต่ดูเหมือนว่ามีสองประเด็นที่นี่: 1) ปัญหาเกี่ยวกับการเผยแพร่การเปลี่ยนแปลงจากวัตถุไปยังคอนเทนเนอร์ของมัน 2) ดูเหมือนว่าคุณต้องการแยกอินเทอร์เฟซสำหรับ ภาชนะบรรจุนามธรรมที่สัตว์สามารถมีชีวิตอยู่

...

หากคุณมีฟังก์ชั่นชั้นหนึ่งคุณสามารถส่งผ่านฟังก์ชั่น (หรือผู้รับมอบสิทธิ์) ไปยัง Animal เพื่อโทรหลังจากที่มันยุ่งเหยิง มันเหมือนกับแนวคิดลูกโซ่เล็กน้อยยกเว้นฟังก์ชั่นแทนที่จะเป็นอินเตอร์เฟส

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

เมื่อสัตว์เคลื่อนไหวเพียงแค่ตั้งตัวแทนใหม่

  • คุณสามารถใช้ Aspect Oriented Programming (AOP) กับคำแนะนำ "after" ใน MakeMess

2

ฉันจะไปกับ 1 แต่ฉันจะสร้างความสัมพันธ์พ่อแม่และลูกพร้อมกับตรรกะการแจ้งเตือนเป็นเสื้อคลุมแยก สิ่งนี้จะลบการพึ่งพาของสัตว์ในสวนสัตว์และช่วยให้การจัดการอัตโนมัติของความสัมพันธ์พ่อแม่และลูก แต่สิ่งนี้ต้องการให้คุณสร้างวัตถุในลำดับชั้นลงในคลาสของอินเตอร์เฟส / นามธรรมก่อนและเขียน wrapper เฉพาะสำหรับแต่ละอินเตอร์เฟส แต่สามารถลบออกได้โดยใช้การสร้างรหัส

สิ่งที่ต้องการ :

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

นี่เป็นวิธีที่ ORM บางรายการทำการติดตามการเปลี่ยนแปลงของพวกเขาในเอนทิตี พวกเขาสร้าง wrappers รอบเอนทิตีและทำให้คุณทำงานกับสิ่งเหล่านั้น wrappers เหล่านั้นมักจะทำโดยใช้การสะท้อนและการสร้างรหัสแบบไดนามิก


1

สองตัวเลือกที่ฉันมักใช้ คุณสามารถใช้วิธีที่สองและวางตรรกะสำหรับการเดินสายเหตุการณ์ในคอลเลกชันเองบนพาเรนต์

วิธีการอื่น (ซึ่งสามารถใช้กับตัวเลือกใดก็ได้จากสามตัวเลือก) คือการใช้การบรรจุ สร้าง AnimalContainer (หรือทำให้เป็นชุดสะสม) ที่สามารถอาศัยอยู่ในบ้านหรือสวนสัตว์หรืออะไรก็ได้ มันมีฟังก์ชั่นการติดตามที่เกี่ยวข้องกับสัตว์ แต่หลีกเลี่ยงปัญหาการสืบทอดเนื่องจากสามารถรวมไว้ในวัตถุใด ๆ ที่ต้องการ


0

คุณเริ่มต้นด้วยความล้มเหลวพื้นฐาน: วัตถุลูกไม่ควรรู้เกี่ยวกับพ่อแม่ของพวกเขา

สตริงรู้หรือไม่ว่าพวกมันอยู่ในรายการ ไม่วันที่ทราบว่ามีอยู่ในปฏิทินหรือไม่ เลขที่

ตัวเลือกที่ดีที่สุดคือเปลี่ยนการออกแบบของคุณเพื่อที่ว่าสถานการณ์จะไม่เกิดขึ้น

หลังจากนั้นให้พิจารณาการผกผันของการควบคุม แทนที่จะMakeMessเปิดAnimalด้วยเอฟเฟกต์หรือเหตุการณ์ให้ส่งผ่านZooเมธอด ตัวเลือกที่ 1 นั้นใช้ได้ถ้าคุณต้องการปกป้องค่าคงที่ที่Animalต้องอยู่ที่ไหนสักแห่งเสมอ มันไม่ได้เป็นผู้ปกครอง แต่เป็นการเชื่อมโยงเพียร์

บางครั้ง 2 และ 3 จะไม่เป็นไร แต่หลักการทางสถาปัตยกรรมที่สำคัญที่ต้องติดตามคือเด็ก ๆ ไม่ทราบเกี่ยวกับผู้ปกครอง


ฉันสงสัยว่านี่เป็นเหมือนปุ่มส่งในรูปแบบมากกว่าสตริงในรายการ
svidgen

1
@svidgen - จากนั้นส่งผ่านการเรียกกลับ เข้าใจผิดได้มากกว่าเหตุการณ์ง่ายกว่าที่จะให้เหตุผลและไม่มีการอ้างอิงถึงสิ่งที่มันไม่จำเป็นต้องรู้ซน
Telastyn
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.