vendredi 18 septembre 2015

Propagating changes in one child object to another

I have came across several cases where I have a parent object with multiple objects, and where changes to information in one child object affects others.

For example, consider the following case:

interface IUniverse
{
    IStorage ShirtStorage;
    IList<IHuman> Humans;
}

Interface IStorage:
{
    string Location;
    int Capacity;
    IList<IShirt> Items;
}

Interface IHuman
{
    string Name;
    int Age;
    IList<IShirt> Shirts;
}

I would like to remove a particular shirt from ShirtStorage in my universe, but at the same time, since the shirt is removed from existence, it should be removed from all humans as well.

I have thought of 3 ways to do this:


Firstly, we can introduce Add(IClothing) and Remove(IClothing) methods to IStorage<T> and IHuman.
interface IUniverse
{
    IStorage ShirtStorage;
    IList<IHuman> Humans;
}

Interface IStorage
{
    string Location;
    int Capacity;
    IList<IShirt> Items;
    **void Add(IShirt);**
    **void Remove(IShirt);**
}

Interface IHuman
{
    string Name;
    int Age;
    IList<IShirt> Shirts;
    **void AddShirts(IShirt);**
    **void RemoveShirts(IShirts);**
}

Afterwards, the implementations of above interfaces will not have anything under the hood which removes a particular shirt from all humans when it is removed from ShirtStorage.

The disadvantage of this design is that each time a programmer removes a shirt from the universe, he will have to manually remove every single reference from each human.

That is to say, the programmer has to know the exact structure of the universe in order to remove a single shirt. In the event where the structure of the universe becomes highly complex, such that references to a particular shirt may appear more than just in IHuman, this may prove to be erroneous and tedious.


Secondly, we similarly introduce Add(IShirt) and Remove(IShirt) methods to the interfaces IStorage and IHuman:

interface IUniverse
{
    IStorage<IShirt> ShirtStorage;
    IList<IHuman> Humans;
}

Interface IStorage
{
    string Location;
    int Capacity;
    IList<IShirt> Items;
    **void Add(IShirt);**
    **void Remove(IShirt);**
}

Interface IHuman
{
    string Name;
    int Age;
    IList<ICShirt> Shirts;
    **void AddShirt(IShirt);**
    **void RemoveShirt(IShirt);**
}

.. however this time round, we use an implementation of the above interfaces such that there is some notification going on under the hood. That is to say,

class Storage : IStorage
{
    IUniverse parentUniverse;
    string Location;
    int Capacity;
    IList<IShirt> Items;

    // ... Implementation for Add(IShirt item) is trivial

    void Remove(IShirt item)
    {
        this.Items.Add(item);
        foreach (IHuman human in this.parentUniverse)
            foreach(IClothing clothing in human.Clothings)
                if (clothing == item)
                    human.RemoveClothing(clothing);
    }
}

Indeed, by placing all the notification in the implementation of the interface, consumers of the interface will not have to go through every single possible reference to a particular IShirt when he wants to remove it from existence, thus making it better in this sense as compared to the previous solution.

However, the disadvantage is that such a design inherently leads to pathological lying, and violates the Single Responsibility Principle as well. If the programmer calls Remove(IShirt) on ShirtStorage, he wouldn't be aware of what reference is being removed from where.

If said programmer wishes to write a GUI using the Mediator pattern for example, he would be unsure of which notification message to send out.

Which humans exactly have shirts removed from them, thereby requiring an update on the GUI for some component which reflects the list of shirts belonging to a particular human? What if I have a Catalog class with names of all the shirts - wouldn't the entry corresponding to removed shirt be removed as well (under the hood)? Would I also have to update the corresponding GUI component for my catalogs?


Thirdly, we introduce the Add(IShirt) and Remove(IShirt) methods to IUniverse instead:

interface IUniverse
{
    IStorage ShirtStorage;
    IList<IHuman> Humans;
    void Add(IShirt);
    void Remove(IShirt);
}

Interface IStorage:
{
    string Location;
    int Capacity;
    IList<IShirt> Items;
}

Interface IHuman
{
    string Name;
    int Age;
    IList<IShirt> Shirts;
}

By doing so, we force consumers of the interface to accept that removing a shirt affects not just the shirt storage, but other members of IUniverse as well.

However, the disadvantages are like those in the second solution. On top of that, IUniverse instances eventually become somewhat of a God Object. Every place where I need to remove a shirt from a universe, I have to have a reference to the universe.

If a particular GUI component simply wants to display information for a ShirtStorage and to allow for interaction with the storage (i.e. adding and removing of shirts), wouldn't this introduce some coupling between the GUI component and the Universe, when the only coupling that should exists is that of the GUI component and IStorage?


I have wrote several applications which have used a mix of all three solutions. Indeed some solutions seem better than others in different cases, but the inconsistencies are a pain because I almost always forgot to do certain things when switching from one design to another.

Aucun commentaire:

Enregistrer un commentaire