La maggior parte degli sviluppatori ha sentito parlare del principio aperto - chiuso, uno dei principi SOLID dello zio Bob. Sembra ragionevole, ma può essere ancora un po' confuso fino al primo utilizzo su codice "vivo". Lo stato completo del principio è: le entità software (classi, moduli, funzioni, ecc.) dovrebbero essere aperte per l'estensione, ma chiuse per la modifica.
Che cosa significa in realtà?
Ci siamo imbattuti in un problema di sviluppo che ci ha mostrato il vero significato del principio "open-closed". In una delle nostre applicazioni web avevamo un modulo con due sezioni (tra le altre):
- canali di domanda
- filtri dinamici
Gli utenti possono aggiungere tutti i filtri che desiderano, ma ci sono alcune regole: la disponibilità dei filtri dipende dai canali scelti.
Canali di domanda: ADSCAMBIO, INTESTAZIONEOFFERTA, PRENOTAZIONE, ALTRO Filtri dinamici (dimensioni): sito web, annunciounità, geo, creativodimensione, dispositivo
Questo articolo riguarda principalmente la rifattorizzazione del codice, quindi ci saranno molti snippet di codice qui sotto. Ho cercato di ridurli, ma una certa quantità di codice è necessaria per mostrare rifattorizzazione del codice. Non è necessario comprendere ogni piccola parte del codice per capire l'idea principale.
La prima implementazione del problema era semplice:
classe 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) {
// è stato controllato uno dei canali di disabilitazione della domanda?
}
}
ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'];
class ResearchDynamicFilter {
_setDynamicFilterDisableWebsitesEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => {
// disabilita i filtri dei siti web
});
}
}
Come si può vedere, si suppone che il filtro del sito web non sia disponibile per HEADEROFFERTE, PRENOTAZIONI e ALTRI canali, quindi è disponibile solo per ADCanale di scambio.
L'ultima cosa che si può dire del codice è che è permanente o statico. Quindi abbiamo più richieste da parte dei nostri clienti, che rendono queste classi più grandi e più complesse.
Sviluppo di funzionalità
Attenzione: quando un componente è aperto alle modifiche, in futuro ci saranno molti cambi di nome. Non presteremo attenzione a questo nei prossimi passi.
Aggiungere un altro filtro per 'Prodotto' (Prodotto Lo schema di disponibilità dei filtri è lo stesso del sito web)
- RicercaFiltro Dinamico La classe deve verificare la presenza di un'ulteriore dimensione durante la disattivazione/abilitazione dei campi
Andiamo più in grande e aggiungiamo uno switcher sopra i canali -> 'Sorgente'. Tutti i canali di richiesta che avevamo finora sono nella fonte Ad Manager. La nuova fonte - SSP - non ha canali di richiesta e l'unico filtro disponibile è quello del sito web.
Regole:
- Esistono due stati di origine: Ad Manager, SSP.
- Tutti i nostri canali di richiesta sono disponibili solo per la fonte Ad Manager.
- Non esistono canali di domanda per la fonte SSP
- 'Sito web' è l'unico filtro disponibile per la sorgente SSP.
Implementazione:
Aggiungere un altro filtro per "Piattaforma".
Regole:
- La piattaforma è disponibile solo quando la sorgente è SSP.
Difficoltà:
- Ora abbiamo 'Sito web', che è disponibile per il canale AD_EXCHANGE di Ad Manager e per Ssp e abbiamo 'Piattaforma' che è disponibile per Ssp ma non per Ad Manager.
- Alterazione lo stato del modulo può diventare davvero complicato e confuso
Implementazione con nuove funzionalità:
Vi presento il prossimo snippet principalmente per mostrare la complessità del codice. Sentitevi liberi di lasciarlo nascosto.
classe ResearchFormStateUpdater {
update () {
(...)
this._triggerCallbacks();
}
_triggerCallbacks () {
// sceglie i callback a seconda della sorgente
}
_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) {
// se uno qualsiasi di disablingDemandChannels è controllato
}
}
ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'];
class ResearchDynamicFilter {
// Non ho semplificato questi due metodi per mostrare l'attuale complessità dell'implementazione.
_setDefaultDynamicFiltersToggleEvent () {
$(this._getBody()).on('dynamicFilter:enableSspFilters', (event, shouldEnableSspOptions) => {
this._setDefaultFiltersOptionDisabledState(shouldEnableSspOptions);
const selectedFilterDimension = this._getFiltersDimension().find('option:selected').val();
se (selectedFilterDimension === 'sito web') {
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) => {
// altera lo stato del filtro in base a "shouldDisable".
});
}
}
ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];
Utilizziamo ancora alcuni 'toggle' (alza la levetta) meccanismo. È davvero difficile cambiare 4 leve e raggiungere lo stato previsto e ora DynamicFilter deve sapere quali dimensioni non sono per la sorgente ssp.
Abbiamo ResearchFormStateUpdater, perché non dovrebbe essere responsabile?
Richiesta finale
Aggiungi un altro filtro per "Partner di rendimento".
Questo è il momento esatto in cui abbiamo deciso di rifattorizzare queste classi. I canali e i filtri analizzati sono solo una piccola parte del problema. Ci sono diverse sezioni di form qui e tutte hanno lo stesso problema. Il nostro refactor dovrebbe neutralizzare la necessità di modificare i metodi interni di queste classi *per* aggiungere nuovi canali o dimensioni.
Nel prossimo snippet, ho lasciato le classi principali quasi come sono nel nostro codice di produzione, per mostrare quanto siano facili da capire ora.
classe 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 = ['sito web', 'prodotto'];
class ResearchDynamicFilter {
_setDynamicFiltersToggleEvent () {
$(this._getBody()).on('dynamicFilter:toggleDynamicFilters', (event, disabledFilters) => {
this._disableFilters(disabledFilters.split(','));
this._enableFilters(disabledFilters.split(','));
});
}
_disableFilters (filtersToDisable) {
// disabilita filtriDaDisabilitare
}
_enableFilters (filtersToDisable) {
const filtersToEnable = $(ResearchDynamicFilter.ALL_FILTERS).not(filtersToDisable).get();
// abilita filtersToEnable
}
}
ResearchDynamicFilter.ALL_FILTERS = ['website', 'ad_unit', 'creative_size', 'geo', 'device', 'product', 'platform'];
Ce l'abbiamo fatta! Ce l'abbiamo fatta?
Ora l'unica cosa che 'ResearchDynamicFilter' deve sapere è l'elenco di tutti i filtri: mi sembra giusto. Il resto della logica e del controllo viene dall'alto: alcuni metodi e costanti superiori.
Proviamo quindi la nostra nuova struttura aggiungendo un filtro per "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'];
Come si può vedere, si tratta di aggiungere alcuni valori alle costanti e alcune condizioni aggiuntive.
Grazie al principio "aperto-chiuso", siamo in grado di modificare la logica di business del modulo aggiungendo solo alcuni valori e condizioni a un livello di astrazione superiore. Non abbiamo bisogno di entrare nel componente e modificare nulla. Questa rifattorizzazione ha interessato l'intero form e ci sono state altre sezioni che ora rispettano tutte il principio di apertura-chiusura.
Non abbiamo ridotto la quantità di codice, anzi l'abbiamo addirittura aumentata (prima/dopo):
- Aggiornatore dello stato dei moduli di ricerca - 211/282 linee
- RicercaFiltro Dinamico - 267/256 linee
Si tratta di una collezione di costanti -> è la nostra interfaccia pubblica ora, la nostra console per controllare il processo senza decine di interruttori.
Leggi anche: