Følgende er en
serie på tre artikler om generisk programmering i tre artikler:
1.
Om
interfaces og abstrakte base klasser
2.
Om
nedarvning, casts og generics
3.
Om
Polymorfi
Det er vigtigt
at forstå at denne serie kun overfladisk gennemgår emnerne.
Serien er altså
kun ment som en slags appetitvækker, der skal sætte dig igang med generisk
programmering i C# på .net.
Det er også vigtigt
at du selv prøver at lave dine egne kode eksempler når du har læst om et emne,
på den måde får du den ’hands on’ oplevelse, der skal til for at forstå
emnerne...
Meget af det som
gennemgåes i denne serie gælder også i andre objektorienterede sprog; for eks.
Java og C++, dog kan der være visse forskelle, som jeg ikke har taget med i
betragtning.
Formålet med
generisk programmering er at undgå gentagelser i koden.
Kode, der
bruges flere steder skal ikke skrives igen og igen, men ved hjælp af generisk
programering skal det placeres ét sted.
Og kan dermed
genbruges.
Desuden foregår
vedligeholdelse ét sted, hvilket er en stor fordel.
Har du en
funktionalitet spredt udover hele din kodebase, og finder en fejl i den
funktionalitet, skal du ud at rette alle steder, hvor den findes ( finder du
mon dem alle).
Har du én gang siddet og vedligeholdt andres
spaghetti kode, véd du hvorfor det er godt at lave generisk programmering.
Generisk kode
er nemmere at overskue, end kode, der ikke er ordentlig opdelt og hvor funktionaliteten
ligger spredt med let hånd ud over det hele.
Det er altid
værd at overveje, når du koder at andre kommer og skal se på det du laver og at
de ikke har den samme baggrund som du, for at forstå din kode.
Iøvrigt siger
erfaringen at kode du selv har lavet for et par måneder siden ligeså godt kunen
være lavet af en anden...
Det er vigtigt
at bruge tid på at sætte sig ind i det man kalder design patterns.
Design patterns
er mønstre som man har opdaget går igen og igen i god generisk kode.
Hver gang du står overfor et problem, er det værd at overveje om nogen måske har
stået i dette problem eller et ligende før dig; Og fundet en god generel løsning på problemet.
Design patterns
blev første gang publiceret i bogform af fire herrer man kalder Gang Of For
(GoF), her er tolv grundlæggende design
patterns, som kan bruges i mange problemstillinger når du koder. De tolv design
patterns kan du finde her http://www.dofactory.com/Patterns/Patterns.aspx
.
Endvidere har
en fyr ved navn Martin Fowler arbejdet en del videre med design patterns, og
fundet rigtig mange flere, det er værd at tage et kig på hans hjemmeside http://www.martinfowler.com/
Lær de
forskellige teknikker i generisk programmering, og find den der passer i den
situation du står i. Så undgår du alt for mange kommentarer som denne, Fra dem
der skal vedligeholde din kode i fremtiden:
//#!£!$!%!$£$$€$€6
<- Dette skulle forestille tegneserie bandeord, men det lader til at Anders
And er bedre til det end jeg ;-)
I denne artikel
skal vi se lidt nærmere på interfaces og akstrakte base klasser.
Det er to
funktionaliteter i C# programmerings sproget, der minder en hel del om
hinanden.
Først vil jeg
altså fortælle lidt om interfaces, hvad de er og hvorfor du skal bruge
dem.
Jo, det kan
betragtes som en slags kontrakt mellem to klasser:
Altså et sæt af
metoder og properties, som en klasse implementerer og som en anden klasse har
brug for.
Et eksempel på
et interface som er meget almindelig brugt og som du sikkert har brugt uden at
tænke over det er IEnumerable<T> (her bruges generics som jeg vil komme ind på i en senere artikel
i denne serie)
Ienumerable
bruges eks. Som regel i foreach loops.
IEenumerable har
defineret én metode nemlig GetEnumerator() (der
iøvrigt returnerer endnu et interface).
Det betyder at en
klasse der implementerer IEnumerable
lover at implementerer metoden GetEnumerator -
hverken mere eller mindre.
Endvidere
betyder det at en klasse, der ønsker at gøre brug af IEnumerable véd præcis
hvad den har at gøre med, nemlig den ene metode.
Ønsker man at
enumerere sig igennem en klasses indhold kan det altså være praktisk hvis den
implementerer netop dette interface.
Samtidig er det
vigtigt at forstå at der ikke er lovet mere end at der findes en sådan
funktionalitet, som specificeret i interfacet, hverken mere eller mindre.
Der kan (og vil
som oftest) være mange flere metoder og properties end de som er defineret i
interfacet, men de er uinteressante for den som skal bruge interfacet.
Så et interface
er altså en slags kontrakt mellem to klasser, men yderligere kan et interfaces
også bruges til at 'fange' ligheder mellem to klasser uden at tvinge dem til at
nedarve fra den samme base klasse.
Eksempelvis
ville det være stærkt uhensigtsmæssigt at alle klasser der skal loopes over via
et foreach loop skulle nedarve fra den samme klasse, da man kun kan nedarve fra
en klasse i .Net.
Dette løses
nemt og bekvemt med interfaces, hvor man blot fanger det klasser som man skal
loop’e over har til fælles - nemlig at de har en enumerator.
En vigtig
faktor når man designer applikationer, er hvor tæt to klasser, der arbejder
sammen, skal knyttes til hinanden.
Jeg har på et
tidspukt lavet en simpel asp.net form, hvor man via noget XML kan definerer
forskellige input felter.
Ifølge
kravspec.'en skulle data, som blev indtastet i formen, sendes til en email
adresse.
Så det ville
være naturligt at lave en klasse der, udfra noget XML, skaber en asp.net form.
Og herefter
lave en anden klasse, der kunne sende en email, udfra nogle felter.
Så kunne den
første klasse blot have en instans af email klassen og sende mails via denne
klasse.
Dette ville
virke og fungerer i praksis.
MEN MEN MEN,
hvad nu hvis jeg pludselig ønsker at gemme data i en database i stedet for at
sende en email.
Så skulle jeg
til at lave om i den første klasse ( den der skaber en form), selvom der ikke
var noget galt med den, udover at den var koblet for tæt til email klassen.
Så det jeg
istedet gjorde var at definerer det jeg kaldte et Ibackend interface, dette
interface har følgende metode:
bool Submit(FieldList fieldList, ref string
result);
Alt jeg kræver
af en backend er altså at man skal kunne submitte en felt liste til den og at
den skal give mig svar tilbage i en streng om hvordan det gik. Dermed har jeg
ikke koblet mig til en klasse, men et interface, som mange klasser kan
implementere.
Nu skal jeg
bare skyde en factory klasse (dette er et design pattern, kender du ikke design
patterns, er det det næste du skal til at igang med ;-), ind som kan finde ud
af skabe en Ibackend for mig.
Jeg laver en
klasse som udfra konfiguration kan skabe en instans af en emailbackend klasse.
Se, jeg er
rigtig glad for at jeg gjorde det, for her den anden dag kom der en
projektleder op til mig og spurgte: "den der form to email ting du lavede
den for noget tid siden, kan den egentlig smide noget i en database".
Hertil kunne jeg svare at den kan den ikke, men jeg skal bare have lave en
backend, der implementerer Ibackend og så er den hjemme....
Dette at en
anden klasse skydes ind imellem og skaber en instans udfra noget konfiguration,
kaldes iøvrigt dependency injection og det hat Martin Fowler skrevet noget
fornuftigt om...
Interfaces kan
yderligere bruges til at skjule implementation for brugerer af et library man
laver.
Ved at bruge
interfaces, ser brugeren kun de metoder, man ønsker de skal se.
Som du
forhåbentlig allerede har fundet ud af så kan man ikke lave noget som helst
funktionalitet i interfaces, bare definerer en kontrakt.
Til tider kan
man have brug for at implementerer noget funktionalitet i en base klasse uden
at den klasse skal kunne bruges til andet end at være en base klasse.
Dertil er
abstrakte base klasser designet.
En abstrakt
base klasse, er en klasse som man ikke kan lave en instans af.
Men man kan
have implementationer af metoder, og have metoder som ikke er implementeret.
For eksempel
har jeg en base klasse for en data access klasse, hvor jeg definerer to
abstrakte metoder:
Map() og
GetDataRowByID().
Map() mapper en
DataRow til en business entity.
GetDataRowByID()
henter en DataRow fra databasen.
Se i en
abstrakt base klasse kan jeg ikke vide hvordan man mapper til en business
entity og heller ikke hvordan man henter en DataRow fra databasen.
Men jeg kan
sige at man skal kunne gøre det.
Det har jeg
gjort ved at markerer metoderne som abstrakte, dermed skal alle klasser som
arver fra min abstrakte base klasse implementere denne metode (man får en
compilerfejl, hvis man ikke implementerer en abstrakt metode).
Men jeg kan
vide at når jeg har de to metoder så kan jeg godt fremskaffe en businessentity
udfra et id, nemlig sådan her:
public virtual businessentity GetById(string id)
{
DataRow data = GetDataRowById(id);
if (data != null)
{
return
Map(data);
}
return null;
}
På den måde
behøver jeg ikke i alle mine nedarvede klasser at lave denne simple mappning.
Det sparer
arbejde hver gang jeg skal implementerer en ny klasse. MEN det er derudover
også en stor fordel at jeg kun har denne implementation ét sted. Nu kan jeg
nemlig gå ind ét sted og rette og så slår det igennem i alle nedarvede klasser.
Det letter altså vedligeholdelse og konsistens betragteligt.