Et kort overblik over objektorienteret softwaredesign

Demonstreret ved at implementere et rollespilsspils klasser

Zeppelin af Richard Wright

Introduktion

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

For at lykkes med at skrive en klar, vedligeholdelig og udvidelig kode af høj kvalitet, skal du vide om designprincipper, der har vist sig effektive gennem årtiers erfaring.

Videregivelse: Eksemplet, vi skal gennemgå, vil være i Python. Eksempler er der for at bevise et punkt og kan være slurvede på andre, indlysende måder.

Objekttyper

Da vi modellerer vores kode omkring objekter, ville det være nyttigt at skelne mellem deres forskellige ansvarsområder og variationer.

Der er tre typer objekter:

1. Enhedsobjekt

Dette objekt svarer generelt til en enhed i den virkelige verden i problemområdet. Lad os sige, at vi bygger et rollespil (RPG), et enhedsobjekt ville være vores enkle Hero-klasse:

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

2. Kontrolobjekt

Kontrolobjekter (undertiden også kaldet Managerobjekter) er ansvarlige for koordinering af andre objekter. Dette er objekter, der kontrollerer og gør brug af andre objekter. Et godt eksempel i vores RPG-analogi ville være Fight-klassen, der kontrollerer to helte og får dem til at kæmpe.

Indkapsling af logikken for en kamp i en sådan klasse giver dig flere fordele: en af ​​dem er handlingens lette udvidelsesmulighed. Du kan meget let give en ikke-spiller karakter (NPC) -type, så helten kan kæmpe, forudsat at den udsætter det samme API. Du kan også meget let arve klassen og tilsidesætte noget af funktionaliteten for at imødekomme dine behov.

3. Afgrænsningsobjekt

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 til at oversætte en tastaturindgang (som en mellemrumstast) til en genkendelig domænehændelse (f.eks. Et tegnhopp).

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, ville en penge- eller skade klasse være en god pasform. Nævnte objekter lader os let skelne, finde og fejlfinde 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 enhedsobjekter.

De vigtigste designprincipper

Designprincipper er regler inden for software-design, der har vist sig værdifulde gennem årene. Hvis du følger dem strengt, vil du hjælpe dig med at sikre, at din software er af højeste kvalitet.

abstraktion

Abstraktion er ideen om at forenkle et koncept til dets nøglen i en 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 Fight-klassen er struktureret. Den måde, du bruger den på, er så enkel som muligt - du giver den to helte som argumenter under indvielse og kalder metoden fight (). Intet mere, intet mindre.

Abstraktion i din kode skal følge reglen om mindst overraskelse. Din abstraktion skal ikke overraske nogen med unødvendig og ikke-relateret adfærd / 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 tænkes som at sætte noget i en kapsel - du begrænser dens eksponering for omverdenen. I software hjælper begrænsning af adgang til indre objekter og egenskaber med dataintegritet.

Indkapsling af 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 nemt kan omarbejde den indre logik, mens du bevarer de offentlige dele og være sikker på, at du ikke har ødelagt noget. Som en bivirkning bliver det enklere at arbejde med den indkapslede funktionalitet, da du har mindre ting at tænke på.

På de fleste sprog gøres dette gennem de såkaldte adgangsmodifikatorer (private, beskyttede osv.). Python er ikke det bedste eksempel på dette, da det mangler sådanne eksplicitte modifikatorer, der er indbygget i runtime, men vi bruger konventioner til at løse dette. Præfikset _ til variablerne / metoderne betegner dem som private.

Forestil dig for eksempel, at vi ændrer vores Fight # _run_attack-metode for at returnere en boolsk variabel, der angiver, om kampen er ovre snarere end at gøre en undtagelse. Vi vil vide, at den eneste kode, vi måske har brudt, er inde i Fight-klassen, 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 små konsekvenser som muligt er den fleksibilitet, du ønsker som udvikler.

nedbrydning

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

Forestil dig, at vi ville integrere flere RPG-funktioner som buffs, inventar, udstyr og karakteregenskaber oven på vores helt:

Jeg antager, at du kan fortælle, at denne kode bliver ret rodet. Vores Hero-objekt gør for mange ting på en gang, og denne kode bliver ret skør som et resultat af det.

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

Svaret er at nedbryde Hero-objektet i flere mindre objekter, som hver omfatter en del af funktionaliteten.

En renere arkitektur

Efter nedbrydning af vores Heroobjektets funktionalitet i HeroAttributter, HeroInventory, HeroEquipements og HeroBuff-objekter, vil tilføjelse af fremtidig funktionalitet blive lettere, mere indkapslet og bedre abstraheret. Du kan fortælle, at vores kode er måde renere og klarere på, 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 and a Zone-objekt.

  • aggregering - Definerer et svagt “have-a” -forhold mellem en helhed og dens dele. Betraktes som svag, fordi delene kan eksistere uden helheden.

Eksempel: HeroInventory and Item.
En HeroInventory kan have mange varer, og en vare kan høre til ethvert HeroInventory (f.eks. Handelsvarer).

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

Eksempel: Hero and HeroAttribute.
Dette er heltenes attributter - du kan ikke ændre deres ejer.

Generalisering

Generalisering er muligvis det vigtigste designprincip - det er processen med at udtrække delte egenskaber og kombinere dem ét sted. Alle af os ved om begrebet funktioner og klassearv - begge dele er en slags generalisering.

En sammenligning kan muligvis rydde op: 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 Hero- og NPC-klasses funktionalitet til en fælles stamfar kaldet Enhed. Dette opnås altid gennem arv.

I stedet for at lade vores NPC- og Hero-klasser implementere alle metoder to gange og overtræde TØR-princippet, reducerede vi kompleksiteten ved at flytte deres fælles funktionalitet til en baseklasse.

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

Arv misbruges ofte af amatørprogrammører, sandsynligvis fordi det er en af ​​de første OOP-teknikker, de griber fat på på grund af dens 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 sammensætning, kan kaldes et sammensat objekt. Det er vigtigt, at denne komposit er enklere end summen af ​​dens kammerater. Når vi kombinerer flere klasser til en, ønsker vi at hæve abstraktionsniveauet højere og gøre objektet enklere.

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

Som sagt er sammensætning at foretrække 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 stash det i en baseklasse, du har arvet.

Lad os illustrere et muligt problem med overarvende funktionalitet:

Vi har lige tilføjet bevægelse til vores spil.

Som vi lærte, brugte vi i stedet for at duplikere koden generalisering til at sætte funktionerne move_right og move_left i entitetsklassen.

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

en god montering :)

Montering er også nødt til at flytte 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:

Flyt blot flytningslogikken til en separat klasse MoveableEntity eller MoveableObject, der kun har denne funktionalitet. Mount-klassen kan derefter arve det.

Hvad skal vi så gøre, hvis vi ønsker mounts, der har helbred, men ikke kan angribe? Mere opsplitning i underklasser? Jeg håber, du kan se, hvordan vores klassehierarki ville begynde at blive kompleks, selvom vores forretningslogik stadig er temmelig enkel.

En noget bedre tilgang ville være at abstrahere bevægelseslogikken i en bevægelsesklasse (eller et andet bedre navn) og indlede den i de klasser, der måtte have brug for den. Dette pakker funktionaliteten pænt og gør den genanvendelig på tværs af alle slags objekter, der ikke er begrænset til enhed.

Hurra, komposition!

Kritisk tænkningsklausul

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

Som alle ting kan for meget være en dårlig ting. Undertiden 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 vigtigste træk din evne til kritisk at evaluere den bedste tilgang til din unikke situation og ikke blindt følge og anvende vilkårlige regler.

Samhørighed, kobling & adskillelse af bekymringer

Samhørighed

Samhørighed repræsenterer klarheden i ansvarsområder inden for et modul eller med andre ord - dets kompleksitet.

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

Du ønsker, at dine klasser skal have høj samhørighed. De skulle kun have et ansvar, og hvis du fanger dem, der har mere - kan det være tid til at opdele det.

kobling

Kobling fanger kompleksiteten mellem at forbinde forskellige klasser. Du ønsker, 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 kraftig 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 ideen om, at et softwaresystem skal opdeles i dele, der ikke overlapper hinanden i funktionalitet. Eller som navnet siger - bekymring - Et generelt udtryk om alt, hvad der giver en løsning på et problem - skal adskilles forskellige steder.

En webside er et godt eksempel på dette - det har sine tre lag (Information, Præsentation og Opførsel) adskilt i tre steder (henholdsvis HTML, CSS og JavaScript).

Hvis du igen ser på RPG Hero-eksemplet, vil du se, at det havde mange bekymringer helt i begyndelsen (anvende buffs, beregne angrebskader, håndtere inventar, udstyr udstyr, administrere attributter). Vi adskilte disse bekymringer gennem nedbrydning i mere sammenhængende klasser, som abstraherer og indkapsler deres detaljer. Vores helteklasse fungerer nu som et sammensat objekt og er meget enklere end før.

Udbetaling

Anvendelse af sådanne principper ser måske alt for kompliceret ud for et så lille stykke kode. Sandheden er det et must for ethvert softwareprojekt, som du planlægger at udvikle og vedligeholde i fremtiden. At skrive en sådan kode har en smule omkostninger helt 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 uafhængig funktionalitet. Lav kobling betyder, at et nyt modul har mindre 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 en ændring i systemkravene 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 (Enhed, grænse og kontrol).

Derefter lærte vi nøgleprincipper i strukturering af nævnte objekter (Abstraktion, generalisering, sammensætning, 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 nævnte principper.

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

Yderligere læsninger

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

Dyrkning af objektorienteret software Guidet af test - En fantastisk bog, der viser, hvordan man praktisk anvender principper, der er beskrevet i denne artikel (og mere) ved at arbejde gennem et projekt.

Effektiv software Design - En top notch blog, der indeholder meget mere end design indsigt.

Software Design og arkitektur specialisering - En stor serie af 4 videokurser, der lærer dig effektiv design i hele dens anvendelse på et projekt, der spænder over alle fire kurser.

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