CodeGym /Java Blog /Willekeurig /SOLID: Vijf basisprincipes van klassenontwerp in Java
John Squirrels
Niveau 41
San Francisco

SOLID: Vijf basisprincipes van klassenontwerp in Java

Gepubliceerd in de groep Willekeurig
Klassen zijn de bouwstenen van applicaties. Net als bakstenen in een gebouw. Slecht geschreven klassen kunnen uiteindelijk problemen veroorzaken. SOLID: Vijf basisprincipes van klassenontwerp in Java - 1Om te begrijpen of een klasse correct is geschreven, kunt u controleren hoe deze voldoet aan "kwaliteitsnormen". In Java zijn dit de zogenaamde SOLID-principes, en we gaan erover praten.

SOLIDE principes op Java

SOLID is een acroniem gevormd uit de hoofdletters van de eerste vijf principes van OOP en klasseontwerp. De principes werden begin jaren 2000 uitgedrukt door Robert Martin en de afkorting werd later geïntroduceerd door Michael Feathers. Dit zijn de SOLID-principes:
  1. Principe van één enkele verantwoordelijkheid
  2. Open Gesloten Principe
  3. Liskov-substitutieprincipe
  4. Interfacescheidingsprincipe
  5. Afhankelijkheid Inversie Principe

Single Responsibility Principle (SRP)

Dit principe stelt dat er nooit meer dan één reden mag zijn om van klasse te veranderen. Elk object heeft één verantwoordelijkheid, die volledig is ingekapseld in de klasse. Alle diensten van een klas zijn erop gericht deze verantwoordelijkheid te ondersteunen. Dergelijke klassen zijn altijd eenvoudig aan te passen indien nodig, omdat duidelijk is waar de klasse wel en niet verantwoordelijk voor is. Met andere woorden, we zullen veranderingen kunnen aanbrengen en niet bang zijn voor de gevolgen, dat wil zeggen de impact op andere objecten. Bovendien is dergelijke code veel gemakkelijker te testen, omdat uw tests één stuk functionaliteit afzonderlijk van alle andere dekken. Stel je een module voor die bestellingen verwerkt. Als een bestelling correct is gevormd, slaat deze module deze op in een database en stuurt een e-mail om de bestelling te bevestigen:

public class OrderProcessor {

    public void process(Order order){
        if (order.isValid() && save(order)) {
            sendConfirmationEmail(order);
        }
    }

    private boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }

    private void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Deze module kan om drie redenen veranderen. Ten eerste kan de logica voor het verwerken van bestellingen veranderen. Ten tweede kan de manier waarop bestellingen worden opgeslagen (databasetype) veranderen. Ten derde kan de manier waarop bevestiging wordt verzonden veranderen (stel bijvoorbeeld dat we een sms moeten sturen in plaats van een e-mail). Het principe van één enkele verantwoordelijkheid houdt in dat de drie aspecten van dit probleem in feite drie verschillende verantwoordelijkheden zijn. Dat betekent dat ze in verschillende klassen of modules moeten zitten. Het combineren van verschillende entiteiten die op verschillende tijdstippen en om verschillende redenen kunnen veranderen, wordt als een slechte ontwerpbeslissing beschouwd. Het is veel beter om een ​​module op te splitsen in drie afzonderlijke modules, die elk een enkele functie vervullen:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}

Open Gesloten Principe (OCP)

Dit principe wordt als volgt beschreven: software-entiteiten (klassen, modules, functies, etc.) moeten open staan ​​voor uitbreiding, maar gesloten voor wijziging . Dit betekent dat het mogelijk moet zijn om het externe gedrag van een klasse te veranderen zonder wijzigingen aan te brengen in de bestaande code van de klasse. Volgens dit principe zijn klassen zo ontworpen dat het aanpassen van een klasse om aan specifieke voorwaarden te voldoen, simpelweg vereist dat deze wordt uitgebreid en sommige functies worden overschreven. Dit betekent dat het systeem flexibel moet zijn, in staat moet zijn om in veranderende omstandigheden te werken zonder de broncode te wijzigen. Voortbordurend op ons voorbeeld met betrekking tot orderverwerking, stel dat we enkele acties moeten uitvoeren voordat een bestelling wordt verwerkt en nadat de bevestigingsmail is verzonden. In plaats van deOrderProcessorclass zelf, zullen we het uitbreiden om ons doel te bereiken zonder het Open Closed-principe te schenden:

public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }
    
    private void beforeProcessing() {
        // Take some action before processing the order
    }
    
    private void afterProcessing() {
        // Take some action after processing the order
    }
}

Liskov-substitutieprincipe (LSP)

Dit is een variatie op het eerder genoemde open gesloten principe. Het kan als volgt worden gedefinieerd: objecten kunnen worden vervangen door objecten van subklassen zonder de eigenschappen van een programma te wijzigen. Dit betekent dat een klasse die is gemaakt door een basisklasse uit te breiden, zijn methoden moet overschrijven, zodat de functionaliteit niet in het gedrang komt vanuit het oogpunt van de klant. Dat wil zeggen, als een ontwikkelaar uw klasse uitbreidt en deze in een toepassing gebruikt, mag hij of zij het verwachte gedrag van overschreven methoden niet wijzigen. Subklassen moeten de methoden van de basisklasse overschrijven, zodat de functionaliteit vanuit het oogpunt van de client niet wordt verbroken. We kunnen dit in detail onderzoeken in het volgende voorbeeld. Stel dat we een klasse hebben die verantwoordelijk is voor het valideren van een bestelling en het controleren of alle goederen in de bestelling op voorraad zijn.isValid()methode die waar of onwaar retourneert :

