Fun with Refactoring

“You mean, you can’t change it? It’s called Software!”

Me (to a colleague)

Bei jeder Unterhaltung zwischen Entwicklern fällt zwangsläufig der Begriff Refactoring. Obwohl der Begriff schon seit 20 Jahren durch die Gänge der Entwicklungsabteilungen hallt, haben viele Beteiligten noch keinen Blick in Martin Fowlers Buch “Refactoring. Wie Sie das Design vorhandener Software verbessern” geworfen.

Eines der größten Missverständnisse um den Begriff Refactoring ist die Vorstellung, ihn als einzelnen, zusätzlichen Schritt in der Erstellung von Software zu sehen. Refactoring ist schon immer integraler Bestandteil der Software Entwicklung gewesen. Den Code umstrukturieren um neue Anforderungen in den Code einfügen zu können oder gewachsenen Code vereinfachen, damit die Kollegen begreifen, was an dieser Stelle passiert, ist für umsichtige Entwickler schon immer Herzensangelegenheit gewesen,

Um eine sehr einfache Analogie von der Baustelle zu wählen. Das Reinigen von Werkzeug und Maschinen ist keine Aufgabe, die den Maurern explizit auf den Weg gegeben muss. Wenn am nächsten Morgen an Kelle und Mischmaschine steinharter Speiss in dicken Brocken hängt, werden die nächsten Stunden nicht sehr produktiv.

Viele Software Entwicklern zaudern noch immer, wenn sie in der eigenen Code Basis schreckliche Programmierverbrechen entdecken. Häufig wird ein TODO an dieser Stelle eingefügt, als ob der Kollege gerade einen Pol entdeckt hat und diese Tat für die Nachwelt dokumentiert werden muss.

Leider verhält es sich mit TODOs wie mit den Flaggen an Nord- und Südpol. Sie überdauern die Jahre und sehen hin und wieder enttäuschte Gesichter von Abenteurern, die zu spät gekommen sind.

Für das Zaudern der Entwickler ist hauptsächlich die Angst vor Fehlern verantwortlich. Insbesondere, wenn wenig automatisierte Tests vorhanden sind. Wer sich guten Code wünscht, sollte seinen Mitarbeitern die Angst nehmen und auf ausreichend viele automatisierte Tests pochen.

Das Refactoring soll das Design der Software verbessern, während die Funktionalität sich nicht ändert. Das bedeutet natürlich, dass der Entwickler eine grundsätzliche Idee hat, wohin die Reise gehen soll und das es ein Problem mit dem aktuellen Design gibt. Gerade für Neulinge kein besonders guter Einstieg in das Thema, wenn noch nicht viel Erfahrung gesammelt werden konnte.

Hier hilft das inkrementelle Refactoring, um alle Mitglieder im Team an dem Refactoring teilhaben zu lassen. Die Idee hinter dem inkrementellen Refactoring ist schrittweise Verändern eines Designs. Bei einer agilen Entwicklung macht diese Art der Änderung sowieso den meisten Sinn.

Fällt einem Entwickler ein strukturelles Problem in der Code Basis auf, dann könnte er mit einem größeren Refactoring, das Problem auf einen Schlag beheben. Der Nachteil an dieser Art ist, die Kollegen lernen nicht davon, sie finden viel geänderten Code vor, der Entwickler wird durch diese Arbeiten für eine geraume Zeit von den geplanten Teamtätigkeiten abgelenkt.

Alternativ kann der Entwickler eine Lösung erarbeiten und diese den Kollegen zur Verfügung stellen. Die Lösung wird dann von den Kollegen verwendet, vielleicht für andere Probleme adaptiert oder verbessert.

Schauen wir ein Logistiksystem an, in dem verschiedene Standorte als Entitäten gespeichert sind. In der Businesslogik werden die Standorte aber häufig nur über ihren Namen verwendet und nur bei Bedarf werden die entsprechenden Entitäten aus der Datenbank geladen. Irgendwann hat sich vielleicht noch der Code Smell eingeschlichen, zusätzlich zu den Namen und den Entitäten, die IDs der Entitäten in der Businesslogik zu verwenden.

