Wer Code kopiert wird erschossen.
Während des Informatik Studiums gab es einen Professor aus dem Fachbereich Softwareentwicklung der mich nachhaltig geprägt hat und von dem diese scherzhafte Aussage stammt. Er ermahnte die Studiengruppe regelmäßig bei der Realisierung von Programmieranforderungen, dass jeder der Code kopiert erschossen wird (natürlich immer mit einen zwinkern).
Meinen Kommilitonen und mir wurden neben allem Spaß dieser Aussage aber auch schnell der wichtige Hintergrund dieser Aussage klar.
Jeder Softwareentwickler sollte der bei der Realisierung seiner Aufgaben diese Aussage im Hinterkopf behalten und sich jedes Mal daran erinnert, wenn er dabei ist bestehenden Code (Methoden, Klassen, usw.) zu kopieren, um diesen für einen – in der Regel leicht abweichenden Zweck – zu kopieren und anzupassen.
Konsequenzen durch das Kopieren von Quellcode:
- Es wird mehr Quellcode (LoC) erzeugt als notwendig ist und jedem Entwickler sollte klar sein, dass im Bereich LoC weniger oft mehr ist.
- Test-Cases (wenn man diese denn hoffentlich vorher geschrieben hat) müssen dadurch in größerem Maße als notwendig angepasst und erweitert werden.
- Änderungen (Bug-Fix, Refactoring, Clean-Up, usw.) am Code müssen manuell in allen kopierten Bereichen nachgezogen werden, was teilweise auch schon bei kleinen Systemen mit mehreren Entwicklern schwierig und Fehleranfällig ist.
Zusammengefasst wird durch das Kopieren von Quellcode das System unnötig aufgebläht und unübersichtlicher, was die Wartbarkeit erschwert und in großen gewachsenen System sogar häufig dazu führt, dass sich eine Angst davor entwickelt bestehende Strukturen anzufassen. Ein Teufelskreis.
Ich bin der festen Überzeugung und bislang konnte mich auch noch niemand vom Gegenteil überzeugen, dass eine bestehende Implementierung immer so refaktorisiert werden kann, dass bei notwendigen Erweiterungen ein Kopieren von bestehendem Code und damit die Schaffung von Redundanzen und Fehlerquellen vermieden werden kann.
Hier ein kleines – ich gebe zu etwas konstruiertes – Beispiel:
Idee ist, dass eine Klasse ein Dictionary
hat, welches mit einer Menge an vordefinierten Key-Values initialisiert ist, wobei jedem Key
ein Default Value
zugewiesen ist, der den Typen des Value
festlegt.
Es soll dann Assign
Methoden geben, die einem Key
einen entsprechend korrekt typisierten Value
zuweisen. Dafür werden den Assign
Methoden der gewünschte Key
sowie der typisierte Value
übergeben (Polymorphy). Die Assign
Methoden sollen dann wie folgt arbeiten:
- Prüfen, ob der übergebene
Key
imDictionary
existiert. - Prüfen, ob der übergebene
Value
für den angegebenenKey
den korrekten Typen hat. - Prüfen ob weitere, auf den konkreten Typen basierte Konsistenzbedingungen erfüllt sind (je nach
Assign
Methode).
Beispiel:
using System; using System.Collections.Generic; namespace Example { public class Example { private Dictionary<int, object> _values; public Example() { InitDictionary(); } private void InitDictionary() { _values = new Dictionary<int, object>(); _values.Add(1, Double.NaN); _values.Add(2, DateTime.MinValue); _values.Add(3, String.Empty); // [...] } public void Assign(int key, string value) { //Check if Key exists if (!_values.ContainsKey(key)) { throw new Exception("Key not defined."); } //Check if value has the correct type if (value.GetType() != _values[key].GetType()) { throw new Exception("Value assigned to key has the wrong type"); } //Consistency Check if(value.Length > 50) { throw new Exception("String assigned to key is too long (50 characters max)"); } _values[key] = value; } public void Assign(int key, DateTime value) { //TODO } public void Assign(int key, double value) { //TODO } } }
Schlecht:
Wenn man es sich nun einfach macht, so kann man einfach die Implementierung der bereits umgesetzten Assign
Methode kopieren und lediglich an den gewünschten Stellen anpassen.
public void Assign(int key, string value) { //Check if Key exists if (!_values.ContainsKey(key)) { throw new Exception("Key not defined."); } //Check if value has the correct type if (value.GetType() != _values[key].GetType()) { throw new Exception("Value assigned to key has the wrong type"); } //Consistency Check if(value.Length > 50) { throw new Exception("String assigned to key is too long (50 characters max)"); } _values[key] = value; } public void Assign(int key, DateTime value) { //Check if Key exists if (!_values.ContainsKey(key)) { throw new Exception("Key not defined."); } //Check if value has the correct type if (value.GetType() != _values[key].GetType()) { throw new Exception("Value assigned to key has the wrong type"); } //Consistency Check if (DateTime.Compare(DateTime.MinValue, value) >= 0) { throw new Exception("DateTime assigned to key must be later than " + DateTime.MinValue.ToString("d")); } _values[key] = value; } public void Assign(int key, double value) { //Check if Key exists if (!_values.ContainsKey(key)) { throw new Exception("Key not defined."); } //Check if value has the correct type if (value.GetType() != _values[key].GetType()) { throw new Exception("Value assigned to key has the wrong type"); } //Consistency Check if (value <= 0) { throw new Exception("Double assigned to key must be larger than 0"); } _values[key] = value; }
Wenn nun im Nachhinein erweiterte Anforderungen kommen, wie:
- Wenn ein
Key
nicht existiert, so soll dieser automatisch imDictionary
ergänzt werden. - Wenn ein
Key
bereits einen Nicht-Default-Value
zugewiesen bekommen hat, so darf dieser nicht überschrieben werden. Also keine Mehrfachzuordnung.
dann müssen diverse Stellen im Quellcode angepasst werden, um diese Anforderungen auch in allen Assign
Methoden anzupassen!
Besser:
public void Assign(int key, string value) { AssignValidationHelper(key, value,() => { //Consistency Check if (value.Length > 50) { throw new Exception("String assigned to key is too long (50 characters max)"); } }); } public void Assign(int key, DateTime value) { AssignValidationHelper(key, value, () => { //Consistency Check if (DateTime.Compare(DateTime.MinValue, value) >= 0) { throw new Exception("DateTime assigned to key must be later than " + DateTime.MinValue.ToString("d")); } }); } public void Assign(int key, double value) { AssignValidationHelper(key, value, () => { //Consistency Check if (value <= 0) { throw new Exception("Double assigned to key must be larger than 0"); } }); } private void AssignValidationHelper(int key, object value, Action additionalValidationAction){ //Check if Key exists if (!_values.ContainsKey(key)) { throw new Exception("Key not defined."); } //Check if value has the correct type if (value.GetType() != _values[key].GetType()) { throw new Exception("Value assigned to key has the wrong type"); } additionalValidationAction(); _values[key] = value; }
Durch diese Form des Template Patterns konnte die allgemeine Validierung ausgelagert und wiederverwendet werden. Bei einer Erweiterung muss nun lediglich eine zentrale Stelle angepasst werden. Wenn sich dabei die Signatur ändern sollte, so wir der Entwickler bereits zum Entwicklung-Zeitpunkt darüber informiert, welche Code Bereiche er anpassen muss. Des Weiteren konnten über 20% LoC eingespart werden.
Fazit
Es lohnt sich immer aufs Kopieren von Code zu verzichten und sich dafür Gedanken zu machen, wie man den Code besser macht.
Hinterlasse einen Kommentar