Selbst wenn Sie meinen Beitrag „Die Bedeutung von JavaScript-Abstraktionen bei der Arbeit mit Remote-Daten“ nicht gelesen haben, sind Sie wahrscheinlich bereits davon überzeugt, dass Wartbarkeit und Skalierbarkeit für Ihr Projekt wichtig sind und der Weg dorthin die Einführung von Abstraktionen ist.
Für die Zwecke dieses Beitrags gehen wir davon aus, dass eine Abstraktion in JavaScript ein Modul ist.
Die anfängliche Implementierung eines Moduls ist nur der Anfang des langen (und hoffentlich dauerhaften) Prozesses seiner Lebensdauer. Ich sehe 3 wichtige Ereignisse im Lebenszyklus eines Moduls:
- Einführung des Moduls. Die anfängliche Implementierung und der Prozess der Wiederverwendung im gesamten Projekt.
- Änderung des Moduls. Anpassung des Moduls im Laufe der Zeit.
- Entfernung des Moduls.
In meinem vorherigen Beitrag lag der Schwerpunkt nur auf dem ersten Punkt. In diesem Artikel geht es mehr um den zweiten Punkt.
Der Umgang mit Änderungen an einem Modul ist ein Problem, das ich häufig sehe. Verglichen mit der Einführung des Moduls ist die Art und Weise, wie Entwickler es warten oder ändern, ebenso oder sogar wichtiger, um das Projekt wartbar und skalierbar zu halten. Ich habe gesehen, wie ein gut geschriebenes und abstrahiertes Modul im Laufe der Zeit durch Änderungen komplett ruiniert wurde. Ich selbst war manchmal derjenige, der diese katastrophalen Änderungen vorgenommen hat!
Wenn ich von katastrophal spreche, meine ich katastrophal aus der Sicht der Wartbarkeit und Skalierbarkeit. Ich verstehe, dass es aus der Perspektive von Fristen und der Freigabe von Features, die funktionieren müssen, nicht immer möglich ist, langsam zu denken und alle potenziellen Auswirkungen Ihrer Änderung zu berücksichtigen.
Die Gründe, warum die Änderungen eines Entwicklers möglicherweise nicht optimal sind, sind unzählig. Ich möchte besonders einen hervorheben:
Die Fähigkeit, Änderungen auf wartungsfreundliche Weise vorzunehmen
Hier ist eine Möglichkeit, wie Sie wie ein Profi mit Änderungen beginnen können.
Beginnen wir mit einem Codebeispiel: einem API-Modul. Ich wähle dies, weil die Kommunikation mit einer externen API eine der ersten grundlegenden Abstraktionen ist, die ich definiere, wenn ich ein Projekt starte. Die Idee ist, die gesamte API-bezogene Konfiguration und die Einstellungen (wie die Basis-URL, die Fehlerbehandlungslogik usw.) in diesem Modul zu speichern.
Führen wir nur eine Einstellung ein: API.url, eine private Methode: API._handleError() und eine öffentliche Methode: API.get()
class API {
constructor() {
this.url = 'http://whatever.api/v1/';
}
/**
* Fetch API's specific way to check
* whether an HTTP response's status code is in the successful range.
*/
_handleError(_res) {
return _res.ok ? _res : Promise.reject(_res.statusText);
}
/**
* Get data abstraction
* @return {Promise}
*/
get(_endpoint) {
return window.fetch(this.url + _endpoint, { method: 'GET' })
.then(this._handleError)
.then( res => res.json())
.catch( error => {
alert('So sad. There was an error.');
throw new Error(error);
});
}
};
In diesem Modul gibt unsere einzige öffentliche Methode, API.get(), ein Promise zurück. An allen Stellen, an denen wir entfernte Daten abrufen müssen, rufen wir anstelle der Fetch-API direkt über window.fetch() unser API-Modul-Abstraktion auf. Zum Beispiel, um Benutzerinformationen abzurufen: API.get('user') oder die aktuelle Wettervorhersage: API.get('weather'). Wichtig bei dieser Implementierung ist, dass die Fetch-API nicht eng mit unserem Code gekoppelt ist.
Nun kommt eine Änderungsanfrage! Unser Tech-Lead bittet uns, zu einer anderen Methode zum Abrufen von Remote-Daten zu wechseln. Wir müssen zu Axios wechseln. Wie können wir diese Herausforderung angehen?
Bevor wir beginnen, die Ansätze zu diskutieren, fassen wir zunächst zusammen, was gleich bleibt und was sich ändert.
- Änderung: In unserer öffentlichen Methode
API.get()- Wir müssen den Aufruf von
window.fetch()durchaxios()ersetzen. Und wir müssen wieder ein Promise zurückgeben, um unsere Implementierung konsistent zu halten. Axios basiert auf Promises. Ausgezeichnet! - Unsere Serverantwort ist JSON. Bei der Fetch-API verketten wir eine
.then( res => res.json())-Anweisung, um unsere Antwortdaten zu parsen. Bei Axios befindet sich die vom Server bereitgestellte Antwort unter der Eigenschaftdata, und wir müssen sie nicht parsen. Daher müssen wir die .then-Anweisung in.then( res => res.data )ändern.
- Wir müssen den Aufruf von
- Änderung: In unserer privaten Methode
API._handleError- Das boolesche Flag
okfehlt in der Objektantwort. Es gibt jedoch die EigenschaftstatusText. Wir können uns daran anhalten. Wenn ihr Wert'OK'ist, dann ist alles in Ordnung.Seitennotiz: Ja,
okgleichtruein der Fetch-API ist nicht dasselbe wie'OK'in Axios’statusText. Aber bleiben wir einfach und überlassen es für den Moment so, um nicht zu weit auszuholen, und führen keine fortgeschrittene Fehlerbehandlung ein.
- Das boolesche Flag
- Keine Änderung:
API.urlbleibt gleich, zusammen mit der ausgefallenen Art, wie wir Fehlercatchen und sie peralertausgeben.
Alles klar! Nun gehen wir die tatsächlichen Ansätze zur Anwendung dieser Änderungen durch.
Ansatz 1: Code löschen. Code schreiben.
class API {
constructor() {
this.url = 'http://whatever.api/v1/'; // says the same
}
_handleError(_res) {
// DELETE: return _res.ok ? _res : Promise.reject(_res.statusText);
return _res.statusText === 'OK' ? _res : Promise.reject(_res.statusText);
}
get(_endpoint) {
// DELETE: return window.fetch(this.url + _endpoint, { method: 'GET' })
return axios.get(this.url + _endpoint)
.then(this._handleError)
// DELETE: .then( res => res.json())
.then( res => res.data)
.catch( error => {
alert('So sad. There was an error.');
throw new Error(error);
});
}
};
Klingt vernünftig genug. Commit. Push. Merge. Fertig.
Es gibt jedoch bestimmte Fälle, warum dies keine gute Idee sein könnte. Stellen Sie sich vor, Folgendes passiert: Nach dem Wechsel zu Axios stellen Sie fest, dass eine Funktion mit XMLHttpRequests (der Axios-Schnittstelle für die Ressourcenabrufmethode) nicht funktioniert, aber mit der neuen Fancy-Browser-API von Fetch zuvor einwandfrei funktionierte. Was machen wir jetzt?
Unser Tech-Lead sagt: Verwenden wir für diesen speziellen Anwendungsfall die alte API-Implementierung und verwenden Axios weiterhin überall sonst. Was tun Sie? Suchen Sie das alte API-Modul in Ihrer Quellcodeverwaltung. Rückgängig machen. Fügen Sie hier und da if-Anweisungen hinzu. Das klingt für mich nicht sehr gut.
Es muss einen einfacheren, besser wartbaren und skalierbareren Weg geben, Änderungen vorzunehmen! Nun, den gibt es.
Ansatz 2: Code refaktorieren. Adapter schreiben!
Es gibt eine eingehende Änderungsanfrage! Lassen Sie uns ganz von vorne beginnen und anstatt den Code zu löschen, verschieben wir die Fetch-spezifische Logik in eine weitere Abstraktion, die als Adapter (oder Wrapper) für alle Fetch-Spezifika dient.
Für diejenigen unter Ihnen, die mit dem Adapter-Pattern (auch Wrapper-Pattern genannt) vertraut sind, ja, genau dahin gehen wir! Schauen Sie sich hier eine ausgezeichnete nerdige Einführung an, wenn Sie an allen Details interessiert sind.
Hier ist der Plan

Schritt 1
Nehmen Sie alle Fetch-spezifischen Zeilen aus dem API-Modul und refaktorieren Sie sie in eine neue Abstraktion: FetchAdapter.
class FetchAdapter {
_handleError(_res) {
return _res.ok ? _res : Promise.reject(_res.statusText);
}
get(_endpoint) {
return window.fetch(_endpoint, { method: 'GET' })
.then(this._handleError)
.then( res => res.json());
}
};
Schritt 2
Refaktorieren Sie das API-Modul, indem Sie die Teile entfernen, die Fetch-spezifisch sind, und alles andere gleich lassen. Fügen Sie FetchAdapter als Abhängigkeit hinzu (in irgendeiner Weise).
class API {
constructor(_adapter = new FetchAdapter()) {
this.adapter = _adapter;
this.url = 'http://whatever.api/v1/';
}
get(_endpoint) {
return this.adapter.get(_endpoint)
.catch( error => {
alert('So sad. There was an error.');
throw new Error(error);
});
}
};
Das ist eine andere Geschichte! Die Architektur ist so geändert, dass Sie verschiedene Mechanismen (Adapter) für den Abruf von Ressourcen handhaben können. Letzter Schritt: Sie haben es erraten! Schreiben Sie einen AxiosAdapter!
const AxiosAdapter = {
_handleError(_res) {
return _res.statusText === 'OK' ? _res : Promise.reject(_res.statusText);
},
get(_endpoint) {
return axios.get(_endpoint)
.then(this._handleError)
.then( res => res.data);
}
};
Und im API-Modul wechseln Sie den Standard-adapter zum Axios-Adapter.
class API {
constructor(_adapter = new /*FetchAdapter()*/ AxiosAdapter()) {
this.adapter = _adapter;
/* ... */
}
/* ... */
};
Großartig! Was machen wir, wenn wir für diesen speziellen Anwendungsfall die alte API-Implementierung verwenden und Axios weiterhin überall sonst verwenden müssen? Kein Problem!
// Import your modules however you like, just an example.
import API from './API';
import FetchAdapter from './FetchAdapter';
// Uses the AxiosAdapter (the default one)
const API = new API();
API.get('user');
// Uses the FetchAdapter
const legacyAPI = new API(new FetchAdapter());
legacyAPI.get('user');
Bewerten Sie also nächstes Mal, wenn Sie Änderungen an Ihrem Projekt vornehmen müssen, welcher Ansatz sinnvoller ist:
- Code löschen. Code schreiben.
- Code refaktorieren. Adapter schreiben.
Beurteilen Sie sorgfältig anhand Ihres spezifischen Anwendungsfalls. Zu viele Adapter in Ihrer Codebasis und die Einführung zu vieler Abstraktionen können zu einer erhöhten Komplexität führen, was ebenfalls nicht gut ist.
Viel Spaß beim Adapter-Erstellen!