Last active
December 3, 2017 19:10
-
-
Save asarnaout/ecad9598890709e22dc9a2acf8242d35 to your computer and use it in GitHub Desktop.
Programming to an Abstraction
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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