Skip to content

Instantly share code, notes, and snippets.

@asarnaout
Last active December 3, 2017 19:10
Show Gist options
  • Save asarnaout/ecad9598890709e22dc9a2acf8242d35 to your computer and use it in GitHub Desktop.
Save asarnaout/ecad9598890709e22dc9a2acf8242d35 to your computer and use it in GitHub Desktop.
Programming to an Abstraction
/*
* Several times developers ask questions like:
*
* -What are IEnumerables, ICollections and ILists? Why should we use them?
* -What's the big deal about all those interfaces? I've always been using Concrete types and they get the job done!
* -Why are we using 'var's everywhere?
*
* Abstract programming allows developers to decouple their code from concrete implementations that could be regularly changed. Writing
* abstract code that adapts to modifications is a main characteristic of high quality code. Future proofing your code by using abstract
* types instead of concrete ones will allow API developers to roll out changes to their code without making their clients' lives a bit
* harder.
*
* So lets imagine the following situation where a developer programs the following API:
*/
public List<int> SomeApi()
{
return new List<int> { 1, 2, 3 };
}
/*
* You would expect the above API's Callers will call it this way:
*/
public void SomeConsumer()
{
List<int> result = SomeApi();
}
/*
* Now assume that that the API developer figures out that the API should return a Stack instead of a List, in this case only two options
* are available:
*
* -Mark the old API as obsolete, and create a new API with a different version then request from all clients to migrate to the new API.
* This is an ok solution, however, supporting multiple versions of the same API and migrating to the new API could be a bit tiresome.
*
* -Change the signature of the API to return Stack<int> instead of List<int> - which will break all clients' code as in the following
* example:
*/
public static Stack<int> SomeApi()
{
Stack<int> stack = new Stack<int>();
stack.Push(3);
stack.Push(2);
stack.Push(1);
return stack;
}
/*
* Once the change is rolled out, API callers will get compilation errors due to the signature change:
*/
public void SomeConsumer()
{
List<int> result = SomeApi(); //Error: Cannot implicitly Convert Stack<int> to List<int>
}
/*
* However, had the API developer future proofed the API by returning an abstract type (IEnumerable) instead of a concrete type (List),
* this situation would have been easily avoided as we can see below:
*/
/*
* Before the update:
*/
public static IEnumerable<int> SomeApi()
{
return new List<int> { 1, 2, 3 };
}
public void SomeConsumer()
{
IEnumerable<int> result = SomeApi();
}
/*
* After the update:
*/
public static IEnumerable<int> SomeApi()
{
Stack<int> stack = new Stack<int>();
stack.Push(3);
stack.Push(2);
stack.Push(1);
return stack;
}
public void SomeConsumer()
{
IEnumerable<int> result = SomeApi(); //Compiles successfully
}
/*
* So what is an IEnumerable?
*
* IEnumerable as in interface defining a collection that could be enumerated. The IEnumerable interface was created to provide
* an interface for the Iterator design pattern where details of how an enumeration is enumerated is abstracted from clients who
* enumerate it. For example, you can use a foreach loop on Lists/LinkedLists/Queues/Stacks/Dictionaries without caring how data is
* stored and enumerated or the internal details concerning the collection.
*
* In C# you can use a foreach loop only on types implementing the IEnumerable interface which explains why you can iterate over the
* latter types using foreach loops.
*
* So lets take a look at the IEnumerable interface:
*/
namespace System.Collections.Generic
{
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
}
/*
* The IEnumerable interface defines a single method called GetEnumerator, this method returns an instance implementing the
* IEnumerator interface. This instance is what will be actually used by the foreach loop to enumerate the collection, so lets take a
* look at the IEnumerator interface:
*/
namespace System.Collections
{
public interface IEnumerator<out T>
{
T Current { get; }
bool MoveNext();
void Reset();
}
}
/*
* Note that the IEnumerator interface defines a property called Current and a method called MoveNext; when a foreach loop iterates over
* a collection, it does the following:
*
* 1- Calls the IEnumerable's 'GetEnumerator' method to get the IEnumerator
* 2- Calls the IEnumerator's 'Current' property to get the current element in the collection.
* 3- After the iteration is completed, the foreach loop will call the IEnumerator's 'MoveNext' method. The IEnumerator should then update
* the 'Current' property to reference the next element in the collection
* 4- If the last call to MoveNext returns true, then go to step 2, otherwise if false is returned then this indicates that the
* collection has been enumerated and that the foreach loop should exit now.
*
* And thus we could infer that a foreach loop is actually syntactic sugar for a more complicated use of the IEnumerable inteface
* as we can see below:
*/
var myList = new List<int>(); //List implements the IEnumerable interface
foreach (var item in myList)
{
}
/*
* Behind the scenes, the above foreach statement is actually compiled to:
*/
using (var enumerator = myList.GetEnumerator())
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
}
}
/*
* Still don't get it? Why don't we take a look at the FCL's internal implementation of the Stack to get a better idea:
*/
namespace System.Collections.Generic
{
//This type's implementation has been slightly modified for simplicity
public class Stack<T> : IEnumerable<T>, IEnumerable
{
private T[] _array;
private int _size;
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return (IEnumerator<T>) new Stack<T>.Enumerator(this);
}
public struct Enumerator : IEnumerator<T>, IDisposable, IEnumerator
{
private Stack<T> _stack;
private int _index;
private T currentElement = default(T);
public T Current => this.currentElement;
internal Enumerator(Stack<T> stack)
{
this._stack = stack;
this._index = this._stack._size - 1;
}
public void Dispose() { }
public bool MoveNext()
{
if (this._index == -1) return false;
int num = this._index - 1;
this._index = num;
bool flag1 = num >= 0;
this.currentElement = !flag1 ? default (T) : this._stack._array[this._index];
return flag1;
}
void IEnumerator.Reset() { }
}
}
}
/*
* Note that the Stack internally uses an array to store its generic elements, yet all of these details are abstracted from developers
* who use the Stack API, and thus when developers call a foreach loop over a stack, then they are enumerating the collection in a
* LIFO manner without caring how this is happening internally.
*
* So now it's clear that any type implementing the IEnumerable interface is a type that could be enumerated. Therefore if you are
* programming an API that needs to return a read-only collection that is ONLY going to be enumerated, therefore, the safest practice in
* this case is to return an IEnumerable instead of a concrete type as we can see below:
*/
public static IEnumerable<int> SomeApi()
{
return new List<int> { 1, 2, 3 };
}
/*
* Since clients are expected to do nothing with the collection other than enumerating it, therefore it is best to return an IEnumerable
*/
public void SomeConsumer()
{
IEnumerable<int> result = SomeApi();
foreach(var item in result)
{ /* Some functionality here */ }
}
/*
* ICollections:
*
* Normally there comes the case where a developer needs to return a collection that could not only be enumerated, but could be also
* modified as clients might want to add new items to the collection, so back to our case:
*/
public static IEnumerable<int> SomeApi()
{
return new List<int> { 1, 2, 3 };
}
public void SomeConsumer()
{
IEnumerable<int> result = SomeApi();
result.Add(4); //Compilation error
}
/*
* The code above will break as 'Add' is not defined in the IEnumerable interface. However, the FCL provides another interface
* 'ICollection' which includes the method 'Add' (as well as other methods such as Clear, Contains, CopyTo and Remove). It is
* worthy to note that the ICollection interface implements the IEnumerable interface and thus any object of type 'ICollection' could be
* enumerated as well.
*/
public static ICollection<int> SomeApi()
{
return new List<int> { 1, 2, 3 };
}
/*
* The code below will compile successfully
*/
public void SomeConsumer()
{
ICollection<int> result = SomeApi();
result.Add(4);
foreach(var item in result)
{ /* Some functionality here */ }
result.Clear();
}
/*
* Just like enumeration, the internal implementation of the 'Add' function and how the element was appended to the collection is
* abstracted from the client and is implemented within the List/Stack/LinkedList/Dictionary classes without exposing these details.
*/
/*
* ILists:
*
* What if your clients need to access your collection using indexers?
*/
public static ICollection<int> SomeApi()
{
return new List<int> { 1, 2, 3 };
}
public void SomeConsumer()
{
ICollection<int> result = SomeApi();
var element = result[1]; //Error
}
/*
* This code will break as there is no indexer defined in the ICollection interface. (An indexer is a property which allows you
* to access a collection using square brackets). So in this case the API should return an IList which defines an indexer as well as
* other functions such as IndexOf, Insert and RemoveAt. Note that the IList interface implements the ICollection interface which in
* turn implements the IEnumerable interface, and thus an IList could be enumerated, modified and indexed at the same time.
*/
public static IList<int> SomeApi()
{
return new List<int> { 1, 2, 3 };
}
/*
* The code below will compile successfully
*/
public void SomeConsumer()
{
IList<int> result = SomeApi();
var element = result[1];
result.Add(4);
foreach(var item in result)
{ /* Some functionality here */ }
}
/*
* To sum up, the FCL contains a huge collection of interfaces that express different functionalities, as an API developer you should
* research all the possible use cases of your public APIs and always return interfaces to program to an abstraction, returning
* concrete types would cause your code to be tightly coupled to implementations that are vulnerable to change.
*/
/*
* So what's the big deal about 'var's?
*
* As a client consuming an API, you are also expected to write generic code that is not tightly coupled with the APIs you are consuming.
*
* For example, we can agree that this is a good practice:
*/
public static IList<int> SomeApi()
{
return new List<int> { 1, 2, 3 };
}
public void SomeConsumer()
{
IList<int> result = SomeApi();
}
/*
* However, if the API developer figures out that none of his clients are using indexers (for example) and that the API doesn't need to
* return an IList but instead return an ICollection, then this code will break:
*/
public static ICollection<int> SomeApi()
{
return new List<int> { 1, 2, 3 };
}
public void SomeConsumer()
{
IList<int> result = SomeApi();
}
/*
* The code will break because the compiler will fail to convert an ICollection (a less derived type) to an IList (a more derived type).
* However, had the consumer used 'var' then this code would have worked perfectly:
*/
public static ICollection<int> SomeApi()
{
return new List<int> { 1, 2, 3 };
}
public void SomeConsumer()
{
var result = SomeApi();
}
/*
* Conclusion: Keep your code as abstract as possible, utilize all existing interfaces while exposing functionality and keep a habit
* of using 'var's
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment