Refactoring C# Legacy Code - entwicklertag.de · Das Problem besteht aus drei Teilen: •...
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
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
Einfache RefactoringsRename Extract Method Introduce VariableIntroduce Parameter etc.
Vollständig werkzeuggestützt durchführbar
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;}
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.
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
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.
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
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
http://refactoring-legacy-code.net
http://linkedin.com/in/stefanlieser
https://twitter.com/StefanLieser
http://xing.com/profile/stefan_lieser