CodeGym /Java blogg /Slumpmässig /SOLID: Fem grundläggande principer för klassdesign i Java...
John Squirrels
Nivå
San Francisco

SOLID: Fem grundläggande principer för klassdesign i Java

Publicerad i gruppen
Klasser är byggstenarna i applikationer. Precis som tegelstenar i en byggnad. Dåligt skrivna klasser kan så småningom orsaka problem. SOLID: Fem grundläggande principer för klassdesign i Java - 1För att förstå om en klass är korrekt skriven kan du kontrollera hur den uppfyller "kvalitetsstandarder". I Java är detta de så kallade SOLID-principerna, och vi ska prata om dem.

SOLIDA principer i Java

SOLID är en akronym bildad av versaler i de första fem principerna för OOP och klassdesign. Principerna uttrycktes av Robert Martin i början av 2000-talet, och sedan introducerades förkortningen senare av Michael Feathers. Här är SOLID principerna:
  1. Principen om ett enda ansvar
  2. Öppen stängd princip
  3. Liskov Substitutionsprincip
  4. Gränssnittssegregationsprincip
  5. Beroendeinversionsprincipen

Single Responsibility Principle (SRP)

Denna princip säger att det aldrig ska finnas mer än en anledning att byta klass. Varje objekt har ett ansvar, som är helt inkapslat i klassen. Alla klassens tjänster syftar till att stödja detta ansvar. Sådana klasser kommer alltid att vara lätta att ändra vid behov, eftersom det är tydligt vad klassen är och inte ansvarar för. Vi kommer med andra ord att kunna göra förändringar och inte vara rädda för konsekvenserna, det vill säga påverkan på andra objekt. Dessutom är sådan kod mycket lättare att testa, eftersom dina tester täcker en funktionalitet isolerad från alla andra. Föreställ dig en modul som behandlar beställningar. Om en beställning är korrekt utformad, sparar den här modulen den i en databas och skickar ett e-postmeddelande för att bekräfta beställningen:

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
    }
}
Denna modul kan ändras av tre skäl. För det första kan logiken för att behandla beställningar ändras. För det andra kan hur order sparas (databastyp) ändras. För det tredje kan sättet att skicka bekräftelse på ändras (anta att vi till exempel behöver skicka ett textmeddelande i stället för ett e-postmeddelande). Principen om ett enda ansvar innebär att de tre aspekterna av detta problem faktiskt är tre olika ansvarsområden. Det betyder att de bör vara i olika klasser eller moduler. Att kombinera flera enheter som kan förändras vid olika tidpunkter och av olika anledningar anses vara ett dåligt designbeslut. Det är mycket bättre att dela upp en modul i tre separata moduler, som var och en utför en enda funktion:

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);
        }
    }

}

Öppen stängd princip (OCP)

Denna princip beskrivs enligt följande: programvaruenheter (klasser, moduler, funktioner etc.) ska vara öppna för förlängning, men stängda för modifiering . Detta innebär att det ska vara möjligt att ändra en klasss yttre beteende utan att göra ändringar i klassens befintliga kod. Enligt denna princip är klasser utformade så att justering av en klass för att passa specifika förhållanden helt enkelt kräver att den utökas och vissa funktioner åsidosätts. Det innebär att systemet måste vara flexibelt, kunna arbeta under föränderliga förhållanden utan att ändra källkoden. För att fortsätta vårt exempel som involverar orderhantering, anta att vi måste utföra några åtgärder innan en beställning behandlas såväl som efter att bekräftelsemailet har skickats. Istället för att ändraOrderProcessorklassen själv, kommer vi att utöka den för att uppnå vårt mål utan att bryta mot Open Closed-principen:

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 Substitution Principle (LSP)

Detta är en variant av den öppna stängda principen som vi nämnde tidigare. Det kan definieras enligt följande: objekt kan ersättas av objekt av underklasser utan att ändra ett programs egenskaper. Detta innebär att en klass skapad genom att utöka en basklass måste åsidosätta dess metoder så att funktionaliteten inte äventyras ur klientens synvinkel. Det vill säga, om en utvecklare utökar din klass och använder den i en applikation, bör han eller hon inte ändra det förväntade beteendet för några åsidosatta metoder. Underklasser måste åsidosätta basklassens metoder så att funktionaliteten inte bryts ur klientens synvinkel. Vi kan utforska detta i detalj i följande exempel. Anta att vi har en klass som ansvarar för att validera en beställning och kontrollera om alla varor i beställningen finns i lager.isValid()metod som returnerar sant eller falskt :

public class OrderStockValidator {

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

        return true;
    }
}
Anta också att vissa beställningar behöver valideras annorlunda än andra, t.ex. för vissa beställningar behöver vi kontrollera om alla varor i beställningen finns i lager och om alla varor är packade. För att göra detta utökar vi OrderStockValidatorklassen genom att skapa OrderStockAndPackValidatorklassen:

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;
    }
}
Men här har vi brutit mot Liskov-substitutionsprincipen, för istället för att returnera falskt om ordern misslyckas med valideringen, kastar vår metod en IllegalStateException. Klienter som använder den här koden förväntar sig inte detta: de förväntar sig ett returvärde på antingen sant eller falskt . Detta kan leda till körtidsfel.

Interface Segregation Principle (ISP)

Denna princip kännetecknas av följande uttalande: klienten ska inte tvingas implementera metoder som de inte kommer att använda . Gränssnittssegregationsprincipen innebär att gränssnitt som är för "tjocka" måste delas upp i mindre, mer specifika, så att klienter som använder små gränssnitt bara vet om de metoder de behöver för sitt arbete. Som ett resultat, när en gränssnittsmetod ändras, bör alla klienter som inte använder den metoden inte ändras. Tänk på det här exemplet: Alex, en utvecklare, har skapat ett "rapport"-gränssnitt och lagt till två metoder: generateExcel()ochgeneratedPdf(). Nu vill en klient använda detta gränssnitt, men tänker bara använda rapporter i PDF-format, inte i Excel. Kommer den här funktionen att tillfredsställa den här klienten? Nej. Klienten måste implementera två metoder, varav en i stort sett inte behövs och endast existerar tack vare Alex, den som designade programvaran. Klienten kommer att använda antingen ett annat gränssnitt eller inte göra något med metoden för Excel-rapporter. Så vad är lösningen? Det är att dela upp det befintliga gränssnittet i två mindre. En för PDF-rapporter, den andra för Excel-rapporter. Detta låter kunderna bara använda den funktionalitet de behöver.

Dependency Inversion Principle (DIP)

I Java beskrivs denna SOLID-princip enligt följande: beroenden inom systemet byggs utifrån abstraktioner. Moduler på högre nivå är inte beroende av moduler på lägre nivå. Abstraktioner bör inte bero på detaljer. Detaljer bör bero på abstraktioner. Mjukvaran behöver utformas så att de olika modulerna är fristående och kopplade till varandra genom abstraktion. En klassisk tillämpning av denna princip är Spring Framework. I Spring Framework är alla moduler implementerade som separata komponenter som kan fungera tillsammans. De är så autonoma att de lika enkelt kan användas i andra programmoduler än Spring Framework. Detta uppnås tack vare beroendet av de stängda och öppna principerna. Alla moduler ger endast åtkomst till abstraktionen, som kan användas i en annan modul. Låt oss försöka illustrera detta med ett exempel. På tal om principen om ett enda ansvar, övervägde viOrderProcessorklass. Låt oss ta en ny titt på koden för denna klass:

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);
        }
    }

}
I det här exemplet OrderProcessorberor vår klass på två specifika klasser: MySQLOrderRepositoryoch ConfirmationEmailSender. Vi kommer också att presentera koden för dessa klasser:

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
    }
}
Dessa klasser är långt ifrån vad vi skulle kalla abstraktioner. Och ur beroendeinversionsprincipens synvinkel vore det bättre att börja med att skapa några abstraktioner som vi kan arbeta med i framtiden, snarare än specifika implementeringar. Låt oss skapa två gränssnitt: MailSenderoch OrderRepository). Dessa kommer att vara våra abstraktioner:

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

public interface OrderRepository {
    boolean save(Order order);
}
Nu implementerar vi dessa gränssnitt i klasser som redan har förberetts för detta:

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;
    }
}
Vi gjorde det förberedande arbetet så att vår OrderProcessorklass inte beror på konkreta detaljer, utan på abstraktioner. Vi kommer att ändra det genom att lägga till våra beroenden till klasskonstruktorn:

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 är vår klass beroende av abstraktioner, inte specifika implementeringar. Vi kan enkelt ändra dess beteende genom att lägga till önskat beroende när ett OrderProcessorobjekt skapas. Vi har undersökt SOLID designprinciperna i Java. Du kommer att lära dig mer om OOP i allmänhet och grunderna i Java-programmering — inget tråkigt och hundratals timmars träning — i CodeGym-kursen. Dags att lösa några uppgifter :)
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION