传播模式改变了对象模型。


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的某个阈值变得混乱时,则需要设置Zoo的IsDirty标志。

有几种处理这种情况的方法(我知道)。

1)每个动物都可以有一个父Zoo参考,以便交流更改。

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

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

这似乎是最糟糕的选择,因为它将Animal耦合到其父对象。如果我想要一个住在房子里的动物怎么办?

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共享任何公共继承父级的House对象。

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

我尚未找到处理这些情况的好策略。还有什么可用的?如何使它更简单?


您认为#1不好是正确的,我也不热衷于#2。通常,您要避免副作用,而要增加副作用。关于选项3,为什么不能将AnimalMakeMess分解为所有类都可以调用的静态方法?
2014年

4
如果通过接口(IAnimalObserver)而不是特定的Parent类进行通信,则#1不一定很糟糕。
coredump 2014年

Answers:


11

我不得不处理几次。我第一次使用选项2(事件),正如您所说的那样,它变得非常复杂。如果您走这条路,我强烈建议您需要非常彻底的单元测试,以确保事件正确完成,并且您不会留下悬挂的引用,否则调试起来会非常痛苦。

第二次,我只是将父级属性实现为子级函数,因此Dirty在每个动物上都保留一个属性,然后让Animal.IsDirtyreturn this.Animals.Any(x => x.IsDirty)。那是在模型中。在模型上方有一个控制器,控制器的工作是知道我更改模型后(模型上的所有动作都通过控制器传递,因此它知道某些更改),然后知道必须调用特定的评估功能,例如触发ZooMaintenance部门检查是否Zoo再次弄脏。或者,我可以将ZooMaintenance检查推迟到计划的某个较晚的时间(每100 ms,1秒,2分钟,24小时,必要时进行)。

我发现后者的维护要简单得多,而且我对性能问题的恐惧从未实现。

编辑

解决此问题的另一种方法是消息总线模式。Controller您无需在我的示例中使用“赞”,而是为每个对象注入IMessageBus服务。然后,Animal班级可以发布消息,例如“ Mess Made”,您的Zoo班级可以订阅“ Mess Made”消息。消息总线服务将负责通知Zoo任何动物何时发布这些消息之一,并且可以重新评估其IsDirty属性。

这样做的好处是,Animals不再需要Zoo引用,Zoo也不必担心订阅和取消订阅每个事件Animal。惩罚是,所有Zoo订阅该消息的类都必须重新评估其属性,即使它不是其动物之一。这可能不重要。如果只有一个或两个Zoo实例,那可能很好。

编辑2

不要忽视选项1的简单性。任何重新访问代码的人都不会在理解它方面遇到很多问题。对于查看Animal该类的人来说很明显,当 MakeMess它被调用时,它将消息传播到,Zoo并且对于从中Zoo获取消息的类也很明显。请记住,在面向对象的编程中,方法调用曾经被称为“消息”。实际上,唯一可以选择退出选项1的时间是,如果发生混乱Zoo,不仅要通知选项Animal。如果还有更多需要通知的对象,那么我可能会转到消息总线或控制器。


5

我制作了一个简单的类图来描述您的域: 在此处输入图片说明

每个Animal 都有一个 Habitat混乱。

Habitat不关心什么,或者有多少动物,它有(除非它是从根本上,在这种情况下,你描述它是不是你设计的一部分)。

但是Animal确实很重要,因为它的行为各不相同Habitat

该图类似于策略设计模式的UML图,但是我们将以不同的方式使用它。

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

当然,在实际的应用程序中HabitatAnimal如果需要,您可以告知并管理。


3

过去,您在选择2之类的架构上取得了相当大的成功。这是最通用的选项,将提供最大的灵活性。但是,如果您可以控制侦听器并且不管理很多订阅类型,则可以通过创建界面来更轻松地订阅事件。

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

class MessEvent
{
  String DetailsOrWhatever;
}

界面选项的优点几乎和选项1一样简单,而且还允许您轻松地将动物放置在House或中FairlyLand


3
  • 选项1实际上非常简单。那只是一个反向引用。但是可以通过调用的接口对其进行概括Dwelling并提供一种MakeMess方法。这打破了循环依赖。然后,当动物弄得一团糟时,它dwelling.MakeMess()也会发出声音。

本着lex parsimoniae的精神,尽管我很了解我,但我很可能会使用下面的链式解决方案,所以我将继续介绍它。(这与@Benjamin Albert建议的模型相同。)

请注意,如果要对关系数据库表进行建模,则该关系将采用另一种方式:Animal将引用Zoo,而Zoo的Animals集合将是查询结果。

  • 更进一步地讲,您可以使用链式架构。也就是说,创建一个接口Messable,并在每个混乱的项目中都包含对的引用next。造成混乱之后,请呼叫MakeMess下一个项目。

因此,这里的Zoo会造成混乱,因为它也会变得混乱。有:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

因此,现在您有了一连串的信息,可以得到已创建混乱的信息。

  • 选项2,可以在这里使用发布/订阅模型,但是感觉确实很重。对象和容器之间的关系是已知的,因此使用比这更笼统的东西似乎有些笨拙。

  • 选项3:在这种特殊情况下,调用Zoo.MakeMess(animal)or House.MakeMess(animal)实际上不是一个糟糕的选择,因为一所房子可能变得比Zoo更具有凌乱的语义。

即使您没有沿链路线走,听起来这里也有两个问题:1)问题是关于从对象到容器的更改传播; 2)听起来您想为提取动物可以居住的地方的容器。

...

如果您具有一流的功能,则可以在动物混乱之后将一个函数(或委托)传递给Animal进行调用。这有点像链的想法,只是用函数而不是接口。

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

当动物移动时,只需设置一个新的代表。

  • 更极端的是,您可以将面向方面的编程(AOP)与MakeMess上的“之后”建议一起使用。

2

我将使用1,但我会将父子关系与通知逻辑一起放入单独的包装器中。这消除了Animal对Zoo的依赖,并允许自动管理父子关系。但这需要您首先将层次结构中的对象重新制作为interfaces / abstract类,并为每个接口编写一个特定的包装器。但是可以使用代码生成将其删除。

就像是 :

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对实体进行更改跟踪的方式。它们围绕实体创建包装器,并让您使用它们。这些包装通常使用反射和动态代码生成来制作。


1

我经常使用的两个选项。您可以使用第二种方法,并将用于连接事件的逻辑放在父级的集合本身中。

另一种方法(实际上可以与这三个选项中的任何一个一起使用)是使用遏制。制作一个可以在房屋,动物园或其他任何地方生活的AnimalContainer(甚至使其成为收藏)。它提供了与动物相关联的跟踪功能,但避免了继承问题,因为可以将其包含在需要它的任何对象中。


0

您从一个基本的失败开始:子对象不应该知道他们的父母。

字符串知道它们在列表中吗?不。日期是否知道日历中存在日期?没有。

最好的选择是更改设计,以使这种情况不存在。

之后,考虑控制反转。代替MakeMessAnimal具有副作用或事件,通Zoo入方法。如果您需要保护Animal始终需要居住在某个地方的不变量,则选项1很好。它不是父母,而是同伴关联。

有时候2和3会没事的,但是遵循的主要建筑原则是孩子对父母一无所知。


我怀疑这更像表单中的提交按钮,而不是列表中的字符串。
svidgen

1
@svidgen-然后传入回调。比事件更简单,更容易推理,并且对它不需要知道的东西没有任何顽皮的引用。
Telastyn 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.