DEV Community

Robin van der Knaap
Robin van der Knaap

Posted on • Originally published at Medium on

Software design principles

“It is one thing to write code that works. It is quite another to write good code that works”Dino Esposito (2009)

Eén van mijn persoonlijke doelen met betrekking tot Webpirates is om de bestaande technische kennis en vaardigheden binnen Webpirates vast te leggen, te delen en te vergroten. In dit kader zal ik regelmatig een artikel schrijven die één of meerdere aspecten belicht van het software-ontwikkelproces. Dit artikel geeft een overzicht van software design principes die in de loop der jaren industriestandaarden zijn geworden en door elke programmeur gekend en toegepast zouden moeten worden.

Het draait allemaal om aanpasbaarheid

Goed geschreven software voldoet niet alleen aan de eisen die vandaag gesteld zijn, maar houdt ook rekening met de gevraagde aanpassingen en toevoegingen van morgen. Het is dus van belang dat de ontwikkelaar zijn of haar code zo ontwerpt dat deze in een later stadium relatief eenvoudig aan te passen is.

Tijdens het ontwerp van de applicatie zijn de aanpasbaarheid en onderhoudbaarheid van de applicatie dan ook de belangrijkste kwaliteitskenmerken voor een ontwikkelaar. Al het andere kan later ingepast worden, als de applicatie maar eenvoudig aan te passen is. Vaak wordt gesteld dat wisselende requirements de oorzaak zijn van slecht geschreven software, dat door alle veranderingen de code onbeheersbaar wordt. Dit is pertinent onjuist. Het is aan de ontwikkelaar om ervoor te zorgen dat zijn software bestand is tegen aanpassingen en uitbreidingen. Wisselende requirements ontmaskeren dus eigenlijk alleen slecht geschreven software en hun auteur.

Symptomen van slecht design

Slecht geschreven software is slecht bestand tegen veranderingen. Robert C. Martin (2000) formuleert slecht design als volgt: Software die voldoet aan de opgegeven requirements maar aan één of meer van de volgende symptomen lijdt is slecht ontworpen:

  • Rigiditeit: Elke verandering zorgt voor teveel aanpassingen in andere delen van het systeem.
  • Fragiliteit: Wanneer een aanpassing doorgevoerd wordt, gaan andere delen van het systeem onverwacht kapot.
  • Immobiliteit: Lastig om code te hergebruiken in een andere applicatie omdat teveel afhankelijkheden met de huidige applicatie bestaan.
  • Viscositeit: Makkelijker om omwegen in de software te bouwen dan daadwerkelijk problemen in de software op te lossen.

High Cohesion, Low Coupling

De beste strategie om software makkelijk aanpasbaar te maken is door het systeem op te delen in losse componenten, high cohesion en loose coupling zijn hierbij de uitgangspunten.

Cohesion geeft de mate aan waarin de verschillende functionaliteiten van een component met elkaar gerelateerd zijn. Hoe meer cohesie hoe beter. In de praktijk betekent dit het gebruik van sterk gespecialiseerde classes met meestal weinig methodes. Met de groei van het aantal functionaliteiten van een class groeit ook het aantal afhankelijkheden van andere classes, dit staat aanpassingen in de software in de weg. Coupling geeft de mate van afhankelijkheid tussen de verschillende componenten aan. Hoe minder afhankelijkheden hoe beter. Dit maakt de code makkelijker te onderhouden, leesbaarder, eenvoudiger om te testen en verkleint tevens de mate van fragiliteit en rigiditeit van het systeem.

Separation of Concerns

Separation of Concerns (SoC) ( geïntroduceerd door Edsger W. Dijkstra, 1974) is een principe dat helpt loose coupling en high cohesion te bereiken. Het toepassen van SoC betekent het opdelen van een systeem in delen die elkaar zo min mogelijk overlappen, elk deel heeft zijn eigen verantwoordelijkheid.

SoC wordt concreet bereikt door code modulair op te stellen, de interne werking van de modules af te schermen (encapsulation), en alleen te communiceren via publieke interfaces. Op deze manier kunnen veranderingen in de interne werking van modules plaats vinden, zonder dat andere modules hiervoor aangepast dienen te worden aangezien de interface gelijk blijft.

In de praktijk zie je toepassingen van SoC overal, in procedurele talen worden procedures en functies gebruikt voor het scheiden van de verschillende aspecten van een systeem, in object georiënteerde talen worden classes gebruikt. Architecturen zoals MVC bereiken SoC door het opdelen van systemen in meerdere logische lagen elk met hun eigen vooraf gedefinieerde verantwoordelijkheden.

Object Oriented Programming (OOP)

Object georiënteerde programmeertalen voorzien de ontwikkelaar van de benodigde gereedschappen en technieken om een systeem op te delen in een groot aantal losse onderdelen (objecten). Door de jaren heen zijn een aantal industrie standaarden ontstaan op het gebied van OOP die algemeen toepasbaar zijn op elke object georiënteerde programmeertaal.

Ik bespreek eerst de basis principes van OOP en vervolgens een aantal meer geavanceerde principes die vallen onder de noemer S.O.L.I.D. principles gebundeld door Robert C. Martin.

Basis OOP principes

Hét standaardwerk op het gebied van OOP is het boek van the Gang of Four (GoF) ‘Design Patterns: Elements of Reusable Object-Oriented Software’. GoF vat object georiënteerd design als volgt samen:

‘You must find pertinent objects, factor them into classes at the right granularity, define class interfaces and inheritance hierarchies, and establish key relationships among them. Your design should be specific to the problem at hand, but also general enough to address future problems and requirements.’

Het identificeren van pertinente objecten gebeurt meestal door de requirements en/of use-cases af te speuren naar zelfstandige naamwoorden en werkwoorden. Zelfstandige naamwoorden representeren vaak objecten of eigenschappen van objecten, werkwoorden zijn vaak methoden van objecten. Voorbeeld:

“De gebruiker moet een nieuwe recipient toe kunnen voegen aan de mailinglist.”

Objecten : Gebruiker, Recipient, Mailinglist

Methode: AddRecipient

Belangrijk om te realiseren bij het identificeren van objecten is het feit dat objecten ‘dingen’ representeren géén processen. Processen zijn de interacties tussen de verschillende objecten. Hierin verschilt OOP dus fundamenteel met procedurele programmeertalen.

Twee basisprincipes worden door GoF naar voren gebracht:

  • Program to an interface, not an implementation
  • Favor object composition over class inheritance

Het eerste principe wijst op het gebruik van interfaces om de verschillende objecten met elkaar te laten communiceren. Zoals eerder uitgelegd zorgt het gebruik van interfaces voor een minder sterke koppeling tussen de verschillenden objecten en biedt hierdoor een verhoogde weerbaarheid tegen toekomstige aanpassingen.

De volgende code laat een class zien die sterk gekoppeld is met de class Logger:

De volgende code laat zien dat het gebruik van een interface zorgt voor loose coupling:

De LogHelper class stelt een class beschikbaar die de interface ILogger implementeert. De helper class kan nu dus elke class ter beschikking stellen zolang deze class maar de ILogger interface implementeert. Welke class wordt aangeboden kan bijvoorbeeld worden bepaald door een configuratiebestand uit te lezen. De helper class wordt in dit geval ook wel een factory object genoemd, een producent van classes.

Het tweede principe, ‘favor object composition over class inheritance’, geeft aan dat overerving van classes niet altijd de beste methode is om code te hergebruiken. Dit wordt echter wel vaak aangedragen als een voordeel van object georiënteerd programmeren. Afgeleide classes kunnen kapot gaan door toekomstige veranderingen in het basis type dat overerft wordt. Tevens dienen afgeleide classes zo gebouwd te worden dat elke plaats waar het basis type ingezet kan worden, ook het afgeleide type kan worden gebruikt. Dit is in de praktijk lang niet altijd het geval, omdat bestaande functionaliteiten van het basis type overschreven of aangepast worden, en niet meer bruikbaar zijn in de context van het basis type. (zie ook Liskov’s Substitution Principle)

Overerving dient eigenlijk alleen gebruikt te worden als er alleen nieuwe methoden aan een class worden toegevoegd. Compositie is meestal een veiligere oplossing voor het hergebruiken van code. Met behulp van compositie creëer je een nieuw type (class) die een instantie van het basis type bevat. Het basis type wordt nooit rechtstreeks aangesproken, maar is een private member. Het compositie object heeft nooit toegang tot de interne delen van de basis class en kan het gedrag dus niet veranderen. Het compositie object is in feite een wrapper en delegeert waar nodig alle aanroepen naar het basis type en voegt zelf functionaliteiten toe. Voorbeeld:

Als we nu beide principes zouden willen combineren moeten we eigenlijk het interne basis object niet rechtstreeks aanspreken, maar vervangen door een interface.

SOLID OOP principes

Naast de basisprincipes beschreven door de Gang of Four bestaan er nog een aantal geavanceerde principes, de belangrijkste zijn gebundeld door Robert C. Martin, genaamd de SOLID principles:

  • S ingle Responsibility Principle: There should never be more than one reason for a class to change. (Robert C. Martin, 1996) Wanneer een class meerdere verantwoordelijkheden heeft, bestaan er meerdere redenen om een class aan te passen (lage cohesie). Dit geeft een koppeling tussen de verschillende verantwoordelijkheden, wat leidt tot rigiditeit en fragiliteit van een applicatie.
  • O pen Closed Principle: A module should be open for extension but closed for modification (Bertrand Meyer, 1988). Dit betekent dat de source code van een module nooit aangepast wordt, maar altijd d.m.v. overerving of compositie wordt aangepast. In de praktijk is het meestal de interface die niet aangepast wordt.
  • L iskov’s Subtitution Principle: Subclasses should be substitutable for their base classes. (Barbara Liskov, 1994). Een afgeleide class zou altijd ingezet moet kunnen worden waar hun basis class ook kan worden ingezet. Eerder is al gezegd dat dit niet altijd het geval is, compositie is dan op zijn plaats.
  • I nterface Segregation Principle: Clients should not be forced to depend on methods that they do not use. (Robert C. Martin, 1996). Interfaces van een class zouden cohesive moeten zijn, gegroepeerd naar functionaliteiten. Wanneer dit niet het geval is, deel dan de interfaces op in verschillende delen. In het geval van een ‘fat interface’ zou elke class die deze interface implementeert alle methoden moeten implementeren, inclusief de methoden die hij niet gebruikt.
  • D ependency Inversion Principle: High-level modules should not depend upon low-level modules. Both should depend on abstractions. Abstractions should not depend upon details. Details should depend upon abstractions. (Robert C. Martin, 1996). Kort gezegd zouden high-level modules niet afhankelijk mogelijk zijn van één of meerdere low-level modules. Het volgende voorbeeld geeft een nieuwsbrief module aan die afhankelijk is van een low-level module om de nieuwsbrief te versturen:

Om de afhankelijkheid op te heffen, kunnen de low-level modules worden geïnjecteerd in de constructor van de high-level module:

Conclusie

De belangrijkste les die getrokken moet worden is het feit dat software ontworpen moet worden met het oog op toekomstige aanpassingen en toevoegingen. Alle principes beschreven in dit artikel bevorderen de flexibiliteit van een applicatie en verhogen hiermee de weerbaarheid tegen veranderingen.

Naast design principles bestaan er ook talloze design patterns die helpen bij het oplossen van bekende design problemen en zorg dragen voor een minimaal aantal afhankelijkheden in de code. Patterns zijn bewust niet genoemd in dit artikel aangezien patterns meestal toegepast worden op specifieke probleemgebieden, terwijl de design principes genoemd in dit artikel algemeen toepasbaar zijn.

Gebruikte literatuur:

Microsoft .NET: Architecting Applications for the Enterprise, Dino Esposito et al, 2009.

Design Principles and Design Patterns, Robert C. Martin, www.objectmentor.com, 2000.

The Single Responsibility Principle, Robert C. Martin, www.objectmentor.com, 1996.

The Open-Closed Principle, Robert C. Martin, www.objectmentor.com, 1996.

The Liskov Substitution Principle, Robert C. Martin, www.objectmentor.com, 1996.

The Interface Segregation Principle Principle, Robert C. Martin, www.objectmentor.com, 1996.

The Dependency Inversion Principle, Robert C. Martin, www.objectmentor.com, 1996.

Patterns of Enterprise Application Architecture, Martin Fowler, 2003.

ASP.NET MVC Framework Unleashed, Stephen Walter, 2009.

Oldest comments (0)