Refactoring C# Legacy Code - entwicklertag.de · Das Problem besteht aus drei Teilen: •...

95
Refactoring C# Legacy Code Stefan Lieser @StefanLieser http://refactoring-legacy-code.net

Transcript of Refactoring C# Legacy Code - entwicklertag.de · Das Problem besteht aus drei Teilen: •...

Refactoring C#Legacy Code

Stefan Lieser @StefanLieser

http://refactoring-legacy-code.net

Houston, we’ve had a problem.

http://er.jsc.nasa.gov/seh/13index.jpg

Wandelbarkeit

https://pixabay.com/de/chamäleon-hautnah-exotische-grün-1414084/

Investitionsschutz = Wandelbarkeit

Nicht-funktionale Anforderungen

Funktionale Anforderungen

Das Problem besteht aus drei Teilen:

• Lesbarkeit/Verständlichkeit ist nicht gegeben • Automatisierte Tests fehlen • Komplexe Refactorings sind notwendig

Das Dilemma:

• Um Tests zu ergänzen, => muss refactored werden.

• Um zu refactoren, => müssen Tests ergänzt werden.

KISS - Keep It Simple Stupid• „Einfach“ in Bezug auf

• Schreiben / Erstellen? • Lesen / Modifizieren !

Ein Sicherheitsnetz spannen:• Versionskontrolle • Continuous Integration • Code Reviews • Automatisierte Tests

Begründung für Refactorings:

• NICHT: Clean Code Developer, Prinzipien,Werte, Craftsmen, …

• Sondern Kundennutzen: • Neues Feature • Bugfix

Es gibt zwei Artenvon Refactorings:

einfache und

komplexe.

Einfache RefactoringsRename Extract Method Introduce VariableIntroduce Parameter etc.

Vollständig werkzeuggestützt durchführbar

Komplexe RefactoringsNicht mehr ausschließlich

werkzeuggestützt durchführbar

Einfache RefactoringsLesbarkeit herstellen.

Erkenntnisse über den Code im Code sichern.

publicdoubleEndpreis(intanzahl,doublenetto){ varnettoPreis=anzahl*netto;returnnettoPreis*1.19;}

publicdoubleEndpreis(intanzahl,doublenetto){ return(anzahl*netto)*1.19;}

publicdoubleEndpreis(intanzahl,doublenetto){ varnettoPreis=anzahl*netto;varbruttoPreis=nettoPreis*1.19;returnbruttoPreis;}

Introduce Variable

Introduce Variable

publicdoubleEndpreis(intanzahl,doublenetto){constdoublemwSt=0.19;varnettoPreis=anzahl*netto;varsteuer=nettoPreis*mwSt;varbruttoPreis=nettoPreis+steuer;returnbruttoPreis;}

publicdoubleEndpreis(intanzahl,doublenetto){ varnettoPreis=anzahl*netto;varbruttoPreis=nettoPreis*1.19;returnbruttoPreis;} Nicht mehr nur

toolgestützt!

publicIDictionary<string,string>ToDictionary(stringconfiguration){returnInsertIntoDictionary(SplitIntoKeyValuePairs(SplitIntoSettings(configuration)));}

"a=1;b=2;c=3"=>{{"a","1"},{"b","2"},{"c","3"}}

publicIDictionary<string,string>ToDictionary(stringconfiguration){varsettings=SplitIntoSettings(configuration);varpairs=SplitIntoKeyValuePairs(settings);varresult=InsertIntoDictionary(pairs);

returnresult;}

publicIDictionary<string,string>ToDictionary(stringconfiguration){returnInsertIntoDictionary(SplitIntoKeyValuePairs(SplitIntoSettings(configuration)));}

Introduce Variable

publicIDictionary<string,string>ToDictionary(stringconfiguration){//Splitconfigurationintosettingsvarsettings=configuration.Split(';');

//Splitsettingsintokey/valuepairsvarpairs=newList<KeyValuePair<string,string>>();foreach(varsettinginsettings){varkeyAndValue=setting.Split('=');pairs.Add(newKeyValuePair<string,string>(keyAndValue[0],keyAndValue[1]));}

//Insertpairsintodictionaryvarresult=newDictionary<string,string>();foreach(varpairinpairs){result.Add(pair.Key,pair.Value);}

returnresult;}

Extract Method

publicIDictionary<string,string>ToDictionary(stringconfiguration){varsettings=SplitIntoSettings(configuration);varpairs=SplitIntoPairs(settings);varresult=InsertIntoDictionary(pairs);returnresult;}

foreach(varsettinginsettings){varkey_and_value=setting.Split('=');result.Add(key_and_value[0],key_and_value[1]);}

Extract Method

foreach(varsettinginsettings){AddSettingToResult(setting,result);}

privatestaticvoidAddSettingToResult(stringsetting,Dictionary<string,string>result){varkey_and_value=setting.Split('=');result.Add(key_and_value[0],key_and_value[1]);}

if(DateTime.Now>=weckZeit){using(varsoundPlayer=newSoundPlayer(@"chimes.wav")){soundPlayer.Play();}timer.Stopp();}

Extract Method

if(WeckzeitErreicht()){using(varsoundPlayer=newSoundPlayer(@"chimes.wav")){soundPlayer.Play();}timer.Stopp();}

privateboolWeckzeitErreicht(){returnDateTime.Now>=weckZeit;}

if(WeckzeitErreicht()){using(varsoundPlayer=newSoundPlayer(@"chimes.wav")){soundPlayer.Play();}timer.Stopp();}

privateboolWeckzeitErreicht(){returnDateTime.Now>=weckZeit;}

Extract Method

if(WeckzeitErreicht()){Wecken();}

publicstringFormat(stringvorname,stringnachname,stringstrasse,stringhn,stringplz,stringort){return$"{vorname}{nachname},{strasse}{hn},{plz}{ort}";}

publicstringFormat(Adresseadresse,stringvorname,stringnachname){return$"{vorname}{nachname},{adresse.Strasse}{adresse.Hn},{adresse.Plz}{adresse.Ort}";}

Extract Class from Parameters

publicclassAdresse{privatestringstrasse;privatestringhn;privatestringplz;privatestringort;

publicAdresse(stringstrasse,stringhn,stringplz,stringort){this.strasse=strasse;this.hn=hn;this.plz=plz;this.ort=ort;}

publicstringStrasse{get{returnstrasse;}}...}

publicstring[]CsvTabellieren(string[]csvZeilen){_csvZeilen=csvZeilen;

RecordsErstellen();int[]breiten=SpaltenbreitenErmitteln();TabelleErstellen(breiten);

return_tabelle;}

publicstring[]CsvTabellieren(string[]csvZeilen){varrecords=RecordsErstellen(csvZeilen);int[]breiten=SpaltenbreitenErmitteln(records);vartabelle=TabelleErstellen(breiten,records);

returntabelle;}

Automatisierte Tests ergänzen

Herausforderungen beim Ergänzen von Tests:• Abhängigkeiten • Aspekte nicht getrennt • Ressourcenzugriffe • Sichtbarkeit • Globaler Zustand

Lösungen:• Zunächst vermehrt Integrationstests,

dann Refactoring und Unit Tests ergänzen. • Mock Frameworks wie

TypeMock Isolator oder Telerik JustMock einsetzen.

Wenige Integrationstests

Viele Unittests

Viele Integrationstests

Wenige Unittests

usingSystem.Collections.Generic;usingSystem.IO;

namespaceressourcen{publicclassCsvReader{publicIEnumerable<string[]>Read(stringfilename){varlines=File.ReadAllLines(filename);foreach(varlineinlines){varfields=line.Split(';');yieldreturnfields;}}}}

Integrierter Ressourcenzugriff

[Test,Isolated]publicvoidIsolated_logic_test(){varsut=newCsvReader();Isolate.WhenCalled(()=>File.ReadAllLines("")).WillReturn(new[]{"a;b;c","1;2;3"});

varrecords=sut.Read("egal.csv").ToArray();

Assert.That(records[0],Is.EqualTo(new[]{"a","b","c"}));Assert.That(records[1],Is.EqualTo(new[]{"1","2","3"}));}

Verhalten der Attrappe beschreiben