public final String DEPARTMENT_BIELEFELD = "Bielefeld";

public static final Map<String, Integer> DEPARTMENTS = new HashMap<>();
DEPARTMENTS.put(DEPARTMENT_BIELEFELD, 42);

@Entity public final class DepartmentEntity { ... } 

Hier wäre es also angebracht die unterschiedlichen Varianten der Adressierung eines Standortes zu vereinheitlichen. Da sich die Standorte im Logistiksystem selten ändern und dies immer mit Implementierungsaufwand verbunden ist, schwebt dem Entwickler vor, alle Varianten durch den Einsatz eines Enum zu ersetzen. Als perspektivisches Ziel auch das Ersetzen der Entitäten in der Datenbank durch diese Enum.

public enum Department {
  BIELEFELD("Bielefeld", 42);

  private final String name;
  private final int id;

  Department(String name, int id) {
    this.property = property;
    this.id = id;
  }

  public String getName() {
    return property;
  }

  public int getId() {
    return id;
  }

  public static Optional<Department> by(Integer id) {
    if (id == null) {
      return Optional.empty();
    }
    return Arrays.stream(values()).filter(d -> d.getId() == id).findFirst();
  }

  public static Optional<StatusAction> by(String name) {
    if (name== null) {
      return Optional.empty();
    }
    return Arrays.stream(values()).filter(d -> d.getName().equals(name)).findFirst();
  }
}

Die Umsetzung für Namen und Datenbank IDs ist schnell gemacht und das obige Enum kann nun von allen Kollegen verwendet werden. Während die Lösung nach und nach umgesetzt wird, machen sich zwei Makel bemerkbar. Die Gleichheit der IDs in der Datenbank und in der DEPARTMENTS Map wird nicht geprüft. Dies könnte irgendwann zu Problemen führen und die Entitäten sind noch nicht mit dem Enum integriert.

public enum Department {
  BIELEFELD("Bielefeld");

  private final DepartmentRepository repository;
  private final String name;
  private final int id;

  Department(String name) {
    this.property = property;
  }

  public String getName() {
    return property;
  }

  public int getId() {
    return id;
  }

  public DepartmentEntity getEntity() throws {
    return repository.findById(ticketService, id);
  }

  private void setId(int id) {
    this.id = id;
  }

  private void setRepository(DepartmentRepository repository) {
    this.repository = repository;
  }

  @Component
  public static class Injector {
    private final DepartmentRepository repository;

    public Injector(DepartmentRepository repository) {
      this.repository= repository;
    }

    @PostConstruct
    public void init() {
      Arrays.stream(values()).forEach(
          d -> {
            d.setId(repository.findByname(d.name).getId());
            d.setRepository(repository);
          });
    }
  }
}

Für eine bessere Integration von Entitäten und Enum erhält diese die getEntity Methode. Damit können nun jederzeit passende Department erzeugt werden.

DepartmentEntity entity = Department.BIELEFELD.getEntity();

Damit die Enum die korrekten IDs aus der Datenbank und das DepartmentRepository zugewiesen bekommt, benötigen wir etwas Hilfe von Spring Boot. In einer @PostConstruct Methode suchen wir für jede Enum-Konstante die passende Entität aus der Datenbank. Dann weisen wir der Konstante die korrekte Id und das Repository zu. Da eine Enum nicht als Spring @Component ausgewiesen werden kann, wird eine zusätzliche Static Nested Class dafür benötigt.

Mit dieser Ergänzung erhalten wir eine Enum, die für jede Konstante die korrekte ID der zugeordneten Entität bereitstellt und bei Bedarf diese auch aus der Datenbank laden kann.

Die einzelnen Schritte beim iterativen Refactoring bringen keine großen Änderungen und bieten die Möglichkeit ein System vorsichtig in ein neues Design zu bringen. Die Vorsicht ist dabei nicht der Software geschuldet sondern der Angst der Kollegen.