Hiding the implementation is a core principle of OOP and a good idea in all paradigms, but it especially important for iterators(or whatever they're called in that specific language) in languages that support lazy iteration.
The problem with exposing the concrete type of iterables - or even interfaces like IList<T>
- is not in the objects that expose them, but in the methods that use them. For example, let's say you have a function for printing a list of Foo
s:
void PrintFoos(IList<Foo> foos)
{
foreach (foo in foos)
{
Console.WriteLine(foo);
}
}
You can only use that function to print lists of foo - but only if they implement IList<Foo>
IList<Foo> foos = //.....
PrintFoos(foos);
But what if you want to print every even-indexed item of the list? You'll need to create a new list:
IList<Foo> everySecondFoo = new List<T>();
bool isIndexEven = true;
foreach (foo; foos)
{
if (isIndexEven)
{
everySecondFoo.Add(foo);
}
isIndexEven = !isIndexEven;
}
PrintFoos(everySecondFoo);
This is quite long, but with LINQ we can do it to a one-liner, which is actually more readable:
PrintFoos(foos.Where((foo, i) => i % 2 == 0).ToList());
Now, did you notice the .ToList()
in the end? This converts the lazy query into a list, so we can pass it to PrintFoos
. This requires an allocation of a second list, and two passes on the items(one on the first list to create the second list, and another on the second list to print it). Also, what if we have this:
void Print6Foos(IList<Foo> foos)
{
int counter = 0;
foreach (foo in foos)
{
Console.WriteLine(foo);
++ counter;
if (6 < counter)
{
return;
}
}
}
// ........
Print6Foos(foos.Where((foo, i) => i % 2 == 0).ToList());
What if foos
has thousands of entries? We will have to go through all of them and to allocate a huge list just to print 6 of them!
Enter Enumerators - C#'s version of The Iterator Pattern. Instead of having our function accept a list, we make it accept Enumerable
:
void Print6Foos(Enumerable<Foo> foos)
{
// everything else stays the same
}
// ........
Print6Foos(foos.Where((foo, i) => i % 2 == 0));
Now Print6Foos
can lazily iterate over the first 6 items of the list, and we don't need to touch the rest of it.
Not exposing the internal representation is the key here. When Print6Foos
accepted a list, we had to give it a list - something that supports random access - and therefore we had to allocate a list, because the signature does not guarantee that it'll only iterate over it. By hiding the implementation, we can easily create a more efficient Enumerable
object that supports only what the function actually needs.
F
that gives me knowledge (methods) ofa
,b
andc
. All is well and good, there can be many things that are different but justF
s to me. Think of the method as "constraints" or clauses of a contractF
commits classes to doing. Suppose that we add ad
though, because we need it. This adds an additional constraint, each time we do this we impose more and more on theF
s. Eventually (worst case) only one thing can be anF
so we may as well not have it. AndF
constrains so much there's only one way to do it.