I encounter this issue multiple times, but I'm never sure how best to deal with it.
Basically, some methods I write require the implementing object to support multiple interfaces. The example I have in mind is that a table structure needs to be able to IIterate
over its records and IStreamable
its content, where
IIterate = interface
procedure First;
procedure Next;
property EOF: boolean;
end;
IStreamable = interface // outputs current record in table only
function StreamOut: TArray<byte>;
procedure StreamIn(const Input: TArray<byte>);
end;
Now I don't think I want to combine these interfaces, as it's rare for methods to require IStreamable
and not every IStreamable
class can IIterate
(my use case has single-record structures too).
The method I am trying to write is
CopyTable(InIterate: IIterate; InStreamable: IStreamable; OutIterate:IIterate; OutStreamable:IStreamable);
Where the In and Out interfaces must be implemented by the same object. At the moment I'm just asking for one of the interfaces and using a Supports
call to get the second, though this doesn't give you any compile-time protection. The obvious alternative is to use the above structure and check that both interfaces have the same implementing class, but that looks ugly.
Is there a better way to work with this kind of problem and get some compile-time safety?
The implementing language is Delphi (XE2), though I think this is a fairly language agnostic question for languages that don't have multiple inheritance.
Best Answer
First of all,
Iterate
is a lame name for that interface, it should be calledIterator
, so I will be calling itIterator
in this answer, because I am allergic to bad naming.You say that you encounter this issue often, and I have to believe you, but I also ought to mention my experience, which is that I have rarely, if ever, encountered this need in the last couple of decades that I have been doing OOP.
Usually, when such a need appears to arise, it is an indication that some other aspect of the design needs refactoring.
The first suspect to examine is the
CopyTable()
function: why does it need both anIterator
and aStreamable
for both the source and the destination table? Why can't it just stream-out the source table to bytes and then stream-in the bytes into the destination table? And why is it even calledCopyTable
when it is probably justCopyStreamable()
orCopyStreamableIterator()
at worse?If there is nothing that can be done about the function, then the best approach in my opinion is to combine the two interfaces into one. The name
CopyStreamableIterator()
which describes what the function does immediately tells you that you need aStreamableIterator
interface which combinesStreamable
andIterator
. There is nothing wrong with that, and you do not even have to make yourTable
class implement that new interface, you can simply implement aStreamableIteratorDecorator
(see decorator pattern) which accepts aStreamable
and anIterator
as constructor parameters and implements theStreamableIterator
interface by delegating to them. (Though you will probably just declare yourTable
class as implementing this interface to be done real quick, because yourTable
already implements these methods, so it is good to go.)Note: I am presuming that in Delphi, just as in Java and C#, a class may extend only one class but multiple interfaces, and that an interface may also extend multiple interfaces, but even if by any chance this is not true, this functionality can be emulated by aggregating interface implementations into classes and adding to classes and interfaces methods that return sub-interfaces.
Amendment
Thinking about it a bit more, I am realizing that your problem may lie in that ill-named
IIterate
interface. This interface is trying to cut corners, so it is built in such a way as to force manipulation of the internal state of the underlying (implementing) object. This causes problems, because it is forcing you to have two separate interfaces, one for altering the internal state of your table to select a row, and another interface for reading/writing the currently selected row.In a language like C# and Java your table would implement
IIterable<IStreamable>
, andIIterable<T>
would have just one method,newIterator() : IIterator<T>
. In turn,IIterator<T>
would be defined as follows:or as follows:
So, instead of selecting rows, your iteration would be yielding each row which can then be streamed. So, you only need to pass an iterator (or better yet an iterable) to your
CopyTable()
function.Adding rows to the target table can be easily accomplished by implementing a
IConsumer<IStreamable>
interface which simply appends the contents of theIStreamable
as a new row at the end of the table.