Doc Brown's answer shows a classic textbook implementation of Law of Demeter - and the annoyance/disorganized-code-bloat of adding dozens of methods that way is probably why programmers, myself included, often don't bother doing so, even if they should.
There is an alternative way to decouple the hierarchy of objects:
Expose interface
types, rather than class
types, via your methods and properties.
In Original Poster's (OP's) case, encoder->WaitEncoderFrame()
would return an IEncoderFrame
instead of a Frame
, and would define what operations are permissible.
SOLUTION 1
In the easiest case, Frame
and Encoder
classes are both under your control, IEncoderFrame
is a subset of methods Frame already publicly exposes, and Encoder
class doesn't actually care what you do to that object. Then, implementation is trivial (code in c#):
interface IEncoderFrame {
void DoOrGetSomething();
}
class Frame : IEncoderFrame {
// A method that already exists in Frame.
public void DoOrGetSomething() { ... }
}
class Encoder {
private Frame _frame;
public IEncoderFrame TheFrame { get { return _frame; } }
...
}
SOLUTION 2
In an intermediate case, where Frame
definition is not under your control, or it would not be appropriate to add IEncoderFrame
's methods to Frame
, then a good solution is an Adapter. That is what CandiedOrange's answer discusses, as new FrameHandler( frame )
. IMPORTANT: If you do this, it is more flexible if you expose it as an interface, not as a class. Encoder
has to know about class FrameHandler
, but clients only need to know interface IFrameHandler
. Or as I named it, interface IEncoderFrame
- to indicate that it is specifically Frame as seen from POV of Encoder:
interface IEncoderFrame {
void DoOrGetSomething();
}
// Adapter pattern. Appropriate if no access needed to Encoder.
class EncoderFrameWrapper : IEncoderFrame {
Frame _frame;
public EncoderFrameWrapper( Frame frame ) {
_frame = frame;
}
public void DoOrGetSomething() {
_frame....;
}
}
class Encoder {
private Frame _frame;
// Adapter pattern. Appropriate if no access needed to Encoder.
public IEncoderFrame TheFrame { get { return new EncoderFrameWrapper( _frame ); } }
...
}
COST: Allocation and GC of a new object, the EncoderFrameWrapper, every time encoder.TheFrame
is called. (You could cache that wrapper, but that adds more code. And is only easy to code reliably if encoder's frame field cannot be replaced with a new frame.)
SOLUTION 3
In the more difficult case, the new wrapper would need to know about both Encoder
and Frame
. That object would itself violate LoD - it is manipulating a relationship between Encoder and Frame that should be Encoder's responsibility - and probably be a pain to get right. Here is what can happen if you start down that road:
interface IEncoderFrame {
void DoOrGetSomething();
}
// *** You will end up regretting this. See next code snippet instead ***
class EncoderFrameWrapper : IEncoderFrame {
Encoder _owner;
Frame _frame;
public EncoderFrameWrapper( Encoder owner, Frame frame ) {
_owner = owner; _frame = frame;
}
public void DoOrGetSomething() {
_frame.DoOrGetSomething();
// Hmm, maybe this wrapper class should be nested inside Encoder...
_owner... some work inside owner; maybe should be owner-internal details ...
}
}
class Encoder {
private Frame _frame;
...
}
That got ugly. There is a less-convoluted implementation, when the wrapper needs to touch details of its creator/owner (Encoder):
interface IEncoderFrame {
void DoOrGetSomething();
}
class Encoder : IEncoderFrame {
private Frame _frame;
// HA! Client gets to think of this as "the frame object",
// but its really me, intercepting it.
public IEncoderFrame TheFrame { get { return this; } }
// This is the method that the LoD approach suggests writing,
// except that we are exposing it only when the instance is accessed as an IEncoderFrame,
// to avoid extending Encoder's already large API surface.
public void IEncoderFrame.DoOrGetSomething() {
_frame.DoOrGetSomething();
... make some change within current Encoder instance ...
}
...
}
Granted, if I knew I would end up here, I might not do this. Could just write the LoD methods, and be done with it. No need to define an interface. On the other hand, I like it that the interface wraps related methods together. I like how it feels to do the "frame-like operations" to what feels like a frame.
FINAL COMMENTS
Consider this: If the implementor of Encoder
felt that exposing Frame frame
was appropriate to their overall architecture, or was "so much easier than implementing LoD", then it would have been much safer if they instead did the first snippet I show - expose a limited subset of Frame, as an interface. In my experience, that is often a completely workable solution. Just add methods to the interface as needed. (I am talking about a scenario where we "know" Frame already has the needed methods, or they would be easy and non-controversial to add. The "implementation" work for each method is adding one line to the interface definition.) And know that even in the worst future scenario, it is possible to keep that API working - here, by removing IEncoderFrame
from Frame
and putting it on Encoder
.
Also note that if you don't have permission to add IEncoderFrame
to Frame
, or the needed methods don't fit well to the general Frame
class, and solution #2 doesn't suit you, perhaps because of the extra object creation-and-destruction, solution #3 can be seen as simply a way to organize the methods of Encoder
, to accomplish LoD. Don't just pass through dozens of methods. Wrap them in an Interface, and use "explicit interface implementation" (if you are in c#), so that they can only be accessed when the object is viewed through that interface.
Another point I want to emphasize is that the decision to expose functionality as an interface, handled all 3 of the situations described above. In the first, IEncoderFrame
is simply a subset of Frame
's functionality. In the second, IEncoderFrame
is an adapter. In the third, IEncoderFrame
is a partition into Encoder
s functionality. It doesn't matter if your needs change between these three situations: the API stays the same.