publicclassPrivatesZeugs{privatevoidSaveToDatabase(stringdata){Console.WriteLine("Ressourcenzugriff..."+data);thrownewException();}

publicvoidDoSomething(){Console.WriteLine("DoSomething...");SaveToDatabase("datendatendaten...");}}

[TestFixture]publicclassPrivateMethoden{[Test]publicvoidPrivate_Methode_nicht_ausführen(){varsut=newPrivatesZeugs();Isolate.NonPublic.WhenCalled(sut,"SaveToDatabase").IgnoreCall();

sut.DoSomething();}}

Verhalten der Attrappe beschreiben

Komplexe RefactoringsMikado Methode

Change

Change

Change

Change

Change

Change

Change

Change

Change

Hm… ist das wirklich clever?

Change

Change

Cha

Change

Cha

Change

B

Cha

Revert!

Change

B

Cha

Change

B

Cha

Change

B

D

Cha

C

Revert!

Change

B

D

Cha

C

Change

B

D

Cha

C

Change

B

D

Cha

C

Change

B

D

Cha

C

E

Revert!

Change

B

D

Cha

C

E

Change

B

D

Cha

C

E

Change

B

D

Cha

C

E

Change

B

D

F

Cha

C

E

Revert!

Change

B

D

F

Cha

C

E

Change

B

D

F

Cha

C

E

Change

B

D

F

Cha

C

E

Change

B

D

F

A

Cha

C

E

Revert!

Change

B

D

F

A

Cha

C

E

Change

Change

Change

Change

Change

Change

Change

Change

Change

Change

Change

Change

✓Change

✓Change

Hurra!

B

D

F

A

Change

C

E

B

D

F

A

Change

C

E

Ana l

y s e

Re f a c t o r i ng

NachLösungensuchen

DasMikadoZielnotieren

DasZielbzw.dieaktuelleVorbedingungnaivimplementieren

GibteseinProblem?

IstdasMikadoZielerreicht?

Nein CommitderÄnderungenindieVersionskontrolle

Ja

ErgibtdieÄnderungSinn?

Ja

Fertig!

Nein

Nein

NächsteVorbedingungauswählen,umdamitzuarbeiten

DieLösungenalsVorbedingungenimMikadoGraphnotieren

VerwerfenallerÄnderungenmitHilfederVersionskontrolle

Start

Ja

• Änderung naiv ausführen.

•Herausfinden, was kaputt gegangen ist.Dies sind die Vorbedingungen für die Änderung.

•Visualisieren: zum Mikado Graph hinzufügen

• REVERT !!!

•Wiederholen, bis nichts mehr kaputt geht.

Demo

Uhrzeit wird fortlaufend angezeigt

Uhrzeit wird fortlaufend angezeigt

Eigene Timerklasseerstellen

Uhrzeit wird fortlaufend angezeigt

Eigene Timerklasse integrieren

Eigene Timerklasseerstellen

Uhrzeit wird fortlaufend angezeigt

Anzeigen der Uhrzeit auf eigenem Event

Eigene Timerklasse integrieren

Eigene Timerklasseerstellen

Uhrzeit wird fortlaufend angezeigt

Anzeigen von Uhrzeit und REstzeit trennen

Anzeigen der Uhrzeit auf eigenem Event

Eigene Timerklasse integrieren

Eigene Timerklasseerstellen

Ziel

UnterzielUnterziel

Unterziel

Unterziel

Unterziel

Unterziel

Unterziel

Analyse

Refactoring

Vorteile der Mikado Methode

• Immer auslieferbar • Nur auf dem Trunk, kein Branching • Fokus auf das Ziel; nur das Nötigste tun • Visualisierung

Fazit

• Einfache Refactorings: Lesbarkeit • Automatisierte Tests: Korrektheit • Komplexe Refactorings: Wandelbarkeit

Onlinekurs „Automatisiertes Testen von .NET Anwendungen“

• zur Zeit kostenlos • http://stefanlieser.teachable.com

Fragen?

http://refactoring-legacy-code.net

http://linkedin.com/in/stefanlieser

https://twitter.com/StefanLieser

http://xing.com/profile/stefan_lieser