Det er ikke så svært at skrive en pæn, veldesignet og flot kode, som det ser ud til. Det kræver en lille indsats at lære de vigtigste regler at kende og bare bruge dem i din kode. Og det behøver ikke at være alt på én gang, men når du føler dig tryg ved én ting, kan du prøve at tænke på en anden og så videre.
Byg solid, ikke dum kode
Lad os starte med at introducere de mest elementære regler, som kaldes SOLID. Det er et udtryk, der beskriver en samling af designprincipper for god Kode som blev opfundet af Robert C. Martin, og hvordan det foregår:
S betyder princippet om fælles ansvar
Den siger, at en klasse skal have én og kun én grund til at blive ændret. Med andre ord bør en klasse kun have ét job at udføre, så vi bør undgå at skrive store klasser med mange ansvarsområder. Hvis vores klasse skal gøre mange ting, skal hver af dem uddelegeres til separate klasser.
klasse Notifikation
def initialize(params)
@params = params
slut
def opkald
EmailSender.new(message).call
end
privat
def besked
# noget implementering
slut
slut
O betyder åbent/lukket princip
Den siger, at man skal kunne udvide en klasses adfærd uden at ændre den. Det skal med andre ord være nemt at udvide en klasse uden at ændre den. Det kan vi f.eks. opnå ved at bruge strategimønstre eller dekoratorer.
klasse Notifikation
def initialize(params, afsender)
@params = params
@sender = afsender
slut
def opkald
afsender.kald besked
end
privat
def besked
# noget implementering
slut
slut
klasse EmailSender
def call(besked)
# noget implementering
slut
slut
klasse SmsSender
def call(besked)
# noget implementering
slut
slut
Med dette design er det muligt at tilføje nye afsendere uden at ændre i koden.
L betyder Liskovs substitutionsprincip
Den siger, at afledte klasser skal kunne erstattes af deres basisklasser. Med andre ord skal brugen af klasser, der kommer fra den samme forfader, være let at erstatte med andre efterkommere.
klasse Logger {
info (besked) {
console.info(this._prefixFor('info') + besked)
}
error (message, err) {
console.error(this._prefixFor('error') + message)
if (err) {
console.error(err)
}
}
_prefixFor (type) {
// en eller anden implementering
}
}
class ScepticLogger extends Logger {
info (besked) {
super.info(besked)
console.info(this._prefixFor('info') + 'Og det var alt, hvad jeg havde at sige.')
}
error (message, err) {
super.error(besked, err)
console.error(this._prefixFor('error') + 'Det var da noget!')
}
}
Vi kan nemt udskifte klassens navn, fordi begge har præcis samme brugerflade.
Jeg mener princippet om adskillelse af grænseflader
Den siger, at man skal lave finkornede grænseflader, som er klientspecifikke. Hvad er en grænseflade? Det er en fastlagt måde at bruge en del af koden på. Så en overtrædelse af denne regel kunne f.eks. være en klasse med for mange metoder eller en metode med for mange argumentmuligheder. Et godt eksempel til at visualisere dette princip er et repository-mønster, ikke kun fordi vi ofte lægger mange metoder ind i en enkelt klasse, men også fordi disse metoder er udsat for en risiko for at acceptere for mange argumenter.
# ikke det bedste eksempel denne gang
klasse NotificationRepository
def find_all_by_ids(ids:, info:)
notifications = Notification.where(id: ids)
info ? notifications.where(type: :info) : notifications
end
end
# og en bedre
klasse NotificationRepository
def find_all_by_ids(ids:)
Notifikation.where(id: ids)
end
def find_all_by_ids_info(ids:)
find_all_by_ids(ids).where(type: :info)
end
end
D betyder Dependency Inversion Principle.
Den siger, at man skal være afhængig af abstraktioner, ikke af konkretioner. Med andre ord bør en klasse, der bruger en anden, ikke være afhængig af dens implementeringsdetaljer, alt, hvad der er vigtigt, er brugergrænsefladen.
klasse Notifikation
def initialize(params, afsender)
@params = params
@sender = afsender
slut
def opkald
afsender.kald besked
end
privat
def besked
# noget implementering
slut
slut
Alt, hvad vi behøver at vide om afsenderobjektet, er, at det udstiller `call`-metoden, som forventer beskeden som et argument.
Ikke den bedste kode nogensinde
Det er også meget vigtigt at kende til de ting, som bør undgås, når man skriver kode, så her kommer endnu en samling med DUMME principper, som gør koden uvedligeholdelig, svær at teste og genbruge.
S betyder Singleton
Singletons betragtes ofte som anti-mønstre og bør generelt undgås. Men hovedproblemet med dette mønster er, at det er en slags undskyldning for globale variabler/metoder og hurtigt kan blive overbrugt af udviklere.
T betyder tæt kobling
Det bevares, når en ændring i et modul også kræver ændringer i andre dele af applikationen.
U betyder uprøvbarhed
Hvis din kode er god, bør det være sjovt at skrive test, ikke et mareridt.
P betyder for tidlig optimering
Ordet for tidligt er nøglen her, hvis du ikke har brug for det nu, så er det spild af tid. Det er bedre at fokusere på en god, ren kode end på nogle mikrooptimeringer - som generelt medfører mere kompleks kode.
Jeg mener indeskriptiv navngivning
Det er det sværeste ved at skrive god kode, men husk, at det ikke kun er for resten af din hold men også for fremtidige dig, så behandl dig ordentligt 🙂 Det er bedre at skrive et langt navn til en metode, men det siger alt, end et kort og gådefuldt.
D betyder duplikering
Hovedårsagen til dobbeltarbejde i koden er princippet om tæt kobling. Hvis din kode er tæt koblet, kan du ikke genbruge den, og der opstår duplikeret kode, så følg DRY, og lad være med at gentage dig selv.
Det er ikke så vigtigt lige nu
Jeg vil også gerne nævne to meget vigtige ting, som ofte udelades. Du har sikkert hørt om den første - det er YAGNI, som betyder: Du får ikke brug for det. Fra tid til anden observerer jeg dette problem, når jeg laver kodegennemgang eller endda skriver min egen kode, men vi bør ændre vores tankegang, når vi implementerer en funktion. Vi skal skrive præcis den kode, vi har brug for i dette øjeblik, ikke mere eller mindre. Vi skal huske på, at alting ændrer sig meget hurtigt (især applikationskrav), så der er ingen grund til at tro, at noget en dag vil blive nyttigt. Spild ikke din tid.
Jeg forstår det ikke
Og den sidste ting, som nok ikke er helt indlysende, og som måske er ret kontroversiel for nogle, er en beskrivende kode. Med det mener jeg ikke kun at bruge de rigtige navne til klasser, variabler eller metoder. Det er rigtig, rigtig godt, når hele koden er læsbar ved første øjekast. Hvad er formålet med den meget korte kode, når den er så gådefuld, som den kan være, og ingen ved, hvad den gør, bortset fra den person, der har skrevet den? Efter min mening er det bedre at skrive nogle tegntilstandserklæringernoget andet mere end et ord og så i går sidde og undre sig: vent, hvad er resultatet, hvordan skete det og så videre.
const params = [
{
film: [
{ titel: 'The Shawshank Redemption' },
{ title: 'En fløj over gøgereden' }
]
},
{
film: [
{ titel: 'Saving Private Ryan' },
{ titel: 'Pulp Fiction' },
{ titel: 'The Shawshank Redemption' },
]
}
]
// første forslag
function uniqueMovieTitlesFrom (params) {
const titles = params
.map(param => param.movies)
.reduce((prev, nex) => prev.concat(next))
.map(movie => movie.title)
return [...new Set(titles)]
}
// andet forslag
function uniqueMovieTitlesFrom (params) {
const titles = {}
params.forEach(param => {
param.movies.forEach(movie => titles[movie.title] = true)
})
return Object.keys(titler)
}
For at opsummere
Som du kan se, er der mange regler at huske på, men som jeg nævnte i begyndelsen, er det et spørgsmål om tid at skrive en god kode. Hvis du begynder at tænke på en forbedring af dine kodevaner, vil du se, at en anden god regel vil følge, for alle gode ting opstår af sig selv ligesom de dårlige.
Læs mere om det:
Hvad er Ruby on Jets, og hvordan bygger man en app med det?
Vuelendar. Et nyt Codest-projekt baseret på Vue.js
Codests ugentlige rapport med de bedste tech-artikler. Bygning af software til 50 millioner samtidige sockets (10)