SOLID

test:

Important tip: this note needs to be highlighted

Important danger: this note needs to be highlighted

Bron: https://github.com/GitbookIO/plugin-codetabs

msg = "Hello World" print msg

var msg = "Hello World"; console.log(msg);

Hello World

https://github.com/GitbookIO/plugin-codetabs

Dit hoofdstuk werd origineel geschreven door Tom Peeters. Bron. maar moet nog langs editor Tim ;)

Inleiding

Software ontwikkelaars worden geconfronteerd met ontwerpproblemen. Professionals zullen echter merken dat bepaalde soorten van ontwerpproblemen steeds terugkomen. Eénmaal je een probleem herkent als een variant van een probleem dat je vroeger al eens hebt opgelost, kan je gebruik maken van de inzichten die je al verworven hebt. Je ziet bepaalde patronen terugkeren.

Wat is nu precies een ontwerppatroon of design pattern

Een ontwerppatroon is een standaardoplossing voor een vaak voorkomend ontwerpprobleem. Deze patronen zijn belangrijk omdat ze je de moeite kunnen besparen om telkens opnieuw het warm water uit te vinden. Bovendien heeft elk patroon een eigen naam, wat ervoor zorgt dat het heel eenvoudig wordt om bepaalde complexe ideeën in een oogwenk te communiceren aan een andere programmeur.

GESCHIEDENIS VAN ONTWERPEN

Sinds het begin van het computertijdperk is probleem-oplossend denken ingrijpend veranderd.

PROGRAMMEREN: THE SEQUEL

In het begin programmeerden we met assembly, en was elk programma beperkt tot een honderdtal lijnen. Elke programmeur had zijn eigen stijl volgens intuïtie.

PROGRAMMEREN: FLOW BASED DESIGN

Toen de complexiteit toenam, gingen meerdere programmeurs code reviews verrichten bij elkaar en merkte men al dat onderhoud en begrijpen van code niet voor de hand lag. Men trachtte normen op te leggen en ging flowcharts maken om programmeurs een goed design te laten maken. Flowcharts bleken ook nuttig om programma’s eenvoudiger te begrijpen.

PROGRAMMEREN: GESTRUCTUREERD PROGRAMMEREN

Gestructureerd programmeren volgde in de jaren ‘70. Een gestructureerde code bestaat uit één enkel begin en afsluitpunt en daar tussen een set van modules. Gestructureerde programma’s zijn makkelijker te lezen en te begrijpen, te onderhouden en vereisen minder ontwikkel-tijd.

PROGRAMMEREN: OBJECT-GEORIENTEERD DESIGN

Object-georiënteerd programmeren gebeurt intuïtief en identificeert natuurlijke objecten ( Hero, vijand, ...) die voorkomen in je probleem. Daarnaast worden relaties zoals composities, referenties, overerving bepaald. Dit resulteert in herbruikbaarheid van code, en overzichtelijkere en makkelijk te onderhouden code.

VANDAAG

Door de toenemende concurrentie moet je als programmeur tegenwoordig zeer dynamisch (Agile Principe) zijn. Ook is de gemiddelde levensduur van een product drastisch verlaagd. Organisaties moeten snel op marktveranderingen kunnen antwoorden. Ook worden business strategieën snel aangepast wat wil zeggen dat bijvoorbeeld een goed software design zeer belangrijk is om snel op deze veranderingen in te kunnen inspelen. Software moet snel ontwikkeld kunnen worden en staat dicht bij de klant ( deze kan al vaak worden betrokken bij de ontwikkeling van gepersonaliseerde software).

Object georienteerd programmeren Intro

De basisgedachte achter object georiënteerd programmeren is dat mensen een beetje van de realiteit proberen te modelleren zodat het model in de vorm van een werkend programma kan worden gegoten. Je kan je object model beschouwen als een blackbox. Bijvoorbeeld een auto als blackbox betekent dat je een handvol pedalen, schakelaars hebt die fungeren als interface. Duwen op de rem betekent dat je auto mindert, maar je hoeft niet te weten hoe dat gebeurt, enkel maar wat er gebeurt. Dit principe heet encapsulatie.

Encapsulatie: je probeert zoveel mogelijk zaken af te schermen van de rest.

Bijvoorbeeld een auto kan starten, maar je weet niet wat er allemaal moet gebeuren om de auto te starten. Dit noemen we een interface.

Klassen en objecten

Klasse: een beschrijving en verzameling van dingen (objecten) met soortgelijke eigenschappen

Object: een instantie van een klasse

Als voorbeeld kunnen we een auto nemen.Een auto catalogeren we als een klasse, want bestaat uit een aantal eigenschappen, zoals de kleur van de auto, het aantal pk, benzine of diesel motor, enzovoort. Maar ook het starten, stoppen, schakelen van de wagen worden als eigenschappen bezien.

