Det er mange diskusjoner, artikler og blogger om temaet kodekvalitet. Folk sier - bruk teknikker Testdrevet ! Testing er et 'must have' for å starte en refactoring! Alt dette er greit, men vi er i 2016, og det er mange produkter og kodebaser som fortsatt er i produksjon, som ble opprettet for ti, femten og til og med tjue år siden. Det er ingen hemmelighet at mange av dem har eldre kode med lav testdekning.
Selv om jeg alltid vil være i forkant, eller til og med blodig, i teknologiverden - engasjert i nye prosjekter og teknologier - er det dessverre ikke alltid mulig, og jeg må ofte håndtere utdaterte systemer. Jeg liker å si at når du utvikler deg fra bunnen av, fungerer du som en skaper og skaper ny materie. Men når du jobber med eldre kode, er du som en kirurg - du vet hvordan systemet fungerer generelt, men du er aldri sikker på om pasienten kommer godt ut av 'operasjonen'. Og siden det er eldre kode, er det ikke mange oppdaterte tester du kan stole på. Dette betyr at ofte er et av de første trinnene å dekke det med testing. For å være mer presis, ikke bare for å gi dekning, men for å utvikle en testdekningsstrategi.
I utgangspunktet var det jeg trengte å bestemme hvilke deler (klasser / pakker) av systemet vi trengte å dekke med tester først, hvor vi trenger enhetstester, hvor avhørstester ville være mest nyttige osv. Det er mange måter å nærme seg denne typen analyser, og den jeg har brukt er kanskje ikke den beste, men den ligner på en automatisk tilnærming. Når tilnærmingen min er implementert, tar det liten tid å gjøre selve analysen, og enda viktigere, det gir litt moro for analysen av eldre kode.
Hovedideen her er å analysere to beregninger - kobling (f.eks. Afferent kobling eller CA) og kompleksitet (f.eks. Cyklomatisk kompleksitet).
Den første måler hvor mange klasser klassen vår måler, så den forteller oss i utgangspunktet hvor nær en bestemt klasse er hjertet i systemet; jo flere klasser det er som bruker klassen vår, desto viktigere er det å dekke dem med tester.
På den annen side, hvis en klasse er veldig enkel (for eksempel den inneholder bare konstanter), så hvis den brukes av mange andre deler av systemet, er det ikke så viktig å lage en test for den. Det er her den andre beregningen kan hjelpe. Hvis en klasse inneholder mye logikk, vil den syklomatiske kompleksiteten være høy.
Den samme logikken kan brukes omvendt; For eksempel, selv om en klasse ikke brukes av mange klasser og kun representerer en bestemt brukssak, er det fortsatt fornuftig å dekke den med tester hvis den interne logiske bruken er kompleks.
Det er imidlertid en advarsel: la oss si at vi har to klasser - en med CA på 100 og kompleksitet på 2, og den andre med CA på 60 og kompleksitet på 20. Selv om summen av beregningene er høyere for det første, bør vi dekk den andre først. Dette er fordi den første klassen blir brukt av mange andre klasser, men den er ikke veldig kompleks. På den annen side blir andre klasse også brukt av mange andre klasser, men den er relativt mer kompleks enn første klasse.
For å oppsummere: vi må identifisere klassene med høy CA og syklomatisk kompleksitet. I matematiske termer trenger du en treningsfunksjon som kan brukes som klassifisering. - f (CA, Complexity) - hvorav verdiene øker sammen med CA og Complexity.
Generelt sett skal klassene med de minste forskjellene mellom de to beregningene ha høyest prioritet for testdekning.Å finne verktøy for å beregne CA og kompleksitet for hele kodebasen og gi en enkel måte å hente ut denne informasjonen i CSV-format, viste seg å være utfordrende. I løpet av søket mitt fant jeg to verktøy som er gratis, så det ville være urettferdig å ikke nevne dem:
Hovedproblemet her er at vi har to kriterier - CA og syklomatisk kompleksitet - så vi må kombinere dem og konvertere dem til en enkelt skalarverdi. Hvis vi hadde en litt annen oppgave - for eksempel å finne en klasse med den verste kombinasjonen av våre kriterier - ville vi ha et klassisk multi-objektiv optimaliseringsproblem:
Vi må finne et punkt på den såkalte Pareto-fronten (rød på bildet over). Det interessante med Pareto-settet er at hvert punkt i settet er en løsning på optimaliseringstesten. Hver gang vi beveger oss nedover den røde linjen, må vi forplikte oss til våre kriterier - hvis den ene forbedres, forverres den andre. Dette kalles skalering og sluttresultatet avhenger av hvordan det gjøres.
Det er mange teknikker vi kan bruke her. Hver har sine egne fordeler og ulemper. Imidlertid er de mest populære lineær skalarisering og den som er basert på en referansepunkt . Det lineære er det enkleste. Treningsfunksjonen vår vil se ut som en lineær kombinasjon av CA og kompleksitet:
f (CA, kompleksitet) = A × CA + B × kompleksitet
der A og B er noen koeffisienter.
Poenget som representerer en løsning for vårt optimaliseringsproblem er på linjen (blå på bildet nedenfor). Nettopp det vil være skjæringspunktet mellom den blå linjen og den røde Pareto-fronten. Vårt opprinnelige problem er ikke akkurat et optimaliseringsproblem. Men vi trenger å lage en kategoriseringsfunksjon. La oss vurdere to verdier av kategoriseringsfunksjonen vår, i utgangspunktet to verdier i vår rang-kolonne.
R1 = A ∗ CA + B ∗ Kompleksitet og R2 = A ∗ CA + B ∗ Kompleksitet
Noen av formlene som er skrevet ovenfor, er ligninger av linjer, enda mer så disse linjene er parallelle. Hvis vi tar flere kategoriseringsverdier i betraktning, vil vi ha flere linjer og derfor flere punkter der Pareto-linjen krysser de blå linjene (stiplet). Disse poengene vil være klasser som tilsvarer en bestemt kategorisert verdi.
Dessverre er det et problem med denne tilnærmingen. For hvilken som helst linje (kategorisert verdi) vil vi ha poeng med liten CA og veldig stor kompleksitet (og omvendt) i den. Dette setter øyeblikkelig poeng med stor forskjell mellom metriske verdier først i listen, som er akkurat det vi ønsket å unngå.
Den andre måten å gjøre skalariseringen på er basert på referansepunktet. Referansepunktet er et punkt med maksimale verdier for begge kriteriene:
(maks (CA), maks (kompleksitet))
Treningsfunksjonen vil være avstanden mellom referansepunktet og datapunktene:
f (CA, kompleksitet) = √ ((CA - CA)2+ (Kompleksitet - Kompleksitet)2)
Vi kan tenke på denne treningsfunksjonen som en sirkel med sentrum på referansepunktet. Radien i dette tilfellet er verdien av kategoriseringen. Løsningen på optimaliseringsproblemet vil være det punktet der sirkelen berører Pareto-fronten. Løsningen på det opprinnelige problemet vil være sett med punkter som tilsvarer de forskjellige sirkelradiene som vist i følgende bilde (deler av sirkler for forskjellige kategorier vises som blå prikkede kurver):
Denne tilnærmingen håndterer ekstreme verdier bedre, men det er fortsatt to problemer: For det første - Jeg vil gjerne ha flere poeng nær referansepunktene for bedre å løse problemet vi har møtt med lineær kombinasjon. For det andre - CA og syklomatisk kompleksitet er iboende forskjellige og har forskjellige verdisett, så vi må normalisere dem (f.eks. Slik at alle verdiene til begge beregningene er fra 1 til 100)
Her er et lite triks vi kan bruke for å løse det første problemet - i stedet for å se på CA og syklomatisk kompleksitet, kan vi se på deres omvendte verdier. Referansepunktet i dette tilfellet vil være (0,0). For å løse det andre problemet kan vi normalisere beregningene ved hjelp av en minimumsverdi. Slik ser det ut:
Normalisert og omvendt kompleksitet - NormKompleksitet :
(1 + min (kompleksitet)) / (1 + kompleksitet) ∗ 100
Invertert og normalisert vekselstrøm - NormCA :
(1 + min (CA)) / (1 + CA) ∗ 100
Merk: Jeg la til 1 for å sikre at det ikke er noen divisjon med 0. t
Følgende bilde viser en graf med inverterte verdier:
Vi kommer til det siste trinnet - å beregne kategoriseringen. Som jeg nevnte, bruker jeg referansemetoden, så alt vi trenger å gjøre er å beregne vektorens lengde, normalisere den og ta den opp med viktigheten av å lage en enhetstest for en klasse. Her er den siste formelen:
Rang (NormComplexity, NormCA) = 100 - √ (NormComplexity2+ NormCA2) / √2
Det er flere tanker jeg vil legge til, men la oss først se på litt statistikk. Her er et histogram over koblingsmålingene:
Det som er interessant med dette bildet er antall klasser med lav CA (0-2). Klasser med CA på 0 brukes ikke i det hele tatt eller er tjenester på høyt nivå. Disse representerer sluttpunkter BRANN , så det er greit hvis vi har mange av dem. Men klassene med CA i 1 er de som brukes direkte av endepunktene, og vi har flere av disse klassene enn endepunktene. Hva betyr dette fra et arkitektur / designperspektiv?
Generelt sett betyr det at vi har en skriptorientert tilnærming - vi skript hver forretningsdokument separat (vi kan ikke gjenbruke koden ettersom forretningssakene er veldig forskjellige). Hvis det er tilfelle, så er det definitivt en luktekode og vi må refaktorere. Hvis ikke, betyr det at samholdet i systemet vårt er lavt, i dette tilfellet trenger vi også refakturering, men arkitekturrefaktoring i dette tilfellet.
Den tilleggsinformasjonen vi kan få fra histogrammet ovenfor er at vi kan filtrere lave koblingsklasser (CA på {0,1}) helt fra listen over klasser som er tilgjengelige for dekning med enhetstester. De samme klassene er imidlertid gode kandidater for integrering / funksjonstesting.
Du finner alle skriptene og ressursene jeg brukte i dette GitHub-arkivet: ashalitkin / code-base-stats .
Ikke nødvendigvis. Først og fremst handler alt om statisk analyse, ikke kjøretid. Hvis en klasse er filtrert fra mange andre klasser, kan det være et tegn på at den brukes mye, men det er ikke alltid tilfelle. For eksempel vet vi ikke om funksjonaliteten brukes mye av sluttbrukere. For det andre, hvis utformingen og kvaliteten på systemet er god nok, vil helt sikkert forskjellige deler / lag av systemet kobles fra over grensesnittene, så en statisk vekselstrømsanalyse vil ikke gi oss et sant bilde. Jeg antar at det er en av hovedårsakene til at CA ikke er et populært verktøy som Sonar. Heldigvis er det bra med oss, hvis du husker at vi er interessert i å bruke dette spesifikt på stygge gamle kodebaser.
Generelt vil jeg si at kjøretidsanalyse vil gi bedre resultater, men dessverre er det mye dyrere, tidkrevende og komplekst, så vår tilnærming er potensielt et nyttig og billigere alternativ.
I slekt: Unikt hovedansvar: En oppskrift på utmerket kode