En kort oversigt over objektorienteret software design

Demonstreret ved at implementere et rollespils klasser

Introduktion

De fleste moderne programmeringssprog understøtter og tilskynder objektorienteret programmering (OOP). Selvom vi for nylig ser ud til at se et lille skift væk fra dette, da folk begynder at bruge sprog, der ikke er stærkt påvirket af OOP (såsom Go, Rust, Elixir, Elm, Scala), har de fleste stadig objekter. De designprincipper, vi skal skitsere her, gælder også for ikke-OOP-sprog.

For at få succes med at skrive klar, høj kvalitet, vedligeholdelig og udvidelig kode skal du vide om designprincipper, der har vist sig at være effektive gennem årtiers erfaring.

Offentliggørelse: Eksemplet, vi skal igennem, vil være i Python. Eksempler er der for at bevise et punkt og kan være sjusket på andre, åbenlyse måder.

Objekttyper

Da vi skal modellere vores kode omkring objekter, ville det være nyttigt at skelne mellem deres forskellige ansvar og variationer.

Der er tre typer objekter:

1. Enhedsobjekt

Dette objekt svarer generelt til en eller anden virkelig enhed i problemrummet. Sig, at vi bygger et rollespil (RPG), et objektobjekt ville være vores enkle Heroklasse:

Disse objekter indeholder generelt egenskaber om sig selv (såsom healtheller mana) og kan ændres gennem visse regler.

2. Kontrolobjekt

Kontrolobjekter (undertiden også kaldet Manager-objekter ) er ansvarlige for koordineringen af ​​andre objekter. Dette er objekter, der styrerog gøre brug af andre genstande. Et godt eksempel i vores RPG-analogi ville være Fightklassen, der styrer to helte og får dem til at kæmpe.

Indkapsling af logikken til en kamp i en sådan klasse giver dig flere fordele: hvoraf den ene er den lette udvidelse af handlingen. Du kan meget let sende en NPC-type (non-player character), som helten kan kæmpe for, forudsat at den udsætter den samme API. Du kan også meget let arve klassen og tilsidesætte noget af funktionaliteten for at imødekomme dine behov.

3. Grænseobjekt

Dette er objekter, der sidder ved grænsen til dit system. Ethvert objekt, der tager input fra eller producerer output til et andet system - uanset om dette system er en bruger, internettet eller en database - kan klassificeres som et grænseobjekt.

Disse grænseobjekter er ansvarlige for at oversætte information til og ud af vores system. I et eksempel, hvor vi tager brugerkommandoer, har vi brug for grænseobjektet for at oversætte et tastaturindgang (som et mellemrum) til en genkendelig domænehændelse (såsom et tegn-spring).

Bonus: Værdiobjekt

Værdiobjekter repræsenterer en simpel værdi i dit domæne. De er uforanderlige og har ingen identitet.

Hvis vi skulle indarbejde dem i vores spil, en Moneyeller Damageville klasse være en kæmpefordel. De nævnte objekter lader os let skelne, finde og fejle relateret funktionalitet, mens den naive tilgang til at bruge en primitiv type - en række heltal eller et heltal - ikke gør det.

De kan klassificeres som en underkategori af Entitygenstande.

Principper for nøgledesign

Designprincipper er regler i software design, der har vist sig værdifulde gennem årene. Hvis du følger dem nøje, hjælper det dig med at sikre, at din software er i topkvalitet.

Abstraktion

Abstraktion er ideen om at forenkle et koncept til dets nøglepunkter i en eller anden sammenhæng. Det giver dig mulighed for bedre at forstå konceptet ved at fjerne det til en forenklet version.

Eksemplerne ovenfor illustrerer abstraktion - se på, hvordan Fightklassen er struktureret. Den måde, du bruger det på, er så simpelt som muligt - du giver det to helte som argumenter i instantiering og kalder fight()metoden. Intet mere, intet mindre.

Abstraktion i din kode skal følge reglen om mindst overraskelse. Din abstraktion bør ikke overraske nogen med unødvendig og ikke-relateret opførsel / egenskaber. Med andre ord - det skal være intuitivt.

Bemærk, at vores Hero#take_damage()funktion ikke gør noget uventet, som at slette vores karakter ved døden. Men vi kan forvente, at det dræber vores karakter, hvis hans helbred går under nul.

Indkapsling

Indkapsling kan betragtes som at lægge noget inde i en kapsel - du begrænser dets eksponering for omverdenen. I software hjælper begrænsning af adgang til indre objekter og egenskaber med dataintegritet.

Indkapsling sorte bokse indre logik og gør dine klasser lettere at administrere, fordi du ved, hvilken del der bruges af andre systemer, og hvad der ikke er. Dette betyder, at du let kan omarbejde den indre logik, mens du bevarer de offentlige dele og være sikker på, at du ikke har brudt noget. Som en bivirkning bliver det lettere at arbejde med den indkapslede funktionalitet udefra, da du har færre ting at tænke på.

På de fleste sprog sker dette gennem de såkaldte adgangsmodifikatorer (private, beskyttede osv.). Python er ikke det bedste eksempel på dette, da det mangler sådanne eksplicit modifikatorer indbygget i runtime, men vi bruger konventioner til at omgå dette. Den _præfiks til variablerne / metoder betegne dem som værende privat.

Forestil dig for eksempel, at vi ændrer vores Fight#_run_attackmetode til at returnere en boolsk variabel, der angiver, om kampen er slut snarere end at hæve en undtagelse. Vi ved, at den eneste kode, vi måske har brudt, er inde i Fightklassen, fordi vi gjorde metoden privat.

Husk, kode ændres oftere end skrevet på ny. At være i stand til at ændre din kode med så klare og få følger som muligt er den fleksibilitet, du vil have som udvikler.

Nedbrydning

Nedbrydning er handlingen ved at opdele et objekt i flere separate mindre dele. Disse dele er lettere at forstå, vedligeholde og programmere.

Forestil dig, at vi ønskede at inkorporere flere RPG-funktioner som buffs, lager, udstyr og karakteregenskaber oven på vores Hero:

Jeg antager, at du kan fortælle, at denne kode bliver ret rodet. Vores Heroobjekt laver for mange ting på én gang, og denne kode bliver ret skør som et resultat af det.

For eksempel er et udholdenhedspunkt 5 sundhed værd. Hvis vi nogensinde vil ændre dette i fremtiden for at gøre det sundt værd, er vi nødt til at ændre implementeringen flere steder.

Svaret er at nedbryde Heroobjektet i flere mindre objekter, som hver omfatter noget af funktionaliteten.

Nu, efter nedbrydning vores Hero objektets funktionalitet i HeroAttributes, HeroInventory, HeroEquipmentog HeroBuffgenstande, tilføjer fremtiden funktionalitet vil være lettere, mere indkapslet og bedre indvindes. Du kan fortælle, at vores kode er meget renere og klarere, hvad den gør.

Der er tre typer af nedbrydningsforhold:

  • forening- Definerer et løst forhold mellem to komponenter. Begge komponenter er ikke afhængige af hinanden, men fungerer muligvis sammen.

Eksempel:Hero og et Zoneobjekt.

  • aggregering - Definerer et svagt "has-a" forhold mellem en helhed og dens dele. Betragtes som svag, fordi delene kan eksistere uden helheden.

Eksempel:HeroInventory og Item.

A HeroInventorykan have mange Itemsog en Itemkan høre til enhver HeroInventory(såsom handelsartikler).

  • komposition - Et stærkt ”has-a” forhold, hvor helheden og delen ikke kan eksistere uden hinanden. Dele kan ikke deles, da det hele afhænger af de nøjagtige dele.

Eksempel:Hero og HeroAttributes.

Dette er heltenes attributter - du kan ikke ændre deres ejer.

Generalisering

Generalisering kan være det vigtigste designprincip - det er processen med at udtrække fælles egenskaber og kombinere dem ét sted. Alle ved vi om begrebet funktioner og klassearv - begge er en slags generalisering.

En sammenligning kan rydde op i ting: mens abstraktion reducerer kompleksiteten ved at skjule unødvendige detaljer, reducerer generalisering kompleksiteten ved at erstatte flere enheder, der udfører lignende funktioner med en enkelt konstruktion.

I det givne eksempel har vi generaliseret vores fælles Heroog NPC klassers funktionalitet til en fælles forfader kaldet Entity. Dette opnås altid gennem arv.

Her, i stedet for at vores NPCog Heroklasser implementerer alle metoderne to gange og overtræder DRY-princippet, reducerede vi kompleksiteten ved at flytte deres fælles funktionalitet til en basisklasse.

Som en advarsel - overdriv ikke arv. Mange erfarne mennesker anbefaler, at du foretrækker sammensætning frem for arv.

Arv misbruges ofte af amatørprogrammerere, sandsynligvis fordi det er en af ​​de første OOP-teknikker, de forstår på grund af dets enkelhed.

Sammensætning

Komposition er princippet om at kombinere flere objekter til en mere kompleks. Praktisk sagt - det skaber forekomster af objekter og bruger deres funktionalitet i stedet for direkte at arve det.

Et objekt, der bruger komposition, kan kaldes et sammensat objekt . Det er vigtigt, at denne komposit er enklere end summen af ​​sine jævnaldrende. Når vi kombinerer flere klasser til en, vil vi hæve abstraktionsniveauet højere og gøre objektet enklere.

Den sammensatte objekts API skal skjule dens indre komponenter og interaktionerne imellem dem. Tænk på et mekanisk ur, det har tre hænder til visning af tiden og en knap til indstilling - men indeholder internt snesevis af bevægelige og indbyrdes afhængige dele.

Som jeg sagde foretrækkes sammensætning frem for arv, hvilket betyder at du skal stræbe efter at flytte fælles funktionalitet til et separat objekt, som klasser derefter bruger - snarere end at gemme det i en basisklasse, du har arvet.

Lad os illustrere et muligt problem med overarvende funktionalitet:

Vi tilføjede lige bevægelse til vores spil.

Som vi lærte, brugte vi generalisering til at placere funktionerne move_rightog move_leftfunktionerne i Entityklassen i stedet for at duplikere koden .

Okay, hvad nu hvis vi ville introducere monteringer i spillet?

Monteringer skal også bevæge sig til venstre og højre, men har ikke evnen til at angribe. Kom til at tænke på det - de har måske ikke engang helbred!

Jeg ved, hvad din løsning er:

Du skal blot flytte movelogikken til en separat MoveableEntityeller MoveableObjectklasse, der kun har den funktionalitet. Den Mountklasse kan derefter arve det.

Hvad gør vi så, hvis vi vil have monteringer, der har helbred, men som ikke kan angribe? Mere opdelt i underklasser? Jeg håber, du kan se, hvordan vores klassehierarki begynder at blive komplekst, selvom vores forretningslogik stadig er ret enkel.

En noget bedre tilgang ville være at abstrakte bevægelseslogikken i en Movementklasse (eller et bedre navn) og instantiere den i de klasser, der muligvis har brug for den. Dette pakker funktionaliteten pænt sammen og gør den genanvendelig på tværs af alle mulige objekter, der ikke er begrænset til Entity.

Hurra, komposition!

Ansvarsfraskrivelse for kritisk tænkning

Selvom disse designprincipper er dannet gennem årtiers erfaring, er det stadig yderst vigtigt, at du er i stand til at tænke kritisk, før du blindt anvender et princip på din kode.

Som alt andet kan for meget være en dårlig ting. Nogle gange kan principper tages for langt, du kan blive for klog med dem og ende med noget, der faktisk er sværere at arbejde med.

Som ingeniør er dit hovedtræk kritisk at evaluere den bedste tilgang til din unikke situation, ikke blindt følge og anvende vilkårlige regler.

Samhørighed, kobling og adskillelse af bekymringer

Samhørighed

Samhørighed repræsenterer klarheden i ansvaret i et modul eller med andre ord - dets kompleksitet.

Hvis din klasse udfører en opgave og intet andet eller har et klart formål - har denne klasse høj samhørighed . På den anden side, hvis det er noget uklart i, hvad det laver eller har mere end et formål - har det lav samhørighed .

Du vil have dine klasser til at have høj samhørighed. De burde kun have et ansvar, og hvis du griber dem i at have mere - er det måske tid til at opdele det.

Kobling

Kobling fanger kompleksiteten mellem at forbinde forskellige klasser. Du vil have, at dine klasser skal have så få og så enkle forbindelser til andre klasser som muligt, så du kan bytte dem ud i fremtidige begivenheder (som at ændre webrammer). Målet er at have løs kobling .

På mange sprog opnås dette ved tung brug af grænseflader - de abstraherer den specifikke klasse, der håndterer logikken, og repræsenterer en slags adapterlag, hvor enhver klasse kan tilslutte sig selv.

Adskillelse af bekymringer

Separation of Concerns (SoC) er tanken om, at et softwaresystem skal opdeles i dele, der ikke overlapper funktionaliteten. Eller som navnet siger - bekymring - Et generelt udtryk om alt, hvad der giver en løsning på et problem - skal adskilles på forskellige steder.

En webside er et godt eksempel på dette - den har sine tre lag (information, præsentation og adfærd) adskilt på tre steder (henholdsvis HTML, CSS og JavaScript).

Hvis du ser igen på RPG- Heroeksemplet, vil du se, at det havde mange bekymringer i starten (anvend buffs, beregne angrebsskader, håndtere lager, udstyre varer, administrere attributter). Vi adskilt disse bekymringer gennem nedbrydning i mere sammenhængende klasser, som abstrakterer og indkapsler deres detaljer. Vores Heroklasse fungerer nu som et sammensat objekt og er meget enklere end før.

Udbytte

Anvendelse af sådanne principper kan se alt for kompliceret ud for et så lille stykke kode. Sandheden er, at det er et must for ethvert softwareprojekt, som du planlægger at udvikle og vedligeholde i fremtiden. At skrive en sådan kode har lidt overhead i starten, men det betaler sig flere gange i det lange løb.

Disse principper sikrer, at vores system er mere:

  • Kan udvides : Høj samhørighed gør det lettere at implementere nye moduler uden bekymring for ikke-relateret funktionalitet. Lav kobling betyder, at et nyt modul har færre ting at oprette forbindelse til, derfor er det lettere at implementere.
  • Vedligeholdelig : Lav kobling sikrer, at en ændring i et modul generelt ikke påvirker andre. Høj samhørighed sikrer, at en ændring i systemkrav kræver ændring af et så lille antal klasser som muligt.
  • Genanvendelig : Høj samhørighed sikrer, at et moduls funktionalitet er komplet og veldefineret. Lav kobling gør modulet mindre afhængigt af resten af ​​systemet, hvilket gør det lettere at genbruge i anden software.

Resumé

Vi startede med at introducere nogle grundlæggende objekttyper på højt niveau (Entity, Boundary og Control).

Vi lærte derefter nøgleprincipper i strukturering af de nævnte objekter (Abstraktion, generalisering, komposition, nedbrydning og indkapsling).

For at følge op introducerede vi to softwarekvalitetsmålinger (kobling og samhørighed) og lærte om fordelene ved at anvende disse principper.

Jeg håber, at denne artikel gav et nyttigt overblik over nogle designprincipper. Hvis du ønsker at videreuddanne dig inden for dette område, her er nogle ressourcer, jeg vil anbefale.

Yderligere læsninger

Designmønstre: Elementer af genanvendelig objektorienteret software - uden tvivl den mest indflydelsesrige bog i marken. Lidt dateret i sine eksempler (C ++ 98), men mønstrene og ideerne forbliver meget relevante.

Growing Object-Oriented Software Guided by Tests - En fantastisk bog, der viser, hvordan man praktisk anvender principper skitseret i denne artikel (og mere) ved at arbejde igennem et projekt.

Effektivt softwaredesign - En førsteklasses blog, der indeholder meget mere end designindsigt.

Software Design og Arkitektur Specialisering - En stor serie af 4 videokurser, der lærer dig effektivt design i hele dets anvendelse på et projekt, der spænder over alle fire kurser.

Hvis denne oversigt har været informativ for dig, kan du overveje at give den den mængde klapper, du synes, den fortjener, så flere mennesker kan snuble over den og få værdi af den.