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:

  1. Prüfen, ob der übergebene Key im Dictionary existiert.
  2. Prüfen, ob der übergebene Value für den angegebenen Key den korrekten Typen hat.
  3. 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 im Dictionary 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.