public class OrderStockValidator {

    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (!item.isInStock()) {
                return false;
            }
        }

        return true;
    }
}
Stel ook dat sommige bestellingen anders moeten worden gevalideerd dan andere, bijvoorbeeld voor sommige bestellingen moeten we controleren of alle goederen in de bestelling op voorraad zijn en of alle goederen zijn verpakt. Om dit te doen, breiden we de OrderStockValidatorklasse uit door de OrderStockAndPackValidatorklasse te maken:

public class OrderStockAndPackValidator extends OrderStockValidator {

    @Override
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if ( !item.isInStock() || !item.isPacked() ){
                throw new IllegalStateException(
                     String.format("Order %d is not valid!", order.getId())
                );
            }
        }

        return true;
    }
}
Maar hier hebben we het Liskov-substitutieprincipe geschonden, want in plaats van false terug te geven als de bestelling niet kan worden gevalideerd, genereert onze methode een IllegalStateException. Klanten die deze code gebruiken, verwachten dit niet: ze verwachten een retourwaarde van true of false . Dit kan leiden tot runtime-fouten.

Interface Segregatie Principe (ISP)

Dit principe wordt gekenmerkt door de volgende stelling: de klant mag niet worden gedwongen methoden te implementeren die hij niet zal gebruiken . Het scheidingsprincipe van de interface houdt in dat interfaces die te "dik" zijn, moeten worden opgedeeld in kleinere, meer specifieke interfaces, zodat klanten die kleine interfaces gebruiken alleen weten welke methoden ze nodig hebben voor hun werk. Als gevolg hiervan, wanneer een interfacemethode verandert, zouden clients die die methode niet gebruiken, niet moeten veranderen. Bekijk dit voorbeeld: Alex, een ontwikkelaar, heeft een "rapport"-interface gemaakt en twee methoden toegevoegd: generateExcel()engeneratedPdf(). Nu wil een klant deze interface gebruiken, maar alleen rapportages in pdf-formaat gebruiken, niet in Excel. Zal deze functionaliteit deze klant tevreden stellen? Nee. De klant zal twee methoden moeten implementeren, waarvan er één grotendeels niet nodig is en alleen bestaat dankzij Alex, degene die de software heeft ontworpen. De klant gebruikt een andere interface of doet niets met de methode voor Excel-rapportages. Dus wat is de oplossing? Het is om de bestaande interface in twee kleinere te splitsen. Een voor PDF-rapporten, de andere voor Excel-rapporten. Hierdoor kunnen klanten alleen de functionaliteit gebruiken die ze nodig hebben.

Dependency Inversion Principle (DIP)

In Java wordt dit SOLID-principe als volgt beschreven: afhankelijkheden binnen het systeem worden opgebouwd op basis van abstracties. Modules van een hoger niveau zijn niet afhankelijk van modules van een lager niveau. Abstracties mogen niet afhankelijk zijn van details. Details zouden afhankelijk moeten zijn van abstracties. Software moet zo worden ontworpen dat de verschillende modules op zichzelf staan ​​en door middel van abstractie met elkaar zijn verbonden. Een klassieke toepassing van dit principe is het Spring Framework. In het Spring Framework zijn alle modules geïmplementeerd als afzonderlijke componenten die kunnen samenwerken. Ze zijn zo autonoom dat ze net zo goed in andere programmamodules dan het Spring Framework kunnen worden gebruikt. Dit wordt bereikt dankzij de afhankelijkheid van de gesloten en open principes. Alle modules bieden alleen toegang tot de abstractie, die in een andere module kan worden gebruikt. Laten we dit proberen te illustreren aan de hand van een voorbeeld. Over het beginsel van de enkele verantwoordelijkheid gesproken, we hebben deOrderProcessorklas. Laten we nog eens kijken naar de code van deze klasse:

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}
In dit voorbeeld is onze OrderProcessorklasse afhankelijk van twee specifieke klassen: MySQLOrderRepositoryen ConfirmationEmailSender. We presenteren ook de code van deze klassen:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Deze klassen zijn verre van wat we abstracties zouden noemen. En vanuit het oogpunt van het principe van afhankelijkheidsinversie zou het beter zijn om te beginnen met het creëren van enkele abstracties waarmee we in de toekomst kunnen werken, in plaats van specifieke implementaties. Laten we twee interfaces maken: MailSenderen OrderRepository). Dit zullen onze abstracties zijn:

public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Nu implementeren we deze interfaces in klassen die hiervoor al zijn voorbereid:

public class ConfirmationEmailSender implements MailSender {

    @Override
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}
We hebben het voorbereidende werk gedaan zodat onze OrderProcessorklas niet afhankelijk is van concrete details, maar van abstracties. We zullen het veranderen door onze afhankelijkheden toe te voegen aan de klassenconstructor:

public class OrderProcessor {

    private MailSender mailSender;
    private OrderRepository repository;

    public OrderProcessor(MailSender mailSender, OrderRepository repository) {
        this.mailSender = mailSender;
        this.repository = repository;
    }

    public void process(Order order){
        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }
}
Nu is onze klasse afhankelijk van abstracties, niet van specifieke implementaties. We kunnen zijn gedrag eenvoudig wijzigen door de gewenste afhankelijkheid toe te voegen op het moment dat een OrderProcessorobject wordt gemaakt. We hebben de SOLID-ontwerpprincipes in Java onderzocht. In de CodeGym-cursus leer je meer over OOP in het algemeen en de basisprincipes van programmeren met Java — niets saai en honderden uren oefenen. Tijd om een ​​paar taken op te lossen :)
Opmerkingen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION