views:

687

answers:

8

This is kind of hard to explain, I hope my English is sufficient:

I have a class "A" which should maintain a list of objects of class "B" (like a private List). A consumer of class "A" should be able to add items to the list. After the items are added to the list, the consumer should not be able to modify them again, left alone that he should not be able to temper with the list itself (add or remove items). But he should be able to enumerate the items in the list and get their values. Is there a pattern for it? How would you do that?

If the question is not clear enough, please let me know.

A: 

EDIT: Added support for edition contexts. Caller can only add elements inside an edition context. You can aditionally enforce that only one edition context can be created for the lifetime of the instance.


Using encapsulation you can define any set of policies to access the inner private member. The following example is a basic implementation of your requirements:

namespace ConsoleApplication2
{
    using System;
    using System.Collections.Generic;
    using System.Collections;

    class B
    {
    }

    interface IEditable
    {
        void StartEdit();
        void StopEdit();
    }

    class EditContext<T> : IDisposable where T : IEditable
    {
        private T parent;

        public EditContext(T parent)
        {
            parent.StartEdit();
            this.parent = parent;
        }

        public void Dispose()
        {
            this.parent.StopEdit();
        }
    }

    class A : IEnumerable<B>, IEditable
    {
        private List<B> _myList = new List<B>();
        private bool editable;

        public void Add(B o)
        {
            if (!editable)
            {
                throw new NotSupportedException();
            }
            _myList.Add(o);
        }

        public EditContext<A> ForEdition()
        {
            return new EditContext<A>(this);
        }

        public IEnumerator<B> GetEnumerator()
        {
            return _myList.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        public void StartEdit()
        {
            this.editable = true;
        }

        public void StopEdit()
        {
            this.editable = false;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            A a = new A();
            using (EditContext<A> edit = a.ForEdition())
            {
                a.Add(new B());
                a.Add(new B());
            }

            foreach (B o in a)
            {
                Console.WriteLine(o.GetType().ToString());
            }

            a.Add(new B());

            Console.ReadLine();
        }
    }
}
smink
This is a nice implementation, but I don't think access to mutators in o is controlled in your example for loop.
Seb Rose
I like the EditContext concept, but there is nothing here to stop A from being edited a second time, maybe during the foreach in the example above. Perhaps if you set editable=true in the constructor of A, and then remove that line from StartEdit?
Chris Ballard
A: 

You basically want to avoid to give away references to the class B items. That's why you should do a copy of the items.

I think this can be solved with the ToArray() method of a List object. You need to create a deep-copy of the list if you want to prevent changes.

Generally speaking: most of the times it is not worthwhile to do a copy to enforce good behaviour, especially when you also write the consumer.

Sklivvz
+3  A: 

To prevent editing the list or its items you have to make them immutable, which means you have to return a new instance of an element on every request.

See Eric Lippert's excellent series of "Immutability in C#": http://blogs.msdn.com/ericlippert/archive/tags/Immutability/C_2300_/default.aspx (you have to scroll down a bit)

VVS
Great link - thanks for that
Chris Ballard
The immutability Eric is talking about doesn't mean returning a new instance every time. He shows you how to write objects that, once they're constructed, never change. That means you can share the same instance everywhere and be assured that no one else will change that instance.
Neil Whitaker
A: 
public class MyList<T> : IEnumerable<T>{

    public MyList(IEnumerable<T> source){
        data.AddRange(source);
    }

    public IEnumerator<T> GetEnumerator(){
        return data.Enumerator();
    }

    private List<T> data = new List<T>();
}

The downside is that a consumer can modify the items it gets from the Enumerator, a solution is to make deepcopy of the private List<T>.

pb
A: 

It wasn't clear whether you also needed the B instances themselves to be immutable once added to the list. You can play a trick here by using a read-only interface for B, and only exposing these through the list.

internal class B : IB
{
    private string someData;

    public string SomeData
    {
        get { return someData; }
        set { someData = value; }
    }
}

public interface IB
{
    string SomeData { get; }
}
Chris Ballard
This is fine as far as it goes, but the caller can cast any retrieved IB interface to B at will, thus allowing them to use the setter.
Seb Rose
It depends on the setup. You can often prevent this by changing the visibility of the class, as in the above example. Anyway, at least this implementation gives you a hint that you aren't supposed to update the objects.
Chris Ballard
A: 

The simplest that I can think of is return a readonly version of the underlying collection if editing is no longer allowed.

public IList ListOfB
{
    get 
    {
        if (_readOnlyMode) 
            return listOfB.AsReadOnly(); // also use ArrayList.ReadOnly(listOfB);
        else
            return listOfB;
    }
}

Personally though, I would not expose the underlying list to the client and just provide methods for adding, removing, and enumerating the B instances.

jop
This doesn't achieve the requirement that "After the items are added to the list, the consumer should not be able to modify them again". The new list contains *references* to the items in the original list and users can call mutators at will.
Seb Rose
I better use your ProxyB class then. :)
jop
+1  A: 

Wow, there are some overly complex answers here for a simple problem.

Have a private List<T>

Have an public void AddItem(T item) method - whenever you decide to make that stop working, make it stop working. You could throw an exception or you could just make it fail silently. Depends on what you got going on over there.

Have a public T[] GetItems() method that does return _theList.ToArray()

Matt Hinze
LOL! I agree, I did the self same thing with a private List<string> and public string[] on some work I was doing last night for this very reason :)
Rob Cooper
This doesn't achieve the requirement that "After the items are added to the list, the consumer should not be able to modify them again". The array contains *references* to the items in the list and users can call mutators at will.
Seb Rose
I agree with Seb Rose: if you don't want the elements of the list to be editable, you have to prevent to return the original reference -> immutability.
VVS
+1  A: 

As many of these answers show, there are many ways to make the collection itself immutable.

It takes more effort to keep the members of the collection immutable. One possibility is to use a facade/proxy (sorry for the lack of brevity):

class B
{
    public B(int data) 
    { 
        this.data = data; 
    }

    public int data
    {
        get { return privateData; }
        set { privateData = value; }
    }

    private int privateData;
}

class ProxyB
{
    public ProxyB(B b)   
    { 
        actual = b; 
    }

    public int data
    {
        get { return actual.data; }
    }

    private B actual;
}

class A : IEnumerable<ProxyB>
{
    private List<B> bList = new List<B>();

    class ProxyEnumerator : IEnumerator<ProxyB>
    {
        private IEnumerator<B> b_enum;

        public ProxyEnumerator(IEnumerator<B> benum)
        {
            b_enum = benum;
        }

        public bool MoveNext()
        {
            return b_enum.MoveNext();
        }

        public ProxyB Current
        {
            get { return new ProxyB(b_enum.Current); }
        }

        Object IEnumerator.Current
        {
            get { return this.Current; }
        }

        public void Reset()
        {
            b_enum.Reset();
        }

        public void Dispose()
        {
            b_enum.Dispose();
        }
    }

    public void AddB(B b) { bList.Add(b); }

    public IEnumerator<ProxyB> GetEnumerator()
    {
        return new ProxyEnumerator(bList.GetEnumerator());
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

The downside of this solution is that the caller will be iterating over a collection of ProxyB objects, rather than the B objects they added.

Seb Rose