1

In some languages (e.g. C#) a class can only ever have 1 base class which seems like a problem for what I'm trying to do. I will give you an example of what i'm trying to do, hopefully this will make my problem clear.

I have got a lot of data objects, some of these data objects have an hierarchy, they can have a parent or a child. For these classes I created an abstract class which has 2 lists (1 of the parents and one of the children) it also has a few methods: AddChild(), AddParent(), RemoveChild(), RemoveParent().

quick example:

 Owner.AddChild(child):<br>
 Adds the child to the collection of childs of 'Owner'<br>
 It also adds 'Owner' to the collection of parents of 'child'.

To make sure all classes which have hierarchical components do this I created an abstract class which they all inherit so no one forgets to do it both ways.

I have also made a class for objects I want to update. This is an abstract class not necessarily for the update part but for the equals part since I need all these classes to overwrite the Equals(object obj) method and GetHashcode() method and you cannot define an override in an interface (at lest not in C#)

Now clearly I'm going at this all wrong, thus my question how do I design my classes so they do get the functionality they need (and share with other classes) without creating an abstract class.

3
  • The source of complexity seems to be that each object is responsible for adding and removing its own links to the rest of the topology. Have you considered moving the Add/Remove behaviour out to a 'collection' class instead, so that the 'data' objects do not need to know or care about Add/Remove/etc? For example, consider the way that List<T> or Dictionary<K,V> doesn't put any burden on the objects which you add/remove to those collections. Apr 10, 2017 at 12:44
  • @BenCottrell do you mean create a sperate class which contains the collection (A kind of list class) which handles the links between classes?
    – Vincent
    Apr 10, 2017 at 12:54
  • Yes, very similar to a collection - the interface for .NET collections are probably a useful source of inspiration for managing objects which are logically related to each other. If your Add/Remove methods belong to that separate collection class, then (If I understand your question correctly) you would no longer need to find ways of sharing that behaviour between your different stored objects, as those objects wouldn't 'care' about how to add/remove/etc. Apr 10, 2017 at 13:09

2 Answers 2

6

To make sure all classes which have hierarchical components do this I created an abstract class which they all inherit so no one forgets to do it both ways.

An interface would also "remind" developers to add those methods.

.. how do I design my classes so they do get the functionality they need (and share with other classes) without creating an abstract class.

If the amount of duplicative code is small or simple, use an interface and just force inheritors to write their own implementations. In your example, AddChild(), AddParent(), RemoveChild(), and RemoveParent() all seem pretty straightforward and simple to build.

But, if the implementations are difficult for you to repeat, you could provide an "inner" class (like TreeNode) for inheritors to wrap:

class DomNodeOrSomething<T> : ITreeNode
{
  private TreeNode data = new TreeNode<T>();

  void AddChild(T item)
  {
    data.AddChild(item);
  }

  void AddParent(T item)
  {
    data.AddParent(item);
  }

  void RemoveChild(T item)
  {
    data.RemoveChild(item);
  }

  void RemoveParent(T item)
  {
    data.RemoveParent(item);
  }
}

And, if that doesn't float your boat, use extension methods.

In the example from MS, a WordCount() method is added to the String interface. When this namespace is included, all String's will "automagically" have a .WordCount() method dangling off them.

namespace ExtensionMethods
{
    public static class MyExtensions
    {
        public static int WordCount(this String str)
        {
            return str.Split(new char[] { ' ', '.', '?' }, 
                             StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }   
}

I believe you can do the same sort of thing for any interface. Something like:

namespace ExtensionMethods
{
  public static class TreeNodeExtensions
  {
    public static void AddChild(this ITreeNode self, ITreeNode item)
    {
      self.children.add(item);
    }
  }
}

This would still require that the ITreeNode interface force inheritors to provide common properties that the extension methods can manipulate.

0

Typically when I need to implement such as design, I use generic template parameters in the base class, as well as in the collection classes, to specify the child type and/or the parent type. If you use type parameters, the common code in the base class can be reused regardless of the type of child or parent.

That said, usually it is advantageous to have a non-generic base class for either child, parent, or both, to simplify the design. You may still need to dynamically cast occasionally. Also, you probably still need interfaces, if the parent type can vary in many ways.

It is possible to do it entirely without base classes, using the wrapper techniques described in another answer, but I prefer the simplest solution. The code below uses an interface for the parent type, to allow its base class to vary.

public interface IParent<TChild>
    where TChild : class
{
    /// <summary>
    /// Invoked after a change occurs in a <see cref="ChildCollection{TParent, TChild}"/>.
    /// </summary>
    /// <param name="item">The relavent <typeparamref name="TChild"/>.</param>
    /// <param name="changeType">The type of change that occured.</param>
    void OnCollectionChanged(TChild item, CollectionChangeType changeType);
}

public class Model
{
    object _parent;

    protected Model() { }

    /// <summary>
    /// Raised when this <see cref="Model"/> changes.
    /// </summary>
    public event EventHandler Changed;

    internal object ObjectParent
    {
        get { return _parent; }
        set { _parent = value; }
    }

    /// <summary>
    /// Call this method when the <see cref="Model"/> has changed.
    /// Do not call if you use <see cref="Model.SetField{T}"/>:
    /// SetField calls OnChanged automatically.
    /// </summary>
    /// <remarks>
    /// If the parent is a <see cref="Model"/>, 
    /// <see cref="OnChildChanged"/> is called on the parent.
    /// </remarks>
    protected virtual void OnChanged()
    {
        Model modelParent = _parent as Model;
        if (modelParent != null)
            modelParent.OnChildChanged(this);
        if (Changed != null)
            Changed(this, EventArgs.Empty);
    }

    /// <summary>
    /// Called after a child <see cref="Model"/> has changed.
    /// </summary>
    /// <param name="item">The child object that changed.</param>
    protected internal virtual void OnChildChanged(Model item)
    {
        OnChanged();
    }

    /// <summary>
    /// Called after a collection of child <see cref="Model"/>s has changed.
    /// </summary>
    /// <param name="item">The relevant child object.</param>
    /// <param name="changeType">The type of change that occured.</param>
    protected internal virtual void OnCollectionChanged(Model item, CollectionChangeType changeType)
    {
        OnChanged();
    }

    /// <summary>
    /// Set the value of a field, calling <see cref="OnChanged"/> if the value changes.
    /// </summary>
    /// <typeparam name="T">Type of the field set.</typeparam>
    /// <param name="field">A reference to the field to be modified.</param>
    /// <param name="value"></param>
    /// <returns></returns>
    protected bool SetField<T>(ref T field, T value)
        where T : IEquatable<T>
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
            return false;
        field = value;
        OnChanged();
        return true;
    }
}

/// <summary>
/// Base class for a model (non-visual) object contained
/// in a <see cref="ChildCollection{TChild, TParent}"/>.
/// </summary>
/// <typeparam name="TParent">
/// Type of the object containing a <see cref="ChildCollection{TChild, TParent}"/>.
/// </typeparam>
public abstract class Child<TParent> : Model
    where TParent : class
{
    /// <summary>
    /// The object owning this <see cref="Child{TParent}"/>.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public TParent Parent
    {
        get { return (TParent)ObjectParent; }
    }

    /// <summary>Set the <see cref="Child{TParent}.Parent"/> property.</summary>
    /// <param name="parent">The new <see cref="Child{TParent}.Parent"/> value.</param>
    protected internal virtual void Attach(TParent parent)
    {
        Check.ArgumentNotNull(parent, "parent");
        Check.Operation(ObjectParent == null, "Already added to another collection.");
        ObjectParent = parent;
    }

    /// <summary>
    /// Set the <see cref="Child{TParent}.Parent"/> property to <see langword="null"/>.
    /// </summary>
    protected internal virtual void Detach()
    {
        Check.Operation(ObjectParent != null);
        ObjectParent = null;
    }
}

/// <summary>
/// A collection of <see cref="Child{TParent}"/> objects.
/// </summary>
/// <typeparam name="TChild">The type of the child object.</typeparam>
/// <typeparam name="TParent">The type of the parent object containing the collection.</typeparam>
public abstract class ChildCollection<TChild, TParent> : ICollection<TChild>
    where TChild : Child<TParent>
    where TParent : class
{
    readonly TParent _parent;
    readonly List<TChild> _items;

    /// <summary>
    /// Initialize a new instance of the <see cref="ChildCollection{TChild, TParent}"/> class.
    /// </summary>
    /// <param name="parent">The parent of the new collection.</param>
    protected ChildCollection(TParent parent)
    {
        Check.ArgumentNotNull(parent, "parent");
        _parent = parent;
        _items = new List<TChild>();
    }

    /// <summary>
    /// The number of items in this <see cref="ChildCollection{TChild, TParent}"/>.
    /// </summary>
    public int Count { get { return _items.Count; } }

    /// <summary>
    /// Get the <typeparamref name="TChild"/> at the specified index.
    /// </summary>
    /// <param name="index">Index of a <typeparamref name="TChild"/>.</param>
    /// <returns>The <typeparamref name="TChild"/> at <paramref name="index"/>.</returns>
    public virtual TChild this[int index]
    {
        get { return _items[index]; }
    }

    /// <summary>
    /// Is this <see cref="ChildCollection{TChild, TParent}"/> read-only?
    /// Always returns false.
    /// </summary>
    public bool IsReadOnly { get { return false; } }

    /// <summary>
    /// Add the specified <typeparamref name="TChild"/> to this 
    /// <see cref="ChildCollection{TChild, TParent}"/>.
    /// </summary>
    /// <param name="item">The <typeparamref name="TChild"/> to be added.</param>
    public void Add(TChild item)
    {
        Check.ArgumentNotNull(item, "item");
        AddCore(item);
        OnChanged(item, CollectionChangeType.Added);
    }

    /// <summary>
    /// Add all <typeparamref name="TChild"/> objects in the specified collection
    /// to this <see cref="ChildCollection{TChild, TParent}"/>.
    /// </summary>
    /// <param name="items">The <typeparamref name="TChild"/> objects to be added.</param>
    public void AddRange(IEnumerable<TChild> items)
    {
        Check.ArgumentNotNull(items, "items");
        foreach (TChild item in items)
            AddCore(item);
        OnChanged(null, CollectionChangeType.Added);
    }

    void AddCore(TChild item)
    {
        if (Contains(item))
            Remove(item);
        item.Attach(_parent);
        _items.Add(item);
    }

    /// <summary>
    /// Is the specified <typeparamref name="TChild"/> contained in this 
    /// <see cref="ChildCollection{TChild, TParent}"/>?
    /// </summary>
    /// <param name="item">The <typeparamref name="TChild"/> to be found.</param>
    /// <returns>
    /// <see langword="true"/> if <paramref name="item"/> is contained in this
    /// <see cref="ChildCollection{TChild, TParent}"/>; otherwise, <see langword="false"/>.
    /// </returns>
    public bool Contains(TChild item)
    {
        return item.Parent == _parent;
    }

    /// <summary>
    /// Returns the index of the specified <typeparamref name="TChild"/> in this 
    /// <see cref="ChildCollection{TChild, TParent}"/>.
    /// </summary>
    /// <param name="item">The <typeparamref name="TChild"/> whose index is returned.</param>
    /// <returns>
    /// The index of <paramref name="item"/>, if found; -1 if not found. 
    /// </returns>
    public int IndexOf(TChild item)
    {
        return Contains(item) ? _items.IndexOf(item) : -1;
    }

    /// <summary>
    /// Remove the specified <typeparamref name="TChild"/> from this 
    /// <see cref="ChildCollection{TChild, TParent}"/>.
    /// </summary>
    /// <param name="item">The <typeparamref name="TChild"/> to be removed.</param>
    /// <returns>
    /// <see langword="true"/> if <paramref name="item"/> was removed;
    /// <see langword="false"/> if <paramref name="item"/> is not contained
    /// in this <see cref="ChildCollection{TChild, TParent}"/>.
    /// </returns>
    public bool Remove(TChild item)
    {
        Check.ArgumentNotNull(item, "item");
        int index = IndexOf(item);
        if (index < 0)
            return false;
        RemoveAt(index);
        return true;
    }

    /// <summary>
    /// Remove the item at the specified index.
    /// </summary>
    /// <param name="index">The index of the item to be removed.</param>
    public virtual void RemoveAt(int index)
    {
        TChild item = _items[index];
        item.Detach();
        _items.RemoveAt(index);
        OnChanged(item, CollectionChangeType.Removed);
        if (_items.Count == 0)
            OnChanged(null, CollectionChangeType.AllRemoved);
    }

    /// <summary>
    /// Insert a <typeparamref name="TChild"/>
    /// after a different <typeparamref name="TChild"/> in this 
    /// <see cref="ChildCollection{TChild, TParent}"/>.
    /// </summary>
    /// <param name="afterItem">
    /// A <typeparamref name="TChild"/> already in this <see cref="ChildCollection{TChild, TParent}"/>.
    /// </param>
    /// <param name="item">The <typeparamref name="TChild"/> to be inserted.</param>
    public void InsertAfter(TChild afterItem, TChild item)
    {
        int index = 0;
        if (afterItem == null)
            index = 1;
        else
            index = _items.IndexOf(afterItem) + 1;
        Insert(index, item);
    }

    /// <summary>
    /// Insert the specified <typeparamref name="TChild"/> at the specified index.
    /// </summary>
    /// <param name="index">
    /// Zero-based index at which <paramref name="item"/> is to be inserted.
    /// </param>
    /// <param name="item">The <typeparamref name="TChild"/> to be inserted.</param>
    public void Insert(int index, TChild item)
    {
        Check.ArgumentNotNull(item, "item");
        int previousIndex = _items.IndexOf(item);
        if (previousIndex == index)
            return;
        if (previousIndex >= 0)
        {
            _items.RemoveAt(previousIndex);
            if (index > previousIndex)
                --index;
            _items.Insert(index, item);
        }
        else
        {
            _items.Insert(index, item);
            item.Attach(_parent);
        }
        OnChanged(item, CollectionChangeType.Inserted);
    }

    /// <summary>
    /// Remove all <typeparamref name="TChild"/> objects from this
    /// <see cref="ChildCollection{TChild, TParent}"/>.
    /// </summary>
    public void Clear()
    {
        if (Count == 0)
            return;
        foreach (TChild item in _items)
            item.Detach();
        _items.Clear();
        OnChanged(null, CollectionChangeType.AllRemoved);
    }

    /// <summary>
    /// Copy all object references from this
    /// <see cref="ChildCollection{TChild, TParent}"/> to the specified array.
    /// </summary>
    /// <param name="array">Destination where object references are copied.</param>
    /// <param name="arrayIndex">Index in <paramref name="array"/> where copying begins.</param>
    public void CopyTo(TChild[] array, int arrayIndex)
    {
        _items.CopyTo(array, arrayIndex);
    }

    /// <summary>
    /// Get an enumerator suitable for enumerating all <typeparamref name="TChild"/> objects
    /// in this <see cref="ChildCollection{TChild, TParent}"/>.
    /// </summary>
    /// <returns>An enumerator.</returns>
    public IEnumerator<TChild> GetEnumerator()
    {
        return _items.GetEnumerator();
    }

    void OnChanged(TChild item, CollectionChangeType changeType)
    {
        IParent<TChild> parentParent = _parent as IParent<TChild>;
        if (parentParent != null)
            parentParent.OnCollectionChanged(item, changeType);
        Model modelParent = _parent as Model;
        if (modelParent != null)
            modelParent.OnCollectionChanged(item, changeType);
    }
}   

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.