Search code examples
c#type-safety

Return an opaque object to the caller without violating type-safety


I have a method which should return a snapshot of the current state, and another method which restores that state.

public class MachineModel
{
    public Snapshot CurrentSnapshot { get; }
    public void RestoreSnapshot (Snapshot saved) { /* etc */ };
}

The state Snapshot class should be completely opaque to the caller--no visible methods or properties--but its properties have to be visible within the MachineModel class. I could obviously do this by downcasting, i.e. have CurrentSnapshot return an object, and have RestoreSnapshot accept an object argument which it casts back to a Snapshot.

But forced casting like that makes me feel dirty. What's the best alternate design that allows me to be both type-safe and opaque?

Update with solution:

I wound up doing a combination of the accepted answer and the suggestion about interfaces. The Snapshot class was made a public abstract class, with a private implementation inside MachineModel:

public class MachineModel
{
    public abstract class Snapshot
    {
        protected internal Snapshot() {}
        abstract internal void Restore(MachineModel model);
    }

    private class SnapshotImpl : Snapshot
    {
        /* etc */
    }

    public void Restore(Snapshot state)
    {
        state.Restore(this);
    }
}

Because the constructor and methods of Snapshot are internal, callers from outside the assembly see it as a completely opaque and cannot inherit from it. Callers within the assembly could call Snapshot.Restore rather than MachineModel.Restore, but that's not a big problem. Furthermore, in practice you could never implement Snapshot.Restore without access to MachineModel's private members, which should dissuade people from trying to do so.


Solution

  • You could reverse the dependency and make Snapshot a child (nested class) of MachineModel. Then Snapshot only has a public (or internal) Restore() method which takes as a parameter an instance of MachineModel. Because Snapshot is defined as a child of MachineModel, it can see MachineModel's private fields.

    To restore the state, you have two options in the example below. You can call Snapshot.RestoreState(MachineModel) or MachineModel.Restore(Snapshot)*.

    public class MachineModel
    {
        public class Snapshot
        {
            int _mmPrivateField;
    
            public Snapshot(MachineModel mm) 
            { 
                // get mm's state
                _mmPrivateField = mm._privateField;
            }
    
            public void RestoreState(MachineModel mm) 
            { 
                // restore mm's state
                mm._privateField = _mmPrivateField;
            }
        }
    
        int _privateField;
    
        public Snapshot CurrentSnapshot
        {
            get { return new Snapshot(this); }
        }
    
        public void RestoreState(Snapshot ss)
        {
            ss.Restore(this);
        }
    }
    

    Example:

        MachineModel mm1 = new MachineModel();
        MachineModel.Snapshot ss = mm1.CurrentSnapshot;
        MachineModel mm2 = new MachineModel();
        mm2.RestoreState(ss);
    

    * It would be neater to have Snapshot.RestoreState() as internal and put all callers outside the assembly, so the only way to do a restore is via MachineModel.RestoreState(). But you mentioned on Jon's answer that there will be callers inside the same assembly, so there isn't much point.