De fleste utviklere har hørt om åpen-lukket-prinsippet - et av onkel Bobs SOLID-prinsipper. Det høres fornuftig ut, men det kan likevel være litt uklart før det brukes for første gang på "live" kode. Prinsippets fulle tilstand er: programvareenheter (klasser, moduler, funksjoner osv.) skal være åpne for utvidelse, men lukket for endring.
Så hva betyr det egentlig?
Vi kom over et utviklingsproblem som har vist oss hva et åpent-lukket prinsipp egentlig handler om. I en av webapplikasjonene våre hadde vi blant annet et skjema med to seksjoner:
- etterspørselskanaler
- dynamiske filtre
Brukerne kan legge til så mange filtre de ønsker, men det finnes noen regler - filtertilgjengeligheten avhenger av hvilke kanaler som er valgt.
Etterspørselskanaler: ADUTVEKSLING, OVERSKRIFTBUD, RESERVASJON, ANNET Dynamiske filtre(dimensjoner): nettsted, annonseenhet, geo, kreativstørrelse, enhet
Denne artikkelen handler for det meste om refaktorering av kode, så det vil være mange kodesnutter nedenfor. Jeg har forsøkt å redusere det, men en viss mengde kode er nødvendig for å vise refaktorisering av kode. Du trenger ikke å forstå hver eneste lille del av koden for å få med deg hovedideen.
Den første implementeringen av problemet var enkel:
class 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 noen av disablingDemandChannels merket av?
}
}
ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'];
class ResearchDynamicFilter {
_setDynamicFilterDisableWebsitesEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => {
// deaktiver nettstedfiltre
});
}
}
Som du kan se, skal nettstedsfilteret ikke være tilgjengelig for HEADERBIDDING, RESERVASJON og ANDRE kanaler, så den er kun tilgjengelig for ADUtvekslingskanal.
Det siste man kan si om kode, er at den er permanent eller statisk. Så vi har flere forespørsler fra kundene våre, noe som gjør disse klassene større og mer komplekse.
Utvikling av funksjoner
Spoilervarsel -> når en komponent er åpen for endringer, vil det bli mange navneendringer i fremtiden. Vi vil ikke ta hensyn til dette i de neste trinnene.
Legg til et nytt filter for 'Produkt' (Produkt filtertilgjengelighetsordningen er den samme som på nettstedet)
- ResearchDynamicFilter klassen må sjekke for en dimensjon til når du deaktiverer/aktiverer felt
La oss gå større og legge til noen brytere over kanaler -> 'Kilde'. Alle etterspørselskanaler vi hadde til nå, er i Ad Manager-kilden. Den nye kilden - SSP - har ingen etterspørselskanaler, og det eneste tilgjengelige filteret er nettstedet.
Regler:
- Det finnes to kildestatuser: Ad Manager og SSP.
- Alle våre etterspørselskanaler er kun tilgjengelige for Ad Manager-kilder.
- Det finnes ingen etterspørselskanaler for SSP-kilder
- "Nettsted" er det eneste filteret som er tilgjengelig for SSP-kilder.
Implementering:
Legg til et nytt filter for "Plattform
Regler:
- Plattformen er bare tilgjengelig når kilden er SSP
Vanskelighetsgrad:
- Nå har vi "Website", som er tilgjengelig for AD_EXCHANGE-kanalen fra Ad Manager og for Ssp, og vi har "Platform" som er tilgjengelig for Ssp, men ikke for Ad Manager
- Veksling skjemaets tilstand kan bli veldig vanskelig og forvirrende
Implementering med ny funksjonalitet:
Jeg presenterer neste utdrag hovedsakelig for å vise kodekompleksiteten. La det gjerne være skjult.
class ResearchFormStateUpdater {
update () {
(...)
this._triggerCallbacks();
}
_triggerCallbacks () {
// velg tilbakekallinger avhengig av kilde
}
_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 noen av disablingDemandChannels er merket av
}
}
ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'];
class ResearchDynamicFilter {
// Jeg har ikke forenklet disse to metodene for å vise kompleksiteten i den nåværende implementasjonen
_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) => {
// bytt filterstatus avhengig av 'shouldDisable'
});
}
}
ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];
Vi bruker fortsatt noen 'toggle' mekanisme. Det er veldig vanskelig å bytte 4 spaker og komme til forventet tilstand, og nå må DynamicFilter vite hvilke dimensjoner som ikke er for ssp-kilde.
Vi har ResearchFormStateUpdater, så hvorfor skulle ikke den ha ansvaret?
Endelig forespørsel
Legg til et nytt filter for 'Yield partner'
Det var akkurat da vi bestemte oss for å refaktorere disse klassene. Kanaler og filtre som analyseres, er bare en liten del av problemet. Det er flere skjemadeler her, og alle har det samme problemet. Vår refaktorering bør nøytralisere behovet for å endre metoder i disse klassene *for å* legge til noen nye kanaler eller dimensjoner.
I neste utdrag har jeg latt hovedklassene være nesten slik de er i produksjonskoden vår for å vise hvor enkle de er å forstå nå.
class 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'];
class ResearchDynamicFilter {
_setDynamicFiltersToggleEvent () {
$(this._getBody()).on('dynamicFilter:toggleDynamicFilters', (event, disabledFilters) => {
this._disableFilters(disabledFilters.split(','));
this._enableFilters(disabledFilters.split(','))));
});
}
_disableFilters (filtersToDisable) {
// deaktivere filtreToDisable
}
_enableFilters (filtersToDisable) {
const filtersToEnable = $(ResearchDynamicFilter.ALL_FILTERS).not(filtersToDisable).get();
// aktiver filterToEnable
}
}
ResearchDynamicFilter.ALL_FILTERS = ['website', 'ad_unit', 'creative_size', 'geo', 'device', 'product', 'platform'];
Vi klarte det! Gjorde vi det?
Nå er det eneste 'ResearchDynamicFilter' trenger å vite, en liste over alle filtrene - det virker rimelig. Resten av logikken og kontrollen kommer ovenfra - noen høyere metoder og konstanter.
La oss prøve ut den nye strukturen ved å legge til 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 ser, handler det om å legge til noen verdier i konstanter og noen tilleggsbetingelser.
Takket være "åpen-lukket-prinsippet" kan vi endre forretningslogikken i skjemaet ved bare å legge til noen verdier og betingelser på et høyere abstraksjonsnivå. Vi trenger ikke å gå inn i komponenten og endre noe som helst. Denne refaktoriseringen påvirket hele skjemaet, og det var flere seksjoner, og alle følger nå prinsippet om åpen-lukket.
Vi reduserte ikke mengden kode - faktisk økte vi den til og med (før/etter):
- ResearchFormStateUpdater - 211/282 linjer
- ResearchDynamicFilter - 267/256 linjer
Alt handler om samlingen i konstanter -> det er vårt offentlige grensesnitt nå, vår konsoll for å kontrollere prosessen uten titalls brytere.
Les også: