Inversion of Control Container met interfaces
In vorige blogitems schreef ik over interfaces en hoe ze ervoor zorgen dat je software beter te testen is en hoe ze helpen bij het uitbreiden van je software. Een andere voordeel van interfaces is dat ze je kunnen helpen bij het onderhouden van je software. Dat kan enerzijds door via het contract af te dwingen hoe een object, als implementatie van een bepaalde interface, dient te werken en anderzijds doordat je gedwongen wordt je implementaties te definiëren en het is logisch dat in sommige gevallen op een slimme plek te doen. IFileSystem is hier een goed voorbeeld: het zal niet voorkomen (of bij hoge uitzondering) dat je in je software meer dan één implementatie hiervan gebruikt. Tijdens het testen zal je weliswaar een specifieke implementatie gebruiken, maar je daadwerkelijke applicatie maakt gebruik van je lokale schijven óf van een Cloud-oplossing, maar niet van beide. Het is dus verstandig de initialisatie van deze implementatie in je software zo snel en globaal mogelijk te doen.
Bijvoorbeeld, ik zou al mijn implementaties kunnen initialiseren in een aparte functie die ik meteen aanroep:
private static IFileSystem fileSystem;
static void Main(string[] args)
{
InitClasses();
// Doe hier dingen.
storeReview(review, fileSystem);
}
private static void InitClasses()
{
fileSystem = new ActualFileSystem();
}
Waarschijnlijk gebruik je hiervoor dan een aparte klasse, maar het idee is hetzelfde: je geeft op één plek aan welke implemenatie gebruikt wordt voor IFileSystem en andere interfaces. Wil je er één aanpassen of iets toevoegen, zoals een bepaalde configuratie, dan doe je dat in InitClasses.
In .NET (en andere programmeeromgevingen) kan je gebruikmaken van een Inversion of Control Container om zoveel mogelijk van deze logica voor je te regelen. Je geeft dan meestal alleen aan welke implementatie je wilt hebben voor welke interface en op veel plekken kan je deze implementatie gewoon ophalen via de container, terwijl je het maar op één plek hebt gedefinieerd. In .NET Core kan je hier bijvoorbeeld Unity Container voor gebruiken.
Eerst plaats ik de review-code in een aparte klasse:
static void Main(string[] args)
{
InitClasses();
var reviewApp = new ReviewApp(fileSystem);
reviewApp.StoreReviews();
}
//...
public ReviewApp(IFileSystem fileSystem)
{
this.fileSystem = fileSystem;
}
Als ik dit uitvoer, dan werkt het zoals het al deed. Om Unity Container te gebruiken moet ik enkele aanpassingen doen:
private static UnityContainer iocContainer;
static void Main(string[] args)
{
InitClasses();
var reviewApp = iocContainer.Resolve<ReviewApp>();
reviewApp.StoreReviews();
}
private static void InitClasses()
{
iocContainer = new UnityContainer();
iocContainer.RegisterType<IFileSystem, ActualFileSystem>();
}
In bovenstaande voorbeeld laat ik iocContainer alles regelen. De container ziet dat ReviewApp een IFileSystem verwacht in de constructor en zoekt op welke ik heb ingesteld en geeft deze mee. Als ik nou een tweede argument verwacht (INetworkHandler bijvoorbeeld), dan hoef ik de initialisatie maar op 2 plekken aan te passen:
public ReviewApp(IFileSystem fileSystem, INetworkHandler networkHandler)
// ...
private static void InitClasses()
//...
iocContainer.RegisterType<IFileSystem, ActualFileSystem>();
iocContainer.RegisterType<INetworkHandler, MyNetworkHandler>();
De container "weet" dat mijn constructor een INetworkHandler verwacht en zoekt op welke ik heb ingesteld. Door op zo min mogelijk plekken aanpassingen te doen, heb ik de software minder complex gemaakt en gemakkelijker te onderhouden. In bovenstaande voorbeeld voegt de IoC Container niet bepaald veel waarde toe. Er zijn echter meer voordelen en het zorgt er ook voor dat je gedwongen wordt je code logischer in elkaar te zetten. Zie voor meer informatie ook: http://unitycontainer.org/articles/introduction.html.