If you really don't want to leak details of "privileged" operations then the cleanest answer in popular OO languages like Java and .NET is multiple interface inheritance.
One that I've used semi-frequently goes something like:
IFooRepository
- contains methods GetFoo
, DeleteFoo
, SaveFoo
IReadOnlyFooRepository
- contains only method GetFoo
In the example above, IFooRepository
might subclass IReadOnlyFooRepository
. Also, if there is a well-defined way to transition between the read-only and writable state, then you might put an Edit
method on the IReadOnlyFooRepository
that returns an instance of IFooRepository
, and/or an AsReadOnly
method on the IFooRepository
that returns an instance of IReadOnlyFooRepository
.
This is not without precedent, at least in .NET. One of the most common interfaces, IList<T>
, has a method AsReadOnly
that returns a ReadOnlyCollection<T>
, which as you might guess, does not expose any kind of write operations.
It's not hard to implement - generally you only need one class to implement both interfaces, and the Edit
/AsReadOnly
methods can just return this
(or self
or whatever it is in your chosen language). Of course, if you do this, don't fool yourself into thinking that it's a form of security, as a caller could potentially just cast between types - but assuming you just want to protect against stupid mistakes rather and not intentional abuse, it's good enough.
You can get more granular if you want - for example, you don't necessarily need one interface to derive from the other, you could have IFooReader
and IFooWriter
with no methods in common. Or you might define an IFooComponent
(or something along those lines) at the root if you only want to share a few methods, but have most of them be unique. Sky's the limit, but the basic idea stands - use interfaces to carve out specific blocks of functionality that are specific to a particular state.
This takes a lot more design work and adds a lot more types to the code if your states and transitions are complex, so be sure that it's really worth the effort. It's perfectly fine and in fact common in a lot of architectures (like most MVVM frameworks) to just throw an exception if an operation isn't valid for the current state, and also provide convenience "guard" methods like CanWrite
, to avoid strewing try-catch loops all over the place. That's good if you have 2 or 3 discrete states; if you have 10 or 20 then you might want to think about either a state pattern or a design such as the above that makes the transitions more explicit.