Die meisten Entwickler haben schon von dem Prinzip "offen - geschlossen" gehört, einem der SOLID-Prinzipien von Onkel Bob. Es hört sich vernünftig an, kann aber bis zur ersten Anwendung im "Live"-Code ein wenig verschwommen sein. Die vollständige Fassung des Prinzips lautet: Software-Entitäten (Klassen, Module, Funktionen usw.) sollten offen für Erweiterungen, aber geschlossen für Änderungen sein.
Was bedeutet das also wirklich?
Wir sind auf ein Entwicklungsproblem gestoßen, das uns gezeigt hat, was es mit dem Prinzip "offen-geschlossen" wirklich auf sich hat. In einer unserer Webanwendungen hatten wir ein Formular mit zwei Abschnitten (neben anderen):
- Nachfragekanäle
- dynamische Filter
Die Benutzer können so viele Filter hinzufügen, wie sie möchten, aber es gibt einige Regeln - die Verfügbarkeit der Filter hängt von den gewählten Kanälen ab.
Kanäle für die Nachfrage: ADAUSTAUSCH, KOPFZEILEBIDDING, RESERVATION, OTHER Dynamische Filter(Dimensionen): Website, Anzeigeeinheit, geo, kreativGröße, Gerät
In diesem Artikel geht es vor allem um Code-Refactor, so wird es eine Menge von Code-Schnipseln unten sein. Ich habe versucht, sie zu reduzieren, aber eine gewisse Menge an Code ist notwendig, um zu zeigen Code-Refactoring. Sie müssen nicht jeden kleinen Teil des Codes verstehen, um die Hauptidee zu verstehen.
Die erste Umsetzung des Problems war einfach:
Klasse ResearchFormStateUpdater {
update () {
(...)
this._updateDynamicFilters();
}
_updateDynamicFilters () {
$('.dynamic-filter').each((_, filter) => {
$(filter).trigger('dynamicFilter:disableWebsites', this._shouldDisableWebsitesFields());
});
}
_shouldDisableWebsitesFields () {
return this._shouldDisableFields(ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS);
}
_shouldDisableFields (disablingDemandChannels) {
// ist eines von disablingDemandChannels angekreuzt?
}
}
ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'];
class ResearchDynamicFilter {
_setDynamicFilterDisableWebsitesEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => {
// Website-Filter deaktivieren
});
}
}
Wie Sie sehen können, sollte der Website-Filter für HEADER nicht verfügbar seinBIDDING, RESERVATION und OTHER Kanäle, so dass sie nur für ADEXCHANGE-Kanal.
Das letzte, was man über Code sagen kann, ist, dass er dauerhaft oder statisch ist. Wir haben also mehr Anfragen von unseren Kunden, die diese Klassen größer und komplexer machen.
Entwicklung von Funktionen
Spoiler-Alarm -> wenn eine Komponente für Änderungen offen ist, wird es in Zukunft viele Namensänderungen geben. Wir werden dies in den nächsten Schritten nicht beachten.
Einen weiteren Filter für 'Produkt' hinzufügen (Produkt Das Schema für die Verfügbarkeit der Filter entspricht dem der Website)
- ResearchDynamicFilter Klasse muss beim Deaktivieren/Aktivieren von Feldern auf eine weitere Dimension prüfen
Gehen wir weiter und fügen wir einen Umschalter über Kanäle -> 'Quelle' hinzu. Alle Nachfragekanäle, die wir bisher hatten, befinden sich in der Quelle Ad Manager. Die neue Quelle - SSP - hat keine Demand Channels und der einzige verfügbare Filter ist Website.
Regeln:
- Es gibt zwei Status der Quelle: Anzeigenmanager und SSP.
- Alle unsere Nachfragekanäle sind nur für die Ad Manager-Quelle verfügbar.
- Es gibt keine Nachfragekanäle für SSP-Quellen
- Website" ist der einzige für die SSP-Quelle verfügbare Filter.
Umsetzung:
Einen weiteren Filter für 'Plattform' hinzufügen
Regeln:
- Die Plattform ist nur verfügbar, wenn die Quelle SSP ist.
Schwierigkeitsgrad:
- Jetzt haben wir "Website", die für den AD_EXCHANGE-Kanal von Ad Manager und für Ssp verfügbar ist, und wir haben "Plattform", die für Ssp, aber nicht für Ad Manager verfügbar ist.
- Umschalten Der Zustand des Formulars kann sehr kompliziert und verwirrend sein.
Implementierung mit neuer Funktionalität:
Ich präsentiere Ihnen das nächste Snippet hauptsächlich, um die Komplexität des Codes zu zeigen. Sie können ihn gerne ausgeblendet lassen.
Klasse ResearchFormStateUpdater {
update () {
(...)
this._triggerCallbacks();
}
_triggerCallbacks () {
// Auswahl der Rückrufe je nach Quelle
}
_adManagerSourceCallbacks () {
(...)
this._enableDemandChannels(ResearchFormStateUpdater.AD_MANAGER_DEMAND_CHANNELS);
this._updateDefaultStateOfDynamicFilters();
this._updateAdManagerDynamicFilters();
}
_sspSourceCallbacks () {
(...)
this._removeDemandChannelsActiveClassAndDisable(ResearchFormStateUpdater.AD_MANAGER_DEMAND_CHANNELS);
this._updateDefaultStateOfDynamicFilters();
}
_updateDefaultStateOfDynamicFilters () {
$('.dynamic-filter').each((_, filter) => {
$(filter).trigger('dynamicFilter:enableSspFilters', this.isSourceSsp);
});
}
_updateAdManagerDynamicFilters () {
$('.dynamic-filter').each((_, filter) => {
$(filter).trigger('dynamicFilter:disableWebsitesAndProducts', this._areFormStateDimensionsDisabled() && !this.isSourceSsp);
});
}
_shouldDisableFields (disablingDemandChannels) {
// ist eines von disablingDemandChannels geprüft
}
}
ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'];
class ResearchDynamicFilter {
// Ich habe diese beiden Methoden nicht vereinfacht, um die aktuelle Komplexität der Implementierung zu zeigen
_setDefaultDynamicFiltersToggleEvent () {
$(this._getBody()).on('dynamicFilter:enableSspFilters', (event, shouldEnableSspOptions) => {
this._setDefaultFiltersOptionDisabledState(shouldEnableSspOptions);
const selectedFilterDimension = this._getFiltersDimension().find('option:selected').val();
if (selectedFilterDimension === 'Website') {
this._toggleChosenFilterDisabledState(false);
} else if (selectedFilterDimension === 'Plattform') {
this._toggleChosenFilterDisabledState(!shouldEnableSspOptions);
} else {
this._toggleChosenFilterDisabledState(shouldEnableSspOptions);
}
});
}
_setDynamicFilterDisableWebsitesAndProductsEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsitesAndProducts', (event, shouldDisableWebsitesAndProducts) => {
const selectedFilterDimension = this._getFiltersDimension().find('option:selected').val();
if ($.inArray(selectedFilterDimension, ['website', 'product']) >= 0) {
this._toggleChosenFilterDisabledState(shouldDisableWebsitesAndProducts);
}
this._setMethodSelectWebsiteAndProductOptionDisabledState(shouldDisableWebsitesAndProducts);
});
}
_toggleNonSspFilters (dimensionSelect, shouldDisable) {
$.each(ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS, (_, option) => {
// Umschalten des Filterstatus in Abhängigkeit von 'shouldDisable'
});
}
}
ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];
Wir verwenden immer noch einige 'Umschalten' Mechanismus. Es ist wirklich schwer, 4 Hebel umzulegen und den erwarteten Zustand zu erreichen, und jetzt muss DynamicFilter wissen, welche Dimensionen nicht für die SPS-Quelle geeignet sind.
Wir haben ResearchFormStateUpdater, warum sollte er nicht dafür zuständig sein?
Letzter Antrag
Einen weiteren Filter hinzufügen für 'Yield partner'.
Das war genau der Moment, in dem wir beschlossen, diese Klassen zu überarbeiten. Die analysierten Kanäle und Filter sind nur ein kleiner Teil des Problems. Es gibt hier mehrere Formularabschnitte und alle haben das gleiche Problem. Unser Refactor sollte die Notwendigkeit neutralisieren, Methoden innerhalb dieser Klassen zu ändern, *um* neue Kanäle oder Dimensionen hinzuzufügen.
Im nächsten Schnipsel habe ich die Hauptklassen fast so belassen, wie sie in unserem Produktionscode sind, um Ihnen zu zeigen, wie einfach sie jetzt zu verstehen sind.
Klasse ResearchFormStateUpdater {
update () {
(...)
this._updateDynamicFilters();
}
_updateDynamicFilters () {
this._toggleAllDynamicFiltersState(this._dynamicFiltersDimensionsToBeDisabled());
}
_dynamicFiltersDimensionsToBeDisabled () {
if (this.isSourceSsp) { return ResearchFormStateUpdater.NO_SSP_FILTERS; }
var disabledFilters = ResearchFormStateUpdater.ONLY_SSP_FILTERS;
if (this.areDemandChannelsExceptAdxSelected) {
disabledFilters = disabledFilters.concat(ResearchFormStateUpdater.ONLY_ADX_FILTERS);
}
return disabledFilters;
}
_toggleAllDynamicFiltersState (disabledFilters) {
$('.dynamic-filter').each((_, filter) => {
this._toggleDynamicFilterState(filter, disabledFilters);
});
}
_toggleDynamicFilterState (dynamicFilter, disabledFilters) {
$(dynamicFilter).trigger('dynamicFilter:toggleDynamicFilters', disabledFilters);
}
}
ResearchFormStateUpdater.NO_SSP_FILTERS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];
ResearchFormStateUpdater.ONLY_SSP_FILTERS = ['Plattform'];
ResearchFormStateUpdater.ONLY_ADX_FILTERS = ['website', 'product'];
class ResearchDynamicFilter {
_setDynamicFiltersToggleEvent () {
$(this._getBody()).on('dynamicFilter:toggleDynamicFilters', (event, disabledFilters) => {
this._disableFilters(disabledFilters.split(','));
this._enableFilters(disabledFilters.split(',')));
});
}
_disableFilters (filtersToDisable) {
// filterToDisable deaktivieren
}
_enableFilters (filtersToDisable) {
const filtersToEnable = $(ResearchDynamicFilter.ALL_FILTERS).not(filtersToDisable).get();
// enable filtersToEnable
}
}
ResearchDynamicFilter.ALL_FILTERS = ['website', 'ad_unit', 'creative_size', 'geo', 'device', 'product', 'platform'];
Wir haben es geschafft! Haben wir?
Das Einzige, was "ResearchDynamicFilter" jetzt wissen muss, ist eine Liste aller Filter - das scheint fair. Der Rest der Logik und Steuerung kommt von oben - einige höhere Methoden und Konstanten.
Probieren wir also unsere neue Struktur aus, indem wir einen Filter für "Yield_partner" hinzufügen:
class ResearchFormStateUpdater {
_dynamicFiltersDimensionsToBeDisabled () {
(...)
if (this.areDemandChannelsExceptEbdaSelected) {
disabledFilters = disabledFilters.concat(ResearchFormStateUpdater.ONLY_EBDA_FILTERS);
}
return disabledFilters;
}
}
ResearchFormStateUpdater.NO_SSP_FILTERS = [(...), 'yield_partner'];
ResearchFormStateUpdater.ONLY_EBDA_FILTERS = [(...), 'yield_partner'];
ResearchDynamicFilter.ALL_FILTERS = [(...), 'yield_partner']; ResearchDynamicFilter.ALL_FILTERS = [(...), 'yield_partner'];
Wie Sie sehen, geht es nur darum, einige Werte zu Konstanten und einige zusätzliche Bedingungen hinzuzufügen.
Dank des Prinzips "offen-geschlossen" können wir die Geschäftslogik des Formulars ändern, indem wir lediglich einige Werte und Bedingungen auf einer höheren Abstraktionsebene hinzufügen. Wir müssen nicht ins Innere der Komponente gehen und irgendetwas ändern. Diese Umstrukturierung betraf das gesamte Formular und es gab weitere Abschnitte, die jetzt alle dem Prinzip der offenen und geschlossenen Struktur folgen.
Wir haben den Umfang des Codes nicht verringert, sondern ihn sogar erhöht (vorher/nachher):
- ResearchFormStateUpdater - 211/282 Zeilen
- ResearchDynamicFilter - 267/256 Zeilen
Alles dreht sich um die Sammlung in Konstanten -> das ist jetzt unsere öffentliche Schnittstelle, unsere Konsole zur Steuerung von Prozessen ohne Dutzende von Schaltern.
Lesen Sie auch: