CodeGym /Java Blog /Willekeurig /Codeerregels: van het maken van een systeem tot het werke...
John Squirrels
Niveau 41
San Francisco

Codeerregels: van het maken van een systeem tot het werken met objecten

Gepubliceerd in de groep Willekeurig
Goedendag, iedereen! Vandaag willen we het met je hebben over het schrijven van goede code. Natuurlijk wil niet iedereen meteen op boeken als Clean Code kauwen, aangezien ze veel informatie bevatten, maar in het begin niet veel duidelijk is. En tegen de tijd dat u klaar bent met lezen, kunt u al uw verlangen om te coderen doden. Dit alles in aanmerking genomen, wil ik u vandaag een kleine handleiding (een kleine reeks aanbevelingen) geven voor het schrijven van betere code. Laten we in dit artikel de basisregels en concepten bespreken die te maken hebben met het maken van een systeem en het werken met interfaces, klassen en objecten. Het lezen van dit artikel zal niet veel tijd in beslag nemen en zal, naar ik hoop, u niet vervelen. Ik werk van boven naar beneden, dat wil zeggen van de algemene structuur van een applicatie tot de kleinere details. Codeerregels: van het maken van een systeem tot het werken met objecten - 1

Systemen

De volgende zijn over het algemeen wenselijke kenmerken van een systeem:
  • Minimale complexiteit. Te gecompliceerde projecten moeten worden vermeden. Het belangrijkste is eenvoud en duidelijkheid (eenvoudiger = beter).
  • Gemak van onderhoud. Wanneer u een toepassing maakt, moet u onthouden dat deze moet worden onderhouden (zelfs als u niet persoonlijk verantwoordelijk bent voor het onderhoud ervan). Dit betekent dat de code duidelijk en voor de hand liggend moet zijn.
  • Losse koppeling. Dit betekent dat we het aantal afhankelijkheden tussen verschillende delen van het programma minimaliseren (zodat we onze naleving van de OOP-principes maximaliseren).
  • Herbruikbaarheid. We ontwerpen ons systeem met de mogelijkheid om componenten in andere toepassingen te hergebruiken.
  • Draagbaarheid. Het moet eenvoudig zijn om een ​​systeem aan te passen aan een andere omgeving.
  • Uniforme stijl. We ontwerpen ons systeem met een uniforme stijl in de verschillende componenten.
  • Uitbreidbaarheid (schaalbaarheid). We kunnen het systeem verbeteren zonder de basisstructuur te schenden (het toevoegen of wijzigen van een component mag geen invloed hebben op alle andere).
Het is praktisch onmogelijk om een ​​applicatie te bouwen die geen aanpassingen of nieuwe functionaliteit vereist. We zullen voortdurend nieuwe onderdelen moeten toevoegen om ons geesteskind te helpen bij de tijd te blijven. Dit is waar schaalbaarheid een rol speelt. Schaalbaarheid is in wezen het uitbreiden van de applicatie, het toevoegen van nieuwe functionaliteit en het werken met meer bronnen (of, met andere woorden, met een grotere belasting). Met andere woorden, om het gemakkelijker te maken om nieuwe logica toe te voegen, houden we ons aan enkele regels, zoals het verminderen van de koppeling van het systeem door de modulariteit te vergroten.Codeerregels: van het maken van een systeem tot het werken met objecten - 2

Afbeeldingsbron

Stadia van het ontwerpen van een systeem

  1. Softwaresysteem. Ontwerp de applicatie in het algemeen.
  2. Verdeling in subsystemen/pakketten. Definieer logisch verschillende onderdelen en definieer de regels voor interactie daartussen.
  3. Verdeling van subsystemen in klassen. Verdeel delen van het systeem in specifieke klassen en interfaces en definieer de interactie daartussen.
  4. Verdeling van klassen in methoden. Maak een volledige definitie van de benodigde methoden voor een klasse, op basis van de toegewezen verantwoordelijkheid.
  5. Methode ontwerp. Maak een gedetailleerde definitie van de functionaliteit van individuele methoden.