Een object betekent bijvoorbeeld een nieuwe renault met een rode kleur, 100pk en dieselmotor.

De auto is de klasse die beschrijft hoe een auto er voor onze probleemstelling moet uit zien, terwijl de renault een instantie van de klasse is, of ook wel object genoemd. Wat betekent dat dit een effectieve auto is die je kan gebruiken.

Hoe maak je een klasse

Een klasse kan bestaan uit:

  • private member variabelen: bepalen de toestand van de klasse

  • constructor

  • public methoden: aanspreekpunten voor de buitenwereld, of interfaces genoemd

  • properties: een gecontroleerde toegang tot de toestand

  • private methoden: hulpmethoden die enkel beschikbaar zijn binnen het object

SOLID

S.O.L.I.D. zijn 5 principes die ons helpen om een goede software architectuur te schrijven (door Robert C. Martin)

  • S : SRP (Single responsibility principle)

  • O : OCP (Open closed principle)

  • L : LSP (Liskov substitution principle)

  • I : ISP (Interface segregation principle)

  • D : DIP (Dependency inversion principle)

Single Responsibility Principle

Een klasse heeft slechts 1 bestaansreden en kan maar 1 reden hebben om te veranderen

Eigenschappen van SRP zijn:

  • coupling

  • cohesion

Cohesion: wat een klasse zou moeten doen. Lage cohesie betekent dat een klasse verschillende zaken doet, en niet gefocust is op de taak die hij zou moeten doen. Terwijl hoge cohesie betekent dat een klasse doet wat hij moet doen, en maar 1 taak uitvoert. Probeer er voor te zorgen dat alle methoden in een klasse betrekking hebben tot 1 doel, maw er een hoge cohesie heerst.

Coupling: Hangt een klasse van nog andere klassen af. Of hoeveel weet een klasse over de werking (inner working) van een andere klasse af.

Men streeft naar "low coupling" en "high cohesion"

Waarop letten?

Klassen mogen maar een beperkt aantal instantievariabelen hebben. De methoden van deze klasse moeten één of meerdere van deze variabelen manipuleren.

Wat bedoelen we met verantwoordelijkheid?

Een reden tot verandering!

Een voorbeeld van cohesie

Een klasse met hoge cohesie:

class EmailMessage
{
    private string sendTo;
    private string subject;
    private string message;
    public EmailMessage(string to, string subject, string message)
    {
        this.sendTo = to;
        this.subject = subject;
        this.message = message;
    }
    public void SendMessage()
    {
        // send message using sendTo, subject and message
    }
}

Een voorbeeld van lage cohesie :

class EmailMessage
{
    private string sendTo;
    private string subject;
    private string message;
    private string username;
    public EmailMessage(string to, string subject, string message)
    {
        this.sendTo = to;
        this.subject = subject;
        this.message = message;
    }
    public void SendMessage()
    {
        // send message using sendTo, subject and message
    }
    public void Login(string username, string password)
    {
        this.username = username;
        // code to login
    }
}

Lage Cohesie

Hoge Cohesie

De Login methode and username klasse variabele heeft niets te maken met de EmailMessage klassen hoofddoel. Daarom zeggen we dat er een lage cohesie is.

Een voorbeeld van high coupling

Bijvoorbeeld iPods. Eens de batterij kapot is moet je een nieuwe iPod kopen, want de batterij is gesoldeerd in het apparaat, en kan dus niet loskomen. Bij lage koppeling (of loosly coupled) zou je de batterij moeten kunnen vervangen. Deze zelfde 1:1 relatie gaat op in software engineering.

Een voorbeeld van high coupling:

class A
{
    elementA;

    MethodA()
    {
        if(elementA)
            return new classB().elementB;
    }
    MethodC()
    {
        new classB().MethodB();
    }
}

class B
{
    elementB;
    MethodB()
    {
        //..
    }
}

Waarom high coupling? Klasse A instantiëert objecten van klasse B, en heeft toegang tot variabelen (elementB). Op deze manier is klasse A erg afhankelijk van klasse B. Waarom afhankelijk? Als we beslissen om een extra parameter toe te voegen in de constructor van B en de default constructor private te maken. Dan moeten we elk gebruik van klasse B aanpassen (dus aanpassingen in klasse A!).

Wat is de oplossing?

We kunnen tight coupling oplossen door de dependencies te inverteren. Dit is het toevoegen van een extra laag. Bijvoorbeeld een interface toevoegen. Op deze manier zal klasseA enkel afhankelijk zijn van de interface en niet van de actuele implementatie van klasse B.

class A
{
    elementA;
    ISomeInteface _interface;

    A(ISomeInterface i)
    {
        _interface = i;
    }

    MethodA()
    {
        if(elementA)
            _interface.elementB;
    }

