The approach you describe works fine if the “client” (interface consumer) and “server” (class provider) have a mutual agreement that:
- the client will be polite and will not try to use the details of the server implementation
- the server will be polite and will not mutate the object after the client has a link to it.
If you do not have a good working relationship between people writing a client and people writing a server, then everything becomes pear-shaped. A rude client can, of course, “discard” immutability by casting to a public configuration type. A rude server can produce an immutable representation, and then mutate an object when the client is less than that.
A good approach is to prevent the client from seeing the mutable type:
public interface IReadOnly { ... } public abstract class Frobber : IReadOnly { private Frobber() {} public class sealed FrobBuilder { private bool valid = true; private RealFrobber real = new RealFrobber(); public void Mutate(...) { if (!valid) throw ... } public IReadOnly Complete { valid = false; return real; } } private sealed class RealFrobber : Frobber { ... } }
Now, if you want to create and modify Frobber, you can create Frobber.FrobBuilder. When you finish your mutations, you call Complete and get a read-only interface. (And then the builder becomes invalid.) Since all the details of the mutation implementation are hidden in a closed nested class, you cannot "drop" the IReadOnly interface in RealFrobber, only for Frobber, which does not have public methods!
And the hostile client cannot create his own Frobber, because Frobber is abstract and has a private constructor. The only way to make Frobber is through the constructor.
Eric Lippert
source share