07.07.2020

Branch by Abstraction

Branch by Abstraction ist ein Large Scale Refactoring, das sich zur Durchführung von größeren, sich über mehrere Sprints erstreckende Aufräumarbeiten eignet. Dem Muster kommt in Wartungs- und Sanierungs-Projekten eine besondere Bedeutung zu, da sich damit größere Codeteile sicher und ohne Zeitdruck ablösen lassen. Neu entwickelter Code wird permanent integriert und ausgeliefert. Clientcode wird sukzessive und in kleinen Schritten auf die Verwendung des neuen Codes umgestellt.

Funktionsweise

Übergeordnetes Ziel von Branch by Abstraction ist die Ablösung eines isoliert betrachtbaren Moduls unter Beibehaltung der von diesem Modul exportierten API.

Ausgangssituation

Die Ausgangssituation ist dadurch gekennzeichnet, dass ein oder mehrere Clients das abzulösende (fehlerhafte) Modul direkt nutzen.

Schritt 1: Abstraktions-Layer einführen

Im ersten Schritt wird dem abzulösenden Modul ein Abstraktions-Layer vorangestellt.

Der Abstraktions-Layer implementiert dieselbe API wie das abzulösende Modul und delegiert sämtliche Funktionsaufrufe eins-zu-eins an das Modul weiter.

Schritt 2: Clientcode umstellen

Im zweiten Schritt wird sämtlicher das abzulösende Modul nutzender Clientcode auf die Verwendung des Abstraktions-Layer umgestellt. Es wird sichergestellt, dass das abzulösende Modul nicht mehr direkt genutzt wird.

Schritt 3: Abstraktions-Layer kopieren und neu implementieren

Im dritten Schritt wird der Abstraktionslayer kopiert und von einem neuen Modul implementiert. In Interface-basierten Programmiersprachen wie Java oder Go handelt es sich dabei nicht um eine Kopie im eigentlichen Sinne, sondern um eine weitere Implementierung desselben Interface.

Sämtlicher Clientcode nutzt weiterhin das abzulösende Modul über den ursprünglichen Abstraktions-Layer. Dieses Vorgehen entkoppelt die Neuentwicklung vom alten Code und erlaubt die kontinuierliche Integration des neu entwickelten Codes.

Schritt 4: Clientcode umstellen

Die Umstellung des Clientcodes auf die Neuimplementierung erfolgt iterativ. Sobald eine Funktion der Neuimplementierung fertig ist, kann der die Funktion nutzende Clientcode umgestellt werden.

Mit Fertigstellung und Umstellung sämtlichen Clientcodes wird das Refactoring mit dem Löschen des fehlerhaften Moduls abgeschlossen.

Beispiel JobDog

JobDog ist ein Recruiting-Portal für qualifiziertes Fachpersonal. JobDog wurde als monolithische Web-Anwendung programmiert und enthält unter anderem das Modul jobcenter. Dieses Modul fasst eine Reihe von Funktionen zusammen, die Jobs in verschiedene Job-Börsen posten. Das folgende Go-Snippet zeigt die Kernfunktionen des Moduls:

package jobcenter

func Add(feed Feed)
func Publish(job Job)
func Cancel(job Job)
func Remove(feed Feed)

Die Funktion Add fügt dem Center einen neuen Job-Feed, z.B. zu Monster.de hinzu. Die Funktion Publish pusht den übergebenen Job an alle registrierten Feeds.

Das Modul ist langsam, fehleranfällig und nur noch schwer wartbar. Außerdem ist das Modul wenig robust und führt häufig zu Datenverlusten.

Das Modul soll basierend auf einem Queueing-Mechanismus neu implementiert werden. Die Kernidee des Queueings ist, dass Publish die zu veröffentlichenden Jobs nicht mehr direkt an die registrierten Feeds überträgt, sondern die Jobs nur noch in eine Queue stellt. Ein parallel laufender Thread ist für die Abarbeitung der Queue und Übertragung der Jobs zuständig und übernimmt die Fehlerbehandlung.

Schritt 1: Abstraktions-Layer einführen

Der Abstraktions-Layer wird als Interface Publisher realisiert. Das Interface definiert für jede der vier jobcenter-Funktionen eine Methode:

type Publisher interface {
    Add(feed Feed)
    Publish(job Job)
    Cancel(job Job)
    Remove(feed Feed)
}

Anschließend wird das Interface vom neuen Typ PublisherV1 implementiert, indem sämtliche Methodenaufrufe eins-zu-eins an die alten jobcenter-Funktionen delegiert werden:

type PublisherV1 struct {
}

func (p PublisherV1) Publish(job domain.Job) {
    jobcenter.Publish(job)
}

Schritt 2: Clientcode umstellen

Nach Fertigstellung des Abstraktions-Layers wird sämtlicher das jobcenter-Modul nutzender Clientcode auf die Verwendung des neuen Interface Publisher umgestellt, das mit PublisherV1 instanziiert wird. Das Beispiel zeigt die Umstellung eines Clients, der die Funktionen Add und Publish nutzt:

func main() {
    var jc jobcenter.Publisher = jobcenter.PublisherV1{}
    jc.Add(domain.Feed{URL: "https://monster.de"})
    jc.Publish(domain.Job{ID: "1"})
    ...
}

Schritt 3: Neuimplementierung

Der Typ PublisherV2 kapselt die Neuimplementierung des jobcenter-Moduls. Der Typ implementiert das selbe Interface wie PublisherV1, beginnend mit der Methode Publish, die den zu veröffentlichenden Job in eine Queue stellt:

var queue []domain.Job

type PublisherV2 struct {
}

func (p PublisherV2) Publish(job domain.Job) {
    queue = append(queue, job)
}

Der neue Publisher wird testgetrieben entwickelt. Sobald eine Funktion umgestellt ist, werden alle Clients, die diese Funktion verwenden, auf die Neuimplementierung umgestellt. Damit das in Go funktioniert, muß PublisherV2 alle Methoden des Interface Publisher implementieren. Sofern die Methoden noch nicht verwendet werden, kann deren Rumpf zunächst leer gelassen werden.

Neben der Methode Publish muß ein Worker implementiert werden, der die Queue kontinuierlich auf neue Jobs prüft und an die registrierten Feeds überträgt.

Schritt 4: Clients auf Neuimplementierung umstellen

Die Umstellung des Clientcodes beginnt mit der Methode Publish. Dabei ist zu beachten, dass Clientcode bis zum Abschluss des Refactorings mit zwei Publishern arbeiten muss: Dem alten für die noch nicht umgestellten Funktionen, und dem neuen für alle erneuerten Funktionen.

func main() {
    var jc1 jobcenter.Publisher = jobcenter.PublisherV1{}
    var jc2 jobcenter.Publisher = jobcenter.PublisherV2{}

    jc1.Add(domain.Feed{URL: "https://monster.de"})
    jc2.Publish(domain.Job{ID: "1"})
    ...
}

Bei der Umstellung gilt es außerdem zu beachten, dass die alten jobcenter-Funktionen mit den neuen Funktionen zusammenspielen müssen. Wird, wie im Beispiel besprochen, die Funktion Publish als erstes umgestellt, dann stehen dem neuen Publisher bzw. dem dazugehörigen Queue-Worker noch keine Feeds zur Verfügung, an die die zu veröffentlichenden Jobs gesendet werden sollen. Entsprechend muss der Worker die über den alten Publisher registrierten Feeds für die Verteilung der Jobs nutzen.

Referenzen

Zurück