    MethodC()
    {
        _interface.MethodB();
    }
}

interface ISomeInterface
{
    MethodB();
    prop elementB;
}

SRP voorbeeld

public class Werknemer
{
    Database db;
    public Werknemer()
    {
        db = new Database();
    }
    void Insert(){

        try {

            string sql = "insert into werknemers(voornaam,achternaam,stad) values ('Tom', 'Peeters', 'Antwerpen')";
            db.query(sql);
        }
        catch(Exception e)
        {
            //Log error
            System.IO.File.WriteAllText(@"c:\Error.txt", e.ToString());
        }
    }

    void Delete()
    {

    }

    void Update()
    {

    }
}

De werknemer klasse is nu verantwoordelijk voor CRUD operaties, maar ook voor het loggen van errors. Dus meer dan 1 verantwoordelijkeheid. Indien we beslissen om niet meer naar een bestand te loggen, moeten we de klasse aanpassen.

Daarom is het beter om dit als volgt te coderen:

public class Werknemer
{
    Database db;
    FileLogger logger;
    public Werknemer()
    {
        db = new Database();
        logger = new FileLogger();
    }
    void Insert(){

        try {

            string sql = "insert into werknemers(voornaam,achternaam,stad) values ('Tom', 'Peeters', 'Antwerpen')";
            db.query(sql);
        }
        catch(Exception e)
        {
            //Log error
            logger.Log(e.ToString());
        }
    }  
}

De klasse FileLogger:

public class FileLogger
{
    public void Log(string error)
    {
        System.IO.File.WriteAllText(@"c:\Error.txt", e.ToString());
    }
}

Met deze FileLogger verhoog je de "coupling" graad, en moet je een extra laag toevoegen, bijvoorbeeld een interface.

public interface ILogger
{
    void Log(string error);
}

public class FileLogger:ILogger
{
}

public class Werknemer
{
    ILogger log;
    public Werknemer(ILogger _log){
        log = _log
    }
}

static void Main(string[] args) 
{

    ILogger log = new FileLogger();
    Werknemer wn = new Werknemer(log);

}

Single responsibility is niet enkel op klasse maar ook op method niveau.

Single Responsibility op method niveau

Probleemstelling

Er is je gevraagd om software te schrijven voor een online video shop. Het programma berekent en print de rekening van een klant bij onze online shop. Onderstaande paragraaf geeft ons de voorbeeldcode van het programma. We zullen deze oplossing grondig analyseren en bekijken hoe we de code kunnen verbeteren. Aan het programma wordt meegegeven welke film de klant heeft gehuurd, en voor hoe lang. Daarna wordt de rekening gemaakt – afhankelijk van hoe lang de film gehuurd geweest is, en welk type film (nieuwe release, kinder, gewone). UML notatie:

Voorbeeld van de MAIN functie

(altijd goed om je architectuur uit te testen door in je main een voorbeeld applicatie te laten draaien)

static void Main(string[] args) 
{ 
    List<Customer> _list = new List<Customer>(); 

    Customer c = new Customer("Peeters"); 
    c.AddRental(new Rental(new Movie("Godfather", 0),3)); 
    _list.Add(c); 

    Customer c2 = new Customer("Vandeperre"); 
    c2.AddRental(new Rental(new Movie("Lion King", 2),2)); 
    _list.Add(c2); 


    Customer c3 = new Customer("Verlinden"); 
    c3.AddRental(new Rental(new Movie("Rundskop", 1),4)); 
    _list.Add(c3); 


    Customer c4 = new Customer("Dams"); 
    c4.AddRental(new Rental(new Movie("Top Gun", 0),1)); 
    _list.Add(c4); 

    foreach (Customer cust in _list) 
    { 
        Console.WriteLine( cust.Statement() ); 
    } 

}

Movie klasse .. een simpele klasse

public class Movie 
{ 
    public  const int CHILDRENS = 2; 
    public  const int REGULAR = 0; 
    public  const int NEW_RELEASE = 1;      

    public Movie(string title, int priceCode) 
    { 
        Title = title; 
        PriceCode = priceCode; 
    } 

    public int PriceCode { get; set; } 

    public string Title { get; set; } 
}

Rental klasse

Deze klasse stelt voor hoe lang een klant een bepaalde film gehuurd heeft.

public class Rental 
{ 
    private Movie _movie; 

    public Rental(Movie movie, int daysRented) 
    { 
        _movie = movie; 
        DaysRented = daysRented; 
    } 

    public int DaysRented { get; set; } 

    public Movie GetMovie() 
    { 
        return _movie; 
    } 
}

Customer klasse

Deze klasse stelt de klant van de winkel voor

public class Customer
{

    List<Rental> _rentals = new List<Rental>();

    public Customer(string name)
    {
        Name = name;
    }

    public void AddRental(Rental arg)
    {
        _rentals.Add(arg);
    }

    public string Name { get; }

    public string Statement()
    {
        double totalAmount = 0;

        string result = "Rental Record for " + Name + "\n";

        foreach (Rental r in _rentals)
        {
            double thisAmount = 0;
            switch (r.GetMovie().PriceCode)
            {
                case Movie.REGULAR:
                    thisAmount += 2;
                    if (r.DaysRented > 2)
                    {
                        thisAmount += (r.DaysRented - 2) * 1.5;
                    }
                    break;

                case Movie.NEW_RELEASE:
                    thisAmount += r.DaysRented * 3;
                    break;

                case Movie.CHILDRENS:
                    thisAmount += 1.5;
                    if (r.DaysRented > 3)
                    {
                        thisAmount += (r.DaysRented - 3) * 1.2;
                    }
                    break;
            }

            //Show figures for this rental 
            result += "\t" + r.GetMovie().Title + "\t" + thisAmount.ToString() + "\n";
            totalAmount += thisAmount;
        }

        //Add footer lines 
        result += "Amount owned is " + totalAmount.ToString() + "\n";

        return result;

    }
}

Analyse van onze architectuur

Voor een dergelijke (simpele) applicatie is design/architectuur niet zo belangrijk. We zien echter dat dit niet echt object georiënteerde code is, wat een invloed heeft op het gemak waarmee de toepassing kan uitgebreid en veranderd worden.

Enkele bemerkingen: de statement functie in onze Customer klasse is te lang en doet te veel. Veel zaken die we hier in doen, zouden naar andere klasses overgedragen moeten worden.

Ook al werkt ons programma (mooi geschreven code of lelijke code speelt echt geen rol voor een compiler), we moeten ons steeds het volgende afvragen: als in onze applicatie toevoegingen of veranderingen moeten aangebracht worden, moet er iemand zijn die dit kan klaar spelen, en een zwak gedesigned systeem is moeilijk te veranderen. Het vergt dan heel wat analysetijd van de programmeur om je programma te doorgronden.

Een voorbeeld van verandering: stel dat je klant vraagt om je rekening ook op een webpagina in HTML af te drukken. Welke impact heeft dit op je programma? Als we naar onze code kijken, merken we dat voor dergelijke vraagstelling het niet mogelijk is code te hergebruiken. Dus moeten we een nieuwe functie maken, die veel gedrag van de reeds bestaande statement functie kopieert. Op zich nog niet echt een probleem, want met wat copy-paste werk kan je de statement functie dupliceren en hernoemen naar htmlstatement() en de result string aanpassen met bijvoorbeeld: result+=”<b>”blabla</b>”.

Maar bedenk eens wat je allemaal moet doen als één regel in het rekening maken verandert? Je moet zowel aanpassingen maken in de statement als de htmlstatement functie, wat gegarandeerd fouten (bugs) zal introduceren!

Nog een andere opmerking. Als de winkel beslist om de classificatie (gewone film, kinder, nieuwe release) te veranderen, maar nog niet zeker is hoe, kan het zijn dat ze je vragen de mogelijke ideeën uit te testen. Dat heeft dan ook een invloed op hoe kosten voor films en huurpunten worden berekend. Als professionele software ontwikkelaar in spe ga ik je reeds verwittigen dat dergelijke veranderingen heel regelmatig voorkomen!

De statement() functie is de plaats waar de veranderingen in classificatie en berekeningen gebeuren. Dus ook niet te vergeten consistente veranderingen te maken in de htmlstatement() functie. Als de berekeningsmethodes steeds complexer worden, zal het met ons design ook steeds moeilijker worden om deze veranderingen door te voeren.

Wat nu volgt zijn voorstellen om onze software architectuur stap voor stap te veranderen totdat we object georiënteerde code hebben geschreven die ons in staat stelt dergelijke veranderingen op een makkelijke manier te realiseren.

Analyseren van de statement functie

Tracht steeds korte functies/methodes te schrijven. Tracht lange functies onder te verdelen in kleinere delen. Kleinere stukken code zijn veel eenvoudiger te onderhouden! Om een functie te verdelen tracht je bij elkaar horende blokken te vinden. Een goede manier is om naar lokale scope variabelen te zoeken. Bijvoorbeeld thisAmount en Rental r, waarbij r niet wordt veranderd, terwijl thisAmount wel. Elke variabele die niet wordt veranderd, kunnen we als argument doorgeven. Indien er variabelen zijn die wel worden veranderd kunnen we, indien er maar 1 is, deze terug retourneren.

We zoeken in onze statement() functie naar deze lijnen code:

switch( r.GetMovie().PriceCode ) 
{ 
    case Movie.REGULAR: 
        thisAmount += 2; 
        if (r.GetDaysRented() > 2) 
        { 
            thisAmount += (r.GetDaysRented() - 2) * 1.5; 
        } 
        break; 

    case Movie.NEW_RELEASE: 
        thisAmount += r.GetDaysRented() * 3; 
        break; 

    case Movie.CHILDRENS: 
        thisAmount += 1.5; 
        if (r.GetDaysRented() > 3) 
        { 
            thisAmount += (r.GetDaysRented() - 3) * 1.5; 
        } 
        break; 
}

En maken hiervoor een aparte functie:

private double AmountFor(Rental r) 
{ 
    double thisAmount=0; 
    switch (r.GetMovie().GetPriceCode()) 
    { 
        case Movie.REGULAR: 
            thisAmount += 2; 
            if (r.GetDaysRented() > 2) 
            { 
                thisAmount += (r.GetDaysRented() - 2) * 1.5; 
            } 
            break; 

        case Movie.NEW_RELEASE: 
            thisAmount += r.GetDaysRented() * 3; 
            break; 

        case Movie.CHILDRENS: 
            thisAmount += 1.5; 
            if (r.GetDaysRented() > 3) 
            { 
                thisAmount += (r.GetDaysRented() - 3) * 1.5; 
            } 
            break; 
    } 
    return thisAmount; 
}

Terwijl we in de statement functie deze verandering maken:

foreach (Rental r in _rentals) 
{ 
    double thisAmount = 0; 
    thisAmount = AmountFor(r); 
    //...

(zie volledige C# code - project SoftwareArchitectuur2) [TODO]

Analyse van AmountFor functie

Als we naar onze nieuwe AmountFor(Rental r) functie kijken, valt het op dat we hier met Rental data werken, en eigenlijk geen data van de customer klasse gebruiken. In de meeste gevallen moeten functies/methodes in die klasse staan vanwaar ze data gebruiken, dus in dit geval van de Rental klasse.

public double GetCharge() 
{ 
    double result = 0; 
    switch (GetMovie().GetPriceCode()) 
    { 
        case Movie.REGULAR: 
            result += 2; 
            if (GetDaysRented() > 2) 
            { 
                result += (GetDaysRented() - 2) * 1.5; 
            } 
            break; 

        case Movie.NEW_RELEASE: 
            result += GetDaysRented() * 3; 
            break; 

        case Movie.CHILDRENS: 
            result += 1.5; 
            if (GetDaysRented() > 3) 
            { 
                result += (GetDaysRented() - 3) * 1; 
            } 
            break; 
    } 
    return result; 
}

Bij deze heb ik ook de naam van de functie veranderd in GetCharge(), omwille van de duidelijkheid. Tracht altijd naamgevingen te gebruiken die direct duidelijk maken wat je programmeert. Dus in de Customer klasse staat nu

public string Statement() 
{ 
    double totalAmount = 0; 
    int frequentRenterPoints = 0; 

    string result = "Rental Record for " + GetName() + "\n"; 

    foreach (Rental r in _rentals) 
    { 
        double thisAmount = 0; 
        thisAmount += r.GetCharge(); 

    // etc.

Het klasse diagramma is nu veranderd naar:

Als we terug naar de statement() functie kijken dan is de variabele thisAmount redundant, en veranderen we naar:

public string Statement() 
{ 
    double totalAmount = 0; 
    int frequentRenterPoints = 0; 

    string result = "Rental Record for " + GetName() + "\n"; 

    foreach (Rental r in _rentals) 
    { 


        //Show figures for this rental 
        result += "\t" + r.GetMovie().GetTitle() + "\t" + r.GetCharge().ToString() + "\n"; 
        totalAmount += r.GetCharge(); 
    } 

    //Add footer lines 
    result += "Amount owned is " + totalAmount.ToString() + "\n"; 

    return result; 
}

Best is om tijdelijke variabelen te verwijderen, omdat je makkelijk vergeet waarvoor ze dienen. Je zou in bovenstaand geval toch kunnen kiezen voor een temporary variabele thisAmount, omdat de getCharge() tweemaal wordt opgeroepen dus tweemaal een berekening maakt, als we dan naar performantie kijken.

In de Customer klasse:

private double getTotalCharge() 
{ 
    double result = 0; 

    foreach (Rental r in _rentals) 
    { 
        result += r.GetCharge(); 
    } 

    return result; 
}

Met de statement functie als:

public string Statement() 
{ 
    string result = "Rental Record for " + GetName() + "\n"; 

    foreach (Rental r in _rentals) 
    { 

        //Show figures for this rental 
        result += "\t" + r.GetMovie().GetTitle() + "\t" +  r.GetCharge().ToString() + "\n";                 
    } 

    //Add footer lines 
    result += "Amount owned is " +getTotalCharge().ToString() + "\n"; 
    result += "You earned " + getTotalFrequentRenterPoints().ToString() + "frequent renter points"; 

    return result; 
}

HTMLStatement() functie

In plaats van tekst te loggen wil ik mijn prijsberekening naar een HTML pagina schrijven. Dit is nu vrij simpel, en bij veranderingen in de prijsberekening moet ik de customer klasse niet meer aanpassen!

public string HtmlStatement() 
{ 
    string result = "<h1>Rental Record for " + GetName() + "</h1>"; 

    foreach (Rental r in _rentals) 
    { 
        //Show figures for this rental 
        result += "<p>" + r.GetMovie().GetTitle() + " &nbsp; " + r r.GetCharge().ToString() + "</br></p"; 
    } 

    //Add footer lines 
    result += "<p>Amount owned is " + getTotalCharge().ToString() + "</br>"; 
    result += "You earned " + getTotalFrequentRenterPoints().ToString() + "frequent renter points</p>"; 

    return result; 
}

Bij een verandering aan de berekening, of toevoeging van nieuwe types films worden de statement functies niet meer gewijzigd, waardoor we duidelijk meer onderhoudvriendelijke code hebben geschreven.

public double GetCharge() 
{ 
    double thisAmount = 0; 
    switch (GetMovie().GetPriceCode()) 
    { 
        case Movie.REGULAR: 
            thisAmount += 2; 
            if (GetDaysRented() > 2) 
            { 
                thisAmount += (GetDaysRented() - 2) * 1.5; 
            } 
            break; 

        case Movie.NEW_RELEASE: 
            thisAmount += GetDaysRented() * 3; 
            break; 

        case Movie.CHILDRENS: 
            thisAmount += 1.5; 
            if (GetDaysRented() > 3) 
            { 
                thisAmount += (GetDaysRented() - 3) * 1; 
            } 
            break; 
    } 
    return thisAmount; 
}

Het valt hier op dat we in de Rental klasse met een Movie object werken. Logischerwijze zou deze functie beter in de movie klasse staan. Het is een slecht idee om een switch te doen op een attribuut van een ander object!

We moeten dan wel het aantal huurdagen meegeven als parameter van deze nieuwe functie. Dus eigenlijk gebruikt deze functie 2 stukken data – type film, en aantal huurdagen. Waarom dan toch naar Movie klasse brengen, en daysRented meegeven als argument? Wel , de voorgestelde veranderingen gingen om type film (wat te doen als nieuw type wordt geïntroduceerd ), daarom is het logisch om de type informatie zo compact mogelijk te bundelen (in 1 functie ipv 2 functies (als je het type zou doorgeven als parameter)).

De Rental klasse:

public double GetCharge() 
{ 
    return GetMovie().GetCharge(DaysRented);              
}

In de klasse Movie zit nu:

public double GetCharge(int daysRented) 
{ 
    double result = 0; 
    switch (GetPriceCode()) 
    { 
        case Movie.REGULAR: 
            result += 2; 
            if (daysRented > 2) 
            { 
                result += (daysRented - 2) * 1.5; 
            } 
            break; 

        case Movie.NEW_RELEASE: 
            result += daysRented * 3; 
            break; 

        case Movie.CHILDRENS: 
            result += 1.5; 
            if (daysRented > 3) 
            { 
                result += (daysRented - 3) * 1; 
            } 
            break; 
    } 
    return result; 
}

SRP, the law of demeter

Dit is het principe van "least knowledge", is een object-oriented software design principe. Een methode van een object mag enkel wie oproepen:

  • het object zelf

  • een argument van de methode

  • elk object dat in de methode gecreerd is

  • alle properties, variabelen van het object zelf

Open-Closed Principe (OCP)

Het open/closed principe stelt dat klasses of functies open moeten zijn voor uitbreiding, maar gesloten voor wijziging!

Open for extension, closed for modification

Gesloten voor wijziging betekent dat het gedrag mag veranderd worden zonder de broncode aan te passen..

Een typisch voorbeeld:

public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }
}

Nu bouwen we een applicatie die de oppervlakte van een collectie rechthoeken zal berekenen.

public class OppBerekenaar
{

    public double Opp(Rechthoek[] shapes)
    {
        double opp = 0;
        foreach (var shape in shapes)
        {
            opp += shape.Width * shape.Height;
        }

        return opp;
    }
}

En we schrijven ons testprogramma:

static void Main(string[] args)
{

    Rechthoek rh1 = new Rechthoek() { Width = 49, Height = 30 };
    Rechthoek rh2 = new Rechthoek() { Width = 30, Height = 20 };
    Rechthoek rh3 = new Rechthoek() { Width = 22, Height = 10 };
    Rechthoek rh4 = new Rechthoek() { Width = 44, Height = 35 };

    Rechthoek[] rechthoeken = new Rechthoek[4];
    rechthoeken[0] = rh1;
    rechthoeken[1] = rh2;
    rechthoeken[2] = rh3;
    rechthoeken[3] = rh4;

    OppBerekenaar opb = new OppBerekenaar();
    double totaal = opb.Opp(rechthoeken);

    Console.WriteLine("totaal: "+ totaal);
}

De volgende vraag komt op: kunnen we het programma uitbreiden zodat we ook de oppervlakte van een cirkel kunnen berekenen?

We passsen de code als volgt aan:

public double Opp(Object[] shapes)
{
    double opp = 0;
    foreach (var shape in shapes)
    {
        if(shape is Rechthoek)
            opp += ((Rechthoek)shape).Width * ((Rechthoek)shape).Height; //CAST to Rechthoek


        if (shape is Cirkel)
            opp += ((Cirkel)shape).Straal * ((Cirkel)shape).Straal * Math.PI;  //CAST to cirkel
    }

    return opp;
}

Wat later krijgen we de vraag om de OppBereken klasse uit te breiden zodat we ook de oppervlakte van driehoeken kunnen opnemen. Dit druist in tegen het principe "gesloten voor wijziging!"

OPC oplossing

Maak gebruik van abstractie.

In .NET betekent abstractie : gebruik maken van interfaces, of abstracte klassen.

Wat is een interface?

Je hebt geleerd dat een klasse slechts van één klasse kan erven. Een klasse kan echter ook nog interfaces implementeren. Wanneer een klasse een interface implementeert sluit de klasse een contract met de compiler dat de klasse zich zal gedragen volgens de interface. Concreet betekent dit dat in de klasse alle eigenschappen (properties) en methoden van de interface moet implementeren. Een interface bevat dus eigenlijk enkel een lijst van eigenschappen en methoden die nog geen concrete invulling hebben.

Volgens WIKIPEDIA: Een interface in de programmeertaal als Java of C# is een soort abstracte klasse die een interface aanduidt die klassen kunnen implementeren. Een interface wordt aangeduid met het sleutelwoord interface en bevat alleen ongedefinieerde methoden.

Wat is een abstracte klasse?

In de informatica is een abstracte klasse een klasse die ongedefinieerde methoden kan bevatten. Deze methoden worden geïmplementeerd in een subklasse van de abstracte klasse. Het is niet mogelijk om een object te maken van abstracte klassen maar wel van niet-abstracte subklassen. Door middel van overerving is het wel mogelijk om de methoden die wel gedefinieerd zijn in de abstracte klasse te erven en in de subklassen te gebruiken.

Een klasse kan meerdere interfaces implementeren maar alleen van één klasse (rechtstreeks) overerven. Een verschil met abstracte klassen is dat een abstracte klasse wel gedefinieerde methoden kan bevatten maar een interface bevat alleen ongedefinieerde methoden.

Om aan het OPC principe te voldoen moeten we als volgt te werk gaan:

We maken een basis klasse voor rechthoeken, cirkels, driehoeken, andere vormen, en deze definieert een abstracte methode om de oppervlakte te berekenen.

public abstract class Vorm
{
    public abstract double Oppervlakte();
}

De andere klassen leiden af van vorm:

public class Rechthoek: Vorm
{
    public int Width { get; set; }
    public int Height { get; set; }

    public override double Oppervlakte()
    {
        return Width * Height;
    }
}
public class Cirkel:Vorm
{

    public int Straal { get; set; }

    public override double Oppervlakte()
    {
        return Straal * Straal * Math.PI;
    }
}

De berekening gebeurt nu als volgt:

public class OppBerekenaar
{

    public double Opp(Vorm[] shapes)
    {
        double opp = 0;
        foreach (var shape in shapes)
        {
            opp += shape.Oppervlakte();
        }

        return opp;
    }

}

Op deze manier is de OppBerekenaar klasse gesloten voor wijziging, maar toch open voor uitbreiding!

In de praktijk

OPC zal je als ervaren programmeur sneller toepassen. Van bij de start van je ontwikkeling zal je niet altijd OPC toepassen, en accepteer dat een klasse veranderd moet worden. Maar bij nog verandering, zorg je ervoor dat je naar het OPC principe refactort.

Liskov Substitution Design Principle

TOPROCESS:

Subtypes moeten vervangbaar zijn door hun super types (parent class).

de IS-A relatie zou vervangen moeten worden door IS-VERVANGBAAR DOOR

Als voorbeeld werken we met een klasse vierkant die overerft van Rechthoek. De klasse Rechthoek heeft eigenschappen als "width" en "height", en vierkant erft deze over. Maar als voor de klasse vierkant de width OF height gekend is, ken je de waarde van de andere ook. En dit is tegen het principe van Liskov.

public class Rechthoek
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int BerekenOpp()
    {
        return Width * Height;
    }
}

De klasse Vierkant erft over van Rechthoek (maar is in programmeren een vierkant wel een rechthoek?) Een vierkant is een rechthoek met gelijke breedte en hoogte, en we kunnen de properties virtual maken in de klasse Rechthoek om dit te realiseren. Rare implementatie, niet? Maar kijk nu naar de client code..

public class Vierkant:Rechthoek
{
    public override int Width
    {
        get
        {
            return base.Width;
        }

        set
        {
            base.Width = value;
            base.Height = value;
        }
    }

    public override int Height
    {
        get
        {
            return base.Height;
        }

        set
        {
            base.Height = value;
            base.Width = value;
        }
    }
}

Client code:

 static void Main(string[] args)
{
    Rechthoek r = new Vierkant();

    r.Width = 5;
    r.Height = 10;

    Console.WriteLine(r.BerekenOpp());
}

De gebruiker weet dat r een Rechthoek is dus is hij in de veronderstelling dat hij de width en height kan aanpassen zoals in de parent klasse. Dit in acht genomen zal de gebruiker verrast zijn om 100 te zien ipv 50.

Oplossen van het LSP probleem

  • Code dat niet vervangbaar is zorgt ervoor dat polymorfisme niet werkt

  • Client code (en dit geval de Main) veronderstelt dat basis klassen kunnen vervangen worden door hun afgeleide klassen (Rechthoek r = new Vierkant())

  • Het oplossen van LSP door switch cases zorgt voor een onderhoudsnachtmerrie!

public abstract class Shape
{
    public abstract int BerekenOpp();
}

public class Rechthoek : Shape
{
    public int Width { get; set; }
    public int Height { get; set; }
    public override int BerekenOpp()
    {
        return Width * Height;
    }
}

public class Vierkant : Shape
{
    public int Side { get; set; }
    public override int BerekenOpp()
    {
        return Side * Side;
    }
}

public class OppBerekenaar
{
    public List<Shape> shapes;
    public int BerekenOppervlakte()
    {
        shapes = new List<Shape>();
        shapes.Add(new Vierkant() { Side = 10 });
        shapes.Add(new Rechthoek(){ Width = 5, Height= 20 });

        int total = 0;
        foreach(Shape s in shapes)
        {
            total += s.BerekenOpp();
        }

        return total;
    }
}

Een ander voorbeeld:

public interface ICar 
{
     void drive();
     void playRadio();
     void addLuggage();
}

Wat gebeurt er als we een Formule 1 auto hebben:

public class FormulaOneCar: ICar 
{
    public void drive() 
    {
        //Code to make it go super fast
    }

    public void addLuggage() 
    {
        throw new NotSupportedException("No room to carry luggage, sorry."); 
    }

    public void playRadio() 
    {
        throw new NotSupportedException("Too heavy, none included."); 
    }
}

De interface dient als het contract, en moet je veronderstellen dat alle auto's dit gedrag hebben.

Dit is de essentie van het Liskov Substitution Principle.

Waarom is het schenden van LSP niet goed?

Gebruik van abstracte klassen betekent dat je in de toekomst makkelijk een subklasse kan toevoegen in de werkende, geteste code. Dit is de essentie van het open closed principe. Maar wanneer je subklassen gebruikt die niet volledig de interface (abstracte klasse) supporteren moet je in de bestaande code speciale gevallen gaan definiëren.

Bijvoorbeeld:

public void DoeIets(Bird b)
{
    if(b is Pinguin) {
        //Doe iets met de pinguin
    }
    else {
        //Doe iets anders
    }
}

Interface Segregation Principle (ISP)

Clients mogen niet afhankelijk zijn over interfaces die ze niet gebruiken

Clients mogen niet gedwongen worden om interfaces te implementeren die operaties bevatten die ze niet nodig hebben, of nooit zullen gebruiken. Deze interfaces noemen we "fat" interfaces.

In plaats van een fat interface is het beter om deze verder op te delen in meer specifieke interfaces.

public interface ILog
{
    void Log(string message);

    void OpenConnection();

    void CloseConnection();
}


public class DBLogger : ILog
{
    public void Log(string message)
    {
        //Code to log data to a database
    }

    public void OpenConnection()
    {
        //Opens database connection
    }

    public void CloseConnection()
    {
        //Closes the database connection
    }

}

public class FileLogger : ILog
{
    public void Log(string message)
    {
        //Code to log to a file           
    }

    public void CloseConnection()
    {
        throw new NotImplementedException();
    }

    public void OpenConnection()
    {
        throw new NotImplementedException();
    }

}

Hoe ISP oplossen:

public interface ILog
{
    void Log(string message);     

}

public interface IDBLog :ILog
{
    void OpenConnection();

    void CloseConnection();
}

public interface IFileLog :ILog
{
    void CheckFileSize();