Gewoonlijk behandelen gewone ontwikkelaars dit ontwerp, terwijl de architect van de applicatie de hierboven beschreven punten afhandelt.

Algemene principes en concepten van systeemontwerp

Luie initialisatie. In dit programmeeridioom verspilt de applicatie geen tijd aan het maken van een object totdat het daadwerkelijk wordt gebruikt. Dit versnelt het initialisatieproces en vermindert de belasting van de vuilnisophaler. Dat gezegd hebbende, moet je hiermee niet te ver gaan, want dat kan het principe van modulariteit schenden. Misschien is het de moeite waard om alle constructie-instanties naar een specifiek onderdeel te verplaatsen, bijvoorbeeld de hoofdmethode of naar een fabrieksklasse . Een kenmerk van goede code is de afwezigheid van repetitieve standaardcode. Dergelijke code wordt in de regel in een aparte klasse geplaatst, zodat deze indien nodig kan worden aangeroepen.

AOP

Ik zou ook willen wijzen op aspectgerichte programmering. Bij dit programmeerparadigma draait alles om het introduceren van transparante logica. Dat wil zeggen, repetitieve code wordt in klassen (aspecten) geplaatst en wordt aangeroepen wanneer aan bepaalde voorwaarden is voldaan. Bijvoorbeeld bij het aanroepen van een methode met een specifieke naam of het benaderen van een variabele van een bepaald type. Soms kunnen aspecten verwarrend zijn, omdat het niet meteen duidelijk is waar de code vandaan wordt gebeld, maar dit is nog steeds een zeer nuttige functionaliteit. Vooral bij het cachen of loggen. We voegen deze functionaliteit toe zonder extra logica toe te voegen aan gewone klassen. De vier regels van Kent Beck voor een eenvoudige architectuur:
  1. Expressiviteit - De bedoeling van een les moet duidelijk worden uitgedrukt. Dit wordt bereikt door de juiste naamgeving, kleine omvang en naleving van het principe van één verantwoordelijkheid (waarop we hieronder in meer detail zullen ingaan).
  2. Minimum aantal klassen en methoden - In uw wens om klassen zo klein en nauw mogelijk te maken, kunt u te ver gaan (resulterend in het antipatroon van shotgun-chirurgie). Dit principe vraagt ​​om het systeem compact te houden en niet te ver te gaan, door een aparte klasse te creëren voor elke mogelijke actie.
  3. Geen duplicatie — Dubbele code, die voor verwarring zorgt en een indicatie is van een suboptimaal systeemontwerp, wordt geëxtraheerd en naar een aparte locatie verplaatst.
  4. Voert alle tests uit - Een systeem dat alle tests doorstaat, is beheersbaar. Elke verandering kan ertoe leiden dat een test mislukt, wat ons onthult dat onze verandering in de interne logica van een methode ook het gedrag van het systeem op onverwachte manieren heeft veranderd.

STEVIG

Bij het ontwerpen van een systeem zijn de bekende SOLID-principes het overwegen waard:

S (enkele verantwoordelijkheid), O (open-gesloten), L (Liskov-substitutie), I (interface-segregatie), D (afhankelijkheidsinversie).

We zullen niet bij elk afzonderlijk principe stilstaan. Dat zou een beetje buiten het bestek van dit artikel vallen, maar je kunt hier meer lezen .

Koppel

Misschien is een van de belangrijkste stappen bij het maken van een goed ontworpen klasse het creëren van een goed ontworpen interface die een goede abstractie vertegenwoordigt, de implementatiedetails van de klasse verbergt en tegelijkertijd een groep methoden presenteert die duidelijk consistent met elkaar zijn. Laten we eens nader kijken naar een van de SOLID-principes — scheiding van interfaces: clients (klassen) mogen geen onnodige methoden implementeren die ze niet zullen gebruiken. Met andere woorden, als we het hebben over het maken van een interface met zo min mogelijk methoden die gericht zijn op het uitvoeren van de enige taak van de interface (wat volgens mij erg lijkt op het principe van één verantwoordelijkheid), is het beter om in plaats daarvan een paar kleinere te maken. van een opgeblazen interface. Gelukkig kan een klasse meer dan één interface implementeren. Vergeet niet om uw interfaces de juiste naam te geven: de naam moet de toegewezen taak zo nauwkeurig mogelijk weergeven. En, natuurlijk, hoe korter het is, hoe minder verwarring het zal veroorzaken. Documentatiecommentaar wordt meestal op interfaceniveau geschreven. Deze opmerkingen geven details over wat elke methode zou moeten doen, welke argumenten nodig zijn en wat het zal opleveren.

Klas

Codeerregels: van het maken van een systeem tot het werken met objecten - 3

Afbeeldingsbron

Laten we eens kijken hoe de lessen intern zijn geregeld. Of beter gezegd, enkele perspectieven en regels die moeten worden gevolgd bij het schrijven van lessen. In de regel zou een klasse moeten beginnen met een lijst met variabelen in een bepaalde volgorde:
  1. openbare statische constanten;
  2. privé statische constanten;
  3. privé-instantievariabelen.
Vervolgens komen de verschillende constructeurs, in volgorde van degene met de minste argumenten tot degene met de meeste. Ze worden gevolgd door methoden van de meest openbare tot de meest private. Over het algemeen staan ​​privémethoden die de implementatie verbergen van bepaalde functies die we willen beperken, helemaal onderaan.

Klas grootte

Nu wil ik het hebben over de grootte van de klassen. Laten we een van de SOLID-principes in herinnering roepen: het single responsibility-principe. Het stelt dat elk object slechts één doel heeft (verantwoordelijkheid), en de logica van al zijn methoden is erop gericht dit te bereiken. Dit vertelt ons dat we grote, opgeblazen klassen moeten vermijden (die eigenlijk het God-object-antipatroon zijn), en als we veel methoden met allerlei verschillende logica in een klasse hebben gepropt, moeten we erover nadenken om het op te splitsen in een klasse. paar logische onderdelen (klassen). Dit zal op zijn beurt de leesbaarheid van de code vergroten, aangezien het niet lang zal duren om het doel van elke methode te begrijpen als we het geschatte doel van een bepaalde klasse kennen. Houd ook de naam van de klasse in de gaten, die de logica die deze bevat, moet weerspiegelen. Als we bijvoorbeeld een klas hebben met meer dan 20 woorden in de naam, we moeten nadenken over refactoring. Elke zichzelf respecterende klasse zou niet zoveel interne variabelen moeten hebben. In feite werkt elke methode met een of enkele ervan, waardoor er veel cohesie binnen de klas ontstaat (en dat is precies zoals het hoort, aangezien de klas een verenigd geheel moet zijn). Als gevolg hiervan leidt het vergroten van de samenhang van een klas tot een verkleining van de klas en natuurlijk neemt het aantal klassen toe. Dit is vervelend voor sommige mensen, omdat je meer in klassenbestanden moet bladeren om te zien hoe een specifieke grote taak werkt. Bovendien is elke klas een kleine module die minimaal gerelateerd moet zijn aan andere. Deze isolatie vermindert het aantal wijzigingen dat we moeten aanbrengen bij het toevoegen van extra logica aan een klasse. elke methode werkt met een of enkele ervan, waardoor er veel cohesie ontstaat binnen de klas (en dat is precies zoals het hoort, aangezien de klas een verenigd geheel moet zijn). Als gevolg hiervan leidt het vergroten van de samenhang van een klas tot een verkleining van de klas en natuurlijk neemt het aantal klassen toe. Dit is vervelend voor sommige mensen, omdat je meer in klassenbestanden moet bladeren om te zien hoe een specifieke grote taak werkt. Bovendien is elke klas een kleine module die minimaal gerelateerd moet zijn aan andere. Deze isolatie vermindert het aantal wijzigingen dat we moeten aanbrengen bij het toevoegen van extra logica aan een klasse. elke methode werkt met een of enkele ervan, waardoor er veel cohesie ontstaat binnen de klas (en dat is precies zoals het hoort, aangezien de klas een verenigd geheel moet zijn). Als gevolg hiervan leidt het vergroten van de samenhang van een klas tot een verkleining van de klas en natuurlijk neemt het aantal klassen toe. Dit is vervelend voor sommige mensen, omdat je meer in klassenbestanden moet bladeren om te zien hoe een specifieke grote taak werkt. Bovendien is elke klas een kleine module die minimaal gerelateerd moet zijn aan andere. Deze isolatie vermindert het aantal wijzigingen dat we moeten aanbrengen bij het toevoegen van extra logica aan een klasse. De cohesie leidt tot een verkleining van de klas en natuurlijk tot een toename van het aantal klassen. Dit is vervelend voor sommige mensen, omdat je meer in klassenbestanden moet bladeren om te zien hoe een specifieke grote taak werkt. Bovendien is elke klas een kleine module die minimaal gerelateerd moet zijn aan andere. Deze isolatie vermindert het aantal wijzigingen dat we moeten aanbrengen bij het toevoegen van extra logica aan een klasse. De cohesie leidt tot een verkleining van de klas en natuurlijk tot een toename van het aantal klassen. Dit is vervelend voor sommige mensen, omdat je meer in klassenbestanden moet bladeren om te zien hoe een specifieke grote taak werkt. Bovendien is elke klas een kleine module die minimaal gerelateerd moet zijn aan andere. Deze isolatie vermindert het aantal wijzigingen dat we moeten aanbrengen bij het toevoegen van extra logica aan een klasse.

Voorwerpen

Inkapseling

Hier zullen we het eerst hebben over een OOP-principe: inkapseling. Het verbergen van de implementatie komt niet neer op het creëren van een methode om variabelen te isoleren (het gedachteloos beperken van de toegang via individuele methoden, getters en setters, wat niet goed is, aangezien het hele punt van inkapseling verloren gaat). Het verbergen van toegang is gericht op het vormen van abstracties, dat wil zeggen dat de klasse gedeelde concrete methoden biedt die we gebruiken om met onze gegevens te werken. En de gebruiker hoeft niet precies te weten hoe we met deze gegevens werken - het werkt en dat is genoeg.

Wet van Demeter

We kunnen ook rekening houden met de wet van Demeter: het is een kleine set regels die helpt bij het beheersen van complexiteit op klasse- en methodeniveau. Stel dat we een Car- object hebben en dat het een methode Move (Object arg1, Object arg2) heeft . Volgens de wet van Demeter is deze methode beperkt tot het aanroepen van:
  • methoden van het Car -object zelf (met andere woorden, het "dit"-object);
  • methoden van objecten gemaakt binnen de verplaatsingsmethode ;
  • methoden van objecten doorgegeven als argumenten ( arg1 , arg2 );
  • methoden van interne auto- objecten (opnieuw "dit").
Met andere woorden, de wet van Demeter is zoiets als wat ouders tegen een kind zouden kunnen zeggen: "je kunt met je vrienden praten, maar niet met vreemden".

Data structuur

Een gegevensstructuur is een verzameling gerelateerde elementen. Wanneer een object als een datastructuur wordt beschouwd, is er een set data-elementen waarop methoden werken. Het bestaan ​​van deze methoden wordt impliciet verondersteld. Dat wil zeggen, een gegevensstructuur is een object waarvan het doel is om de opgeslagen gegevens op te slaan en ermee te werken (verwerken). Het belangrijkste verschil met een regulier object is dat een gewoon object een verzameling methoden is die werken op gegevenselementen waarvan impliciet wordt aangenomen dat ze bestaan. Begrijp je dat? Het belangrijkste aspect van een gewoon object zijn methoden. Interne variabelen vergemakkelijken hun correcte werking. Maar in een datastructuur zijn de methoden er om uw werk te ondersteunen met de opgeslagen data-elementen, die hier voorop staan. Een type datastructuur is een data transfer object (DTO). Dit is een klasse met openbare variabelen en geen methoden (of alleen methoden voor lezen/schrijven) die wordt gebruikt om gegevens over te dragen bij het werken met databases, het ontleden van berichten uit sockets, enz. Gegevens worden gewoonlijk niet voor een lange periode in dergelijke objecten opgeslagen. Het wordt vrijwel onmiddellijk omgezet naar het type entiteit waarop onze applicatie werkt. Een entiteit is op zijn beurt ook een datastructuur, maar het doel ervan is om deel te nemen aan de bedrijfslogica op verschillende niveaus van de applicatie. Het doel van een DTO is het transporteren van data van/naar de applicatie. Voorbeeld van een DTO: is ook een gegevensstructuur, maar het doel ervan is om deel te nemen aan bedrijfslogica op verschillende niveaus van de applicatie. Het doel van een DTO is het transporteren van data van/naar de applicatie. Voorbeeld van een DTO: is ook een gegevensstructuur, maar het doel ervan is om deel te nemen aan bedrijfslogica op verschillende niveaus van de applicatie. Het doel van een DTO is het transporteren van data van/naar de applicatie. Voorbeeld van een DTO:

@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
Alles lijkt duidelijk genoeg, maar hier leren we over het bestaan ​​van hybriden. Hybriden zijn objecten die methoden hebben om belangrijke logica af te handelen, interne elementen op te slaan en ook accessor-methoden (get/set) bevatten. Dergelijke objecten zijn rommelig en maken het moeilijk om nieuwe methoden toe te voegen. Je moet ze vermijden, omdat het niet duidelijk is waar ze voor dienen: elementen opslaan of logica uitvoeren?

Principes van het creëren van variabelen

Laten we even nadenken over variabelen. Laten we meer specifiek nadenken over welke principes van toepassing zijn bij het maken ervan:
  1. Idealiter zou u een variabele moeten declareren en initialiseren vlak voordat u deze gebruikt (maak er geen aan en vergeet hem).
  2. Declareer waar mogelijk variabelen als definitief om te voorkomen dat hun waarde na initialisatie verandert.
  3. Vergeet de tellervariabelen niet, die we meestal gebruiken in een soort for- lus. Dat wil zeggen, vergeet ze niet op nul te zetten. Anders kan al onze logica breken.
  4. Probeer variabelen in de constructor te initialiseren.
  5. Als er een keuze is tussen het gebruik van een object met een referentie of zonder ( new SomeObject() ), kies dan voor zonder, aangezien het object na gebruik wordt verwijderd tijdens de volgende opschooncyclus en de bronnen niet worden verspild.
  6. Houd de levensduur van een variabele (de afstand tussen het maken van de variabele en de laatste keer dat ernaar wordt verwezen) zo kort mogelijk.
  7. Initialiseer variabelen die in een lus worden gebruikt vlak voor de lus, niet aan het begin van de methode die de lus bevat.
  8. Begin altijd met het meest beperkte bereik en breid alleen uit wanneer dat nodig is (probeer een variabele zo lokaal mogelijk te maken).
  9. Gebruik elke variabele slechts voor één doel.
  10. Vermijd variabelen met een verborgen doel, bijv. een variabele die is opgesplitst tussen twee taken — dit betekent dat het type variabele niet geschikt is om een ​​ervan op te lossen.

methoden

Codeerregels: van het maken van een systeem tot het werken met objecten - 4

uit de film "Star Wars: Episode III - Revenge of the Sith" (2005)

Laten we direct doorgaan met de implementatie van onze logica, dwz met methoden.
  1. Regel #1 - Compactheid. Idealiter zou een methode niet langer moeten zijn dan 20 regels. Dit betekent dat als een openbare methode aanzienlijk "aanzwelt", u moet nadenken over het opsplitsen van de logica en het verplaatsen naar afzonderlijke privémethoden.

  2. Regel #2 — if , else , while en andere statements mogen geen zwaar geneste blokken bevatten: veel nesten vermindert de leesbaarheid van de code aanzienlijk. Idealiter zou u niet meer dan twee geneste {} blokken moeten hebben.

    En het is ook wenselijk om de code in deze blokken compact en eenvoudig te houden.

  3. Regel #3 - Een methode mag slechts één bewerking uitvoeren. Dat wil zeggen, als een methode allerlei complexe logica uitvoert, splitsen we deze op in submethoden. Als gevolg hiervan zal de methode zelf een façade zijn waarvan het doel is om alle andere bewerkingen in de juiste volgorde aan te roepen.

    Maar wat als de operatie te simpel lijkt om in een aparte methode te stoppen? Toegegeven, soms voelt het alsof je een kanon op mussen afvuurt, maar kleine methoden bieden een aantal voordelen:

    • Beter codebegrip;
    • Methoden worden vaak complexer naarmate de ontwikkeling vordert. Als een methode in het begin eenvoudig is, zal het iets gemakkelijker zijn om de functionaliteit ervan te compliceren;
    • Implementatiedetails zijn verborgen;
    • Eenvoudig hergebruik van code;
    • Meer betrouwbare code.

  4. De stepdown-regel — Code moet van boven naar beneden worden gelezen: hoe lager je leest, hoe dieper je in de logica verdiept. En vice versa, hoe hoger je komt, hoe abstracter de methodes. Switch-statements zijn bijvoorbeeld nogal oncompact en ongewenst, maar als u het gebruik van een switch niet kunt vermijden, moet u proberen deze zo laag mogelijk te verplaatsen, naar de methoden op het laagste niveau.

  5. Methodeargumenten — Wat is het ideale getal? Idealiter helemaal geen :) Maar gebeurt dat echt? Dat gezegd hebbende, moet je proberen zo min mogelijk argumenten te hebben, want hoe minder er zijn, hoe gemakkelijker het is om een ​​methode te gebruiken en hoe gemakkelijker het is om deze te testen. Probeer bij twijfel te anticiperen op alle scenario's voor het gebruik van de methode met een groot aantal invoerparameters.

  6. Bovendien zou het goed zijn om methoden te scheiden die een booleaanse vlag als invoerparameter hebben, aangezien dit op zichzelf al impliceert dat de methode meer dan één bewerking uitvoert (indien waar, doe dan één ding; indien onwaar, doe dan iets anders). Zoals ik hierboven schreef, is dit niet goed en moet het indien mogelijk worden vermeden.

  7. Als een methode een groot aantal invoerparameters heeft (een uiterste is 7, maar je zou echt na 2-3 moeten gaan nadenken), moeten sommige argumenten worden gegroepeerd in een apart object.

  8. Als er meerdere vergelijkbare (overbelaste) methoden zijn, moeten vergelijkbare parameters in dezelfde volgorde worden doorgegeven: dit verbetert de leesbaarheid en bruikbaarheid.

  9. Wanneer u parameters doorgeeft aan een methode, moet u er zeker van zijn dat ze allemaal worden gebruikt, waarom heeft u ze anders nodig? Knip alle ongebruikte parameters uit de interface en klaar ermee.

  10. try/catch ziet er van nature niet erg mooi uit, dus het zou een goed idee zijn om het naar een aparte tussenmethode te verplaatsen (een methode voor het afhandelen van uitzonderingen):

    
    public void exceptionHandling(SomeObject obj) {
        try {  
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

Ik sprak hierboven over dubbele code, maar laat me het nogmaals herhalen: als we een aantal methoden met herhaalde code hebben, moeten we deze naar een aparte methode verplaatsen. Dit maakt zowel de methode als de klas compacter. Vergeet de regels voor namen niet: details over het correct benoemen van klassen, interfaces, methoden en variabelen zullen in het volgende deel van het artikel worden besproken. Maar dat is alles wat ik vandaag voor je heb.
Opmerkingen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION