Onlangs hebben we Top 30 Java Interview Questions behandeld, en vandaag willen we dieper ingaan op SOLID. Deze principes, bedacht door de softwaregoeroe Robert C. Martin (ook bekend als Uncle Bob), zijn:

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

Maar waarom zou je je hier druk om maken? Stel je voor dat je een Lego-toren bouwt. SOLID-principes zijn als de handleiding die ervoor zorgt dat je toren niet omvalt wanneer je nieuwe stukken toevoegt. Ze maken je code:

  • Leesbaarder (je toekomstige zelf zal je dankbaar zijn)
  • Makkelijker te onderhouden en aan te passen
  • Robuuster tegen veranderingen in vereisten
  • Minder vatbaar voor bugs bij het toevoegen van nieuwe functies

Klinkt goed, toch? Laten we elk principe eens nader bekijken en zien hoe ze in de praktijk werken.

Single Responsibility Principle (SRP): Eén Taak, Eén Klasse

Het Single Responsibility Principle is als de Marie Kondo van programmeren - het gaat allemaal om het opruimen van je klassen. Het idee is simpel: een klasse moet één, en slechts één, reden hebben om te veranderen.

Laten we eens kijken naar een klassiek voorbeeld van een SRP-schending:


public class Report {
    public void generateReport() {
        // Genereer rapportinhoud
    }

    public void saveToDatabase() {
        // Sla rapport op in database
    }

    public void sendEmail() {
        // Verstuur rapport via e-mail
    }
}

Deze Report klasse doet veel te veel. Het genereert het rapport, slaat het op en verstuurt het. Het is als een Zwitsers zakmes - handig, maar niet ideaal voor een specifieke taak.

Laten we dit refactoren om SRP te volgen:


public class ReportGenerator {
    public String generateReport() {
        // Genereer en retourneer rapportinhoud
    }
}

public class DatabaseSaver {
    public void saveToDatabase(String report) {
        // Sla rapport op in database
    }
}

public class EmailSender {
    public void sendEmail(String report) {
        // Verstuur rapport via e-mail
    }
}

Nu heeft elke klasse een enkele verantwoordelijkheid. Als we moeten veranderen hoe rapporten worden gegenereerd, hoeven we alleen de ReportGenerator klasse aan te passen. Als het databaseschema verandert, passen we alleen DatabaseSaver aan. Deze scheiding maakt onze code meer modulair en makkelijker te onderhouden.

Open/Closed Principle (OCP): Open voor Uitbreiding, Gesloten voor Wijziging

Het Open/Closed Principle klinkt als een paradox, maar het is eigenlijk heel slim. Het stelt dat software-entiteiten (klassen, modules, functies, enz.) open moeten zijn voor uitbreiding, maar gesloten voor wijziging. Met andere woorden, je moet het gedrag van een klasse kunnen uitbreiden zonder de bestaande code te wijzigen.

Laten we eens kijken naar een veelvoorkomende OCP-schending:


public class PaymentProcessor {
    public void processPayment(String paymentMethod) {
        if (paymentMethod.equals("creditCard")) {
            // Verwerk creditcardbetaling
        } else if (paymentMethod.equals("paypal")) {
            // Verwerk PayPal-betaling
        }
        // Meer betaalmethoden...
    }
}

Elke keer dat we een nieuwe betaalmethode willen toevoegen, moeten we deze klasse wijzigen. Dat is een recept voor bugs en hoofdpijn.

Hier is hoe we dit kunnen refactoren om OCP te volgen:


public interface PaymentMethod {
    void processPayment();
}

public class CreditCardPayment implements PaymentMethod {
    public void processPayment() {
        // Verwerk creditcardbetaling
    }
}

public class PayPalPayment implements PaymentMethod {
    public void processPayment() {
        // Verwerk PayPal-betaling
    }
}

public class PaymentProcessor {
    public void processPayment(PaymentMethod paymentMethod) {
        paymentMethod.processPayment();
    }
}

Nu, wanneer we een nieuwe betaalmethode willen toevoegen, maken we gewoon een nieuwe klasse die PaymentMethod implementeert. De PaymentProcessor klasse hoeft helemaal niet te veranderen. Dat is de kracht van OCP!

Liskov Substitution Principle (LSP): Als Het Eruit Ziet Als Een Eend En Kwaakt Als Een Eend, Moet Het Een Eend Zijn

Het Liskov Substitution Principle, genoemd naar computerwetenschapper Barbara Liskov, stelt dat objecten van een superklasse vervangbaar moeten zijn door objecten van zijn subklassen zonder de correctheid van het programma te beïnvloeden. In eenvoudigere termen, als klasse B een subklasse is van klasse A, moeten we B overal kunnen gebruiken waar we A gebruiken zonder dat er iets misgaat.

Hier is een klassiek voorbeeld van het schenden van LSP:


public class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height);
    }
}

Dit lijkt logisch op het eerste gezicht - een vierkant is een speciaal soort rechthoek, toch? Maar het schendt LSP omdat je een Square niet overal kunt gebruiken waar je een Rectangle gebruikt zonder onverwacht gedrag. Als je de breedte en hoogte van een Square afzonderlijk instelt, krijg je onverwachte resultaten.

Een betere aanpak zou zijn om compositie te gebruiken in plaats van overerving:


public interface Shape {
    int getArea();
}

public class Rectangle implements Shape {
    private int width;
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

public class Square implements Shape {
    private int side;

    public Square(int side) {
        this.side = side;
    }

    public int getArea() {
        return side * side;
    }
}

Nu zijn Square en Rectangle aparte implementaties van de Shape interface, en vermijden we de LSP-schending.

Interface Segregation Principle (ISP): Klein is Mooi

Het Interface Segregation Principle stelt dat geen enkele client gedwongen mag worden om afhankelijk te zijn van methoden die het niet gebruikt. Met andere woorden, maak geen dikke interfaces; splits ze op in kleinere, meer gerichte interfaces.

Hier is een voorbeeld van een opgeblazen interface:


public interface Worker {
    void work();
    void eat();
    void sleep();
}

public class Human implements Worker {
    public void work() { /* ... */ }
    public void eat() { /* ... */ }
    public void sleep() { /* ... */ }
}

public class Robot implements Worker {
    public void work() { /* ... */ }
    public void eat() { throw new UnsupportedOperationException(); }
    public void sleep() { throw new UnsupportedOperationException(); }
}

De Robot klasse wordt gedwongen om methoden te implementeren die het niet nodig heeft. Laten we dit oplossen door de interface te scheiden:


public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

public class Human implements Workable, Eatable, Sleepable {
    public void work() { /* ... */ }
    public void eat() { /* ... */ }
    public void sleep() { /* ... */ }
}

public class Robot implements Workable {
    public void work() { /* ... */ }
}

Nu implementeert onze Robot alleen wat het nodig heeft. Dit maakt onze code flexibeler en minder vatbaar voor fouten.

Dependency Inversion Principle (DIP): Hoog-Niveau Modules Moeten Niet Afhangen van Laag-Niveau Modules

Het Dependency Inversion Principle klinkt misschien ingewikkeld, maar het is eigenlijk heel eenvoudig. Het stelt dat:

  1. Hoog-niveau modules niet afhankelijk moeten zijn van laag-niveau modules. Beide moeten afhankelijk zijn van abstracties.
  2. Abstracties mogen niet afhankelijk zijn van details. Details moeten afhankelijk zijn van abstracties.

Hier is een voorbeeld van het schenden van DIP:


public class LightBulb {
    public void turnOn() {
        // Zet de gloeilamp aan
    }

    public void turnOff() {
        // Zet de gloeilamp uit
    }
}

public class Switch {
    private LightBulb bulb;

    public Switch() {
        bulb = new LightBulb();
    }

    public void operate() {
        // Schakelaarlogica
    }
}

In dit voorbeeld is de Switch klasse (hoog-niveau module) direct afhankelijk van de LightBulb klasse (laag-niveau module). Dit maakt het moeilijk om de Switch te veranderen om andere apparaten te bedienen.

Laten we dit refactoren om DIP te volgen:


public interface Switchable {
    void turnOn();
    void turnOff();
}

public class LightBulb implements Switchable {
    public void turnOn() {
        // Zet de gloeilamp aan
    }

    public void turnOff() {
        // Zet de gloeilamp uit
    }
}

public class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void operate() {
        // Schakelaarlogica met device.turnOn() en device.turnOff()
    }
}

Nu zijn zowel Switch als LightBulb afhankelijk van de Switchable abstractie. We kunnen dit gemakkelijk uitbreiden om andere apparaten te bedienen zonder de Switch klasse te veranderen.

Samenvatting: SOLID als een Rots

SOLID-principes lijken misschien veel om in één keer te begrijpen, maar ze zijn ongelooflijk krachtige hulpmiddelen in je OOP-gereedschapskist. Ze helpen je om code te schrijven die:

  • Makkelijker te begrijpen en te onderhouden is
  • Flexibeler en aanpasbaar aan veranderingen is
  • Minder vatbaar voor bugs is bij het toevoegen van nieuwe functies

Onthoud, SOLID is geen strikte set regels, maar eerder een gids om je te helpen betere ontwerpbeslissingen te nemen. Naarmate je deze principes in je dagelijkse codering toepast, zul je patronen zien ontstaan en zal je code vanzelf robuuster en onderhoudbaarder worden.

Dus, de volgende keer dat je een klasse ontwerpt of code refactort, vraag jezelf af: "Is dit SOLID?" Je toekomstige zelf (en je team) zullen je er dankbaar voor zijn!

"Het geheim van het bouwen van grote apps is om nooit grote apps te bouwen. Breek je applicaties op in kleine stukjes. Assembleer vervolgens die testbare, hapklare stukjes in je grote applicatie" - Justin Meyer

Veel programmeerplezier, en moge je code altijd SOLID zijn!