De flesta utvecklare har hört talas om principen öppen - stängd - en av farbror Bobs SOLID-principer. Det låter rimligt, men det kan fortfarande vara lite suddigt fram till den första användningen på "levande" kod. Principen i sin helhet är: programvaruenheter (klasser, moduler, funktioner etc.) ska vara öppna för utbyggnad, men stängda för modifiering.
Så vad betyder det egentligen?
Vi har stött på ett utvecklingsproblem som har visat oss vad principen om öppet och slutet egentligen handlar om. I en av våra webbapplikationer hade vi ett formulär med två sektioner (bland andra):
- efterfrågekanaler
- dynamiska filter
Användare kan lägga till så många filter de vill, men det finns vissa regler - filtertillgänglighet beror på valda kanaler.
Kanaler för efterfrågan: ADUTBYTE, RUBRIKBUDGIVNING, RESERVERING, ÖVRIGT Dynamiska filter(dimensioner): webbplats, annonsenhet, geo, kreativstorlek, enhet
Den här artikeln handlar mest om kodrefaktorering, så det kommer att finnas många kodsnuttar nedan. Jag försökte minska det, men en viss mängd kod är nödvändig för att visa refaktorisering av kod. Du behöver inte förstå varje liten del av koden för att förstå huvudtanken.
Den första implementeringen av problemet var enkel:
klass 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) {
// är någon av disablingDemandChannels markerad ?
}
}
ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'];
klass ResearchDynamicFilter {
_setDynamicFilterDisableWebsitesEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => {
// inaktivera webbplatsfilter
});
}
}
Som du kan se ska webbplatsfiltret inte vara tillgängligt för HEADERBIDDING, RESERVATION och ÖVRIGA kanaler, så den är endast tillgänglig för ADUtbyteskanal.
Det sista man kan säga om kod är att den är permanent eller statisk. Så vi har fler förfrågningar från våra kunder som gör dessa klasser större och mer komplexa.
Utveckling av funktioner
Spoilervarning -> när en komponent är öppen för ändringar kommer det att ske en hel del namnändringar i framtiden. Vi kommer inte att uppmärksamma detta i nästa steg.
Lägg till ett nytt filter för "Product (Produkt filtertillgängligheten är densamma som på webbplatsen)
- ForskningDynamisktFilter klassen måste kontrollera ytterligare en dimension när fält inaktiveras/aktiveras
Låt oss gå större och lägga till några växlare ovanför kanaler -> "Källa". Alla efterfrågekanaler som vi har haft fram till nu finns i Ad Manager-källan. Den nya källan - SSP - har inga efterfrågekanaler och det enda tillgängliga filtret är webbplats.
Regler:
- Det finns två tillstånd för källan: Annonshanterare, SSP.
- Alla våra kanaler för efterfrågan är endast tillgängliga för Ad Manager-källor.
- Det finns inga kanaler för efterfrågan på SSP-källa
- "Website" är det enda tillgängliga filtret för SSP-källan.
Genomförande:
Lägg till ett nytt filter för "Plattform
Regler:
- Plattformen är endast tillgänglig när källan är SSP
Svårighetsgrad:
- Nu har vi "Website", som är tillgänglig för AD_EXCHANGE-kanalen från Ad Manager och för Ssp och vi har "Platform" som är tillgänglig för Ssp men inte för Ad Manager
- Växling formulärets status kan bli riktigt knepig och förvirrande
Implementering med ny funktionalitet:
Jag presenterar nästa kodsnutt främst för att visa kodens komplexitet. Känn dig fri att lämna den dold.
klass ResearchFormStateUpdater {
update () {
(...)
this._triggerCallbacks();
}
_triggerCallbacks () {
// välj återuppringningar beroende på källa
}
_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) {
// om något av disablingDemandChannels är markerat
}
}
ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'];
klass ResearchDynamicFilter {
// Jag förenklade inte dessa två metoder för att visa den nuvarande implementeringens komplexitet
_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) => {
// växla filterstatus beroende på 'shouldDisable'
});
}
}
ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];
Vi använder fortfarande vissa 'växla' mekanism. Det är verkligen svårt att växla 4 spakar och komma till förväntat tillstånd och nu måste DynamicFilter veta vilka dimensioner som inte är för ssp-källa.
Vi har ResearchFormStateUpdater, varför skulle den inte vara ansvarig?
Sista begäran
Lägg till ett nytt filter för 'Yield partner'
Det var precis då vi bestämde oss för att göra om dessa klasser. Kanaler och filter som analyseras är bara en liten del av problemet. Det finns flera formulärsektioner här och alla har samma problem. Vår refaktor bör neutralisera behovet av att ändra metoder inuti dessa klasser * för att * lägga till några nya kanaler eller dimensioner.
I nästa snutt har jag lämnat huvudklasserna nästan som de är i vår produktionskod för att visa hur lättförståeliga de är nu.
klass 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 = ['webbplats', 'produkt'];
klass ResearchDynamicFilter {
_setDynamicFiltersToggleEvent () {
$(this._getBody()).on('dynamicFilter:toggleDynamicFilters', (event, disabledFilters) => {
this._disableFilters(disabledFilters.split(','));
this._enableFilters(disabledFilters.split(',')));
});
}
_disableFilters (filterToDisable) {
// inaktivera filterToDisable
}
_enableFilters (filterToDisable) {
const filtersToEnable = $(ResearchDynamicFilter.ALL_FILTERS).not(filtersToDisable).get();
// aktivera filterToEnable
}
}
ResearchDynamicFilter.ALL_FILTERS = ['website', 'ad_unit', 'creative_size', 'geo', 'device', 'product', 'platform'];
Vi klarade det! Gjorde vi?
Nu är det enda som "ResearchDynamicFilter" behöver veta en lista över alla filter - det verkar rimligt. Resten av logiken och kontrollen kommer från ovan - några högre metoder och konstanter.
Så låt oss prova vår nya struktur genom att lägga till ett filter för "Yield_partner":
klass 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 handlar det om att lägga till några värden till konstanter och några ytterligare villkor.
Tack vare "open-closed-principen" kan vi ändra formulärets affärslogik genom att bara lägga till några värden och villkor på en högre abstraktionsnivå. Vi behöver inte gå in i komponenten och ändra någonting. Denna refaktor påverkade hela formuläret och det fanns fler sektioner och de följer alla principen om öppen slutenhet nu.
Vi minskade inte mängden kod - faktum är att vi till och med ökade den (före/efter):
- ForskningFormStateUpdater - 211/282 linjer
- ForskningDynamisktFilter - 267/256 linjer
Det handlar om samlingen i konstanter -> det är vårt offentliga gränssnitt nu, vår konsol för att styra processen utan tiotals omkopplare.
Läs också: