De fleste udviklere har hørt om åben-lukket-princippet - et af onkel Bobs SOLID-principper. Det lyder fornuftigt, men det kan stadig være lidt sløret, indtil det første gang bruges på "levende" kode. Princippet er: Softwareenheder (klasser, moduler, funktioner osv.) skal være åbne for udvidelser, men lukkede for ændringer.
Så hvad betyder det egentlig?
Vi er stødt på et udviklingsproblem, som har vist os, hvad et åbent-lukket princip egentlig handler om. I en af vores webapplikationer havde vi en formular med to sektioner (blandt andre):
- efterspørgselskanaler
- dynamiske filtre
Brugerne kan tilføje så mange filtre, som de ønsker, men der er nogle regler - tilgængeligheden af filtre afhænger af de valgte kanaler.
Efterspørgselskanaler: ADUDVEKSLING, HEADERBIDDING, RESERVATION, ANDET Dynamiske filtre(dimensioner): hjemmeside, annonceenhed, geo, kreativstørrelse, enhed
Denne artikel handler mest om refaktorering af kode, så der vil være mange kodestykker nedenfor. Jeg har forsøgt at reducere det, men en vis mængde kode er nødvendig for at vise refaktorering af kode. Du behøver ikke at forstå hver eneste lille del af koden for at forstå hovedidéen.
Den første implementering af problemet var enkel:
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) {
// er nogen af disablingDemandChannels markeret?
}
}
ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'];
klasse ResearchDynamicFilter {
_setDynamicFilterDisableWebsitesEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => {
// deaktiverer webstedsfiltre
});
}
}
Som du kan se, er det meningen, at webstedsfilteret ikke skal være tilgængeligt for HEADERBIDDING, RESERVATION og ANDRE kanaler, så den er kun tilgængelig for ADUdvekslingskanal.
Det sidste, man kan sige om kode, er, at den er permanent eller statisk. Så vi får flere anmodninger fra vores kunder, hvilket gør disse klasser større og mere komplekse.
Udvikling af funktioner
Spoiler alert -> når en komponent er åben for ændringer, vil der være mange navneændringer i fremtiden. Vi vil ikke være opmærksomme på dette i de næste trin.
Tilføj et nyt filter for 'Produkt' (Produkt filtertilgængelighedsordningen er den samme som på hjemmesiden)
- ForskningDynamiskFilter Klassen skal tjekke for endnu en dimension, når den deaktiverer/aktiverer felter
Lad os gøre det større og tilføje en omskifter over kanaler -> 'Kilde'. Alle de efterspørgselskanaler, vi har haft indtil nu, er i Ad Manager-kilden. Den nye kilde - SSP - har ingen efterspørgselskanaler, og det eneste tilgængelige filter er hjemmeside.
Regler:
- Der er to tilstande for kilden: Ad Manager, SSP.
- Alle vores efterspørgselskanaler er kun tilgængelige for Ad Manager-kilder.
- Der er ingen efterspørgselskanaler for SSP-kilder
- 'Website' er det eneste tilgængelige filter for SSP-kilder.
Implementering:
Tilføj et nyt filter til 'Platform'
Regler:
- Platformen er kun tilgængelig, når kilden er SSP
Sværhedsgrad:
- Nu har vi 'Website', som er tilgængelig for AD_EXCHANGE-kanalen fra Ad Manager og for Ssp, og vi har 'Platform', som er tilgængelig for Ssp, men ikke for Ad Manager.
- Toggling Formularens tilstand kan blive virkelig vanskelig og forvirrende
Implementering med ny funktionalitet:
Jeg præsenterer det næste uddrag primært for at vise kodens kompleksitet. Du er velkommen til at lade det være skjult.
klasse ResearchFormStateUpdater {
update () {
(...)
this._triggerCallbacks();
}
_triggerCallbacks () {
// vælg tilbagekald afhængigt af kilden
}
_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) {
// hvis nogen af disablingDemandChannels er markeret
}
}
ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'];
klasse ResearchDynamicFilter {
// Jeg har ikke forenklet de to metoder for at vise den nuværende implementeringskompleksitet
_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 === 'platform') {
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) => {
// skifter filtertilstand afhængigt af 'shouldDisable'
});
}
}
ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];
Vi bruger stadig nogle 'toggle' mekanisme. Det er virkelig svært at skifte 4 håndtag og komme til den forventede tilstand, og nu skal DynamicFilter vide, hvilke dimensioner der ikke er til ssp-kilde.
Vi har ResearchFormStateUpdater, så hvorfor skulle den ikke have ansvaret?
Endelig anmodning
Tilføj et nyt filter til 'Yield partner'
Det var præcis på det tidspunkt, vi besluttede at refaktorere disse klasser. Kanaler og filtre, der analyseres, er kun en lille del af problemet. Der er flere formularsektioner her, og de har alle det samme problem. Vores refaktorering burde neutralisere behovet for at ændre metoder inde i disse klasser *for at* tilføje nogle nye kanaler eller dimensioner.
I det næste uddrag har jeg ladet hovedklasserne være næsten, som de er i vores produktionskode, for at vise dig, hvor lette de er at forstå nu.
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 = ['platform'];
ResearchFormStateUpdater.ONLY_ADX_FILTERS = ['website', 'product'];
klasse ResearchDynamicFilter {
_setDynamicFiltersToggleEvent () {
$(this._getBody()).on('dynamicFilter:toggleDynamicFilters', (event, disabledFilters) => {
this._disableFilters(disabledFilters.split(','));
this._enableFilters(disabledFilters.split(',')));
});
}
_disableFilters (filtersToDisable) {
// deaktiverer filtreToDisable
}
_enableFilters (filtersToDisable) {
const filtersToEnable = $(ResearchDynamicFilter.ALL_FILTERS).not(filtersToDisable).get();
// aktiver filtreToEnable
}
}
ResearchDynamicFilter.ALL_FILTERS = ['website', 'ad_unit', 'creative_size', 'geo', 'device', 'product', 'platform'];
Vi gjorde det! Gjorde vi det?
Nu er det eneste, 'ResearchDynamicFilter' behøver at vide, en liste over alle filtre - det virker rimeligt. Resten af logikken og kontrollen kommer ovenfra - nogle højere metoder og konstanter.
Så lad os afprøve vores nye struktur ved at tilføje et filter for 'Yield_partner':
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'];
Som du kan se, handler det om at tilføje nogle værdier til konstanter og nogle ekstra betingelser.
Takket være "åben-lukket-princippet" kan vi ændre formularens forretningslogik ved blot at tilføje nogle værdier og betingelser på et højere abstraktionsniveau. Vi behøver ikke at gå ind i komponenten og ændre noget. Denne refaktorering påvirkede hele formularen, og der var flere sektioner, og de overholder alle princippet om åben-lukkethed nu.
Vi reducerede ikke mængden af kode - faktisk øgede vi den endda (før/efter):
- ResearchFormStateUpdater - 211/282 linjer
- ForskningDynamiskFilter - 267/256 linjer
Det hele handler om samlingen i konstanter -> det er vores offentlige grænseflade nu, vores konsol til at styre processen uden snesevis af omskiftere.
Læs også her: