Enamik arendajaid on kuulnud avatud - suletud põhimõttest - üks onu Bobi SOLIDi põhimõtetest. See kõlab mõistlikult, kuid see võib siiski olla veidi ähmane kuni esimese kasutamiseni "elusas" koodis. Põhimõtte täielik olek on: tarkvara üksused (klassid, moodulid, funktsioonid jne) peaksid olema avatud laiendamiseks, kuid suletud muutmiseks.
Mida see siis tegelikult tähendab?
Me puutusime kokku ühe arendusprobleemiga, mis näitas meile, mida avatud-suletud põhimõte tegelikult tähendab. Ühes meie veebirakenduses oli meil (muu hulgas) kahe sektsiooniga vorm:
- nõudluskanalid
- dünaamilised filtrid
Kasutajad võivad lisada nii palju filtreid kui nad soovivad, kuid on mõned reeglid - filtrite kättesaadavus sõltub valitud kanalitest.
Nõudluskanalid: ADVAHETUS, PÄISPAKKUMINE, RESERVEERIMINE, MUUD Dünaamilised filtrid(mõõtmed): veebileht, kuulutusüksus, geo, loomingulinesuurus, seade
See artikkel käsitleb peamiselt koodi refaktoorimist, nii et allpool on palju koodilõike. Ma püüdsin seda vähendada, kuid mõningane kogus koodi on vajalik, et näidata koodi refaktooring. Te ei pea mõistma iga väikest koodi osa, et saada aru peamisest ideest.
Probleemi esimene rakendamine oli lihtne:
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) {
// kas mõni disablingDemandChannels on kontrollitud ?
}
}
ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'];
class ResearchDynamicFilter {
_setDynamicFilterDisableWebsitesEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => { {
// veebisaitide filtrite keelamine
});
}
}
Nagu näete, peaks veebilehe filter olema HEADERi jaoks kättesaamatu.BIDDING, RESERVATION ja muud kanalid, nii et see on saadaval ainult ADVAHETUSKANAL.
Viimane asi, mida saab öelda koodi kohta, on see, et see on püsiv või staatiline. Seega on meil rohkem kliendipäringuid, mis teevad need klassid suuremaks ja keerulisemaks.
Funktsiooni arendamine
Spoiler hoiatus -> kui komponent on avatud muudatuste jaoks, muutub tulevikus palju nimesid. Järgmistes sammudes me sellele tähelepanu ei pööra.
Lisa veel üks filter 'Toote' jaoks (Toode filtri kättesaadavuse skeem on sama nagu veebisaidil)
- ResearchDynamicFilter klass peab kontrollima veel ühte mõõdet, kui väljad keelatakse/aktiveeritakse.
Läheme suuremaks ja lisame kanalite kohal mõne lüliti -> 'Source'. Kõik nõudluskanalid, mis meil seni olid, on Ad Manager allikas. Uues allikas - SSP - ei ole nõudluskanaleid ja ainus olemasolev filter on veebileht.
Reeglid:
- Allikas on kahes seisundis: SSP.
- Kõik meie nõudluskanalid on saadaval ainult Ad Manageri allikale.
- SSP allikale ei ole nõudluskanaleid
- 'Veebileht' on ainus SSP allika jaoks saadaval olev filter.
Rakendamine:
Lisa veel üks filter 'Platvormi' jaoks
Reeglid:
- Platvorm on saadaval ainult siis, kui allikas on SSP
Raskusaste:
- Nüüd on meil 'Website', mis on saadaval AD_EXCHANGE kanalile Ad Manager ja Ssp ja meil on 'Platform', mis on saadaval Ssp, kuid mitte Ad Manager
- Vaheldumine vormi olek võib muutuda väga keeruliseks ja segaseks.
Uue funktsionaalsusega rakendamine:
Esitan teile järgmise katkendi peamiselt selleks, et näidata koodi keerukust. Jäta see vabalt peidetud.
class ResearchFormStateUpdater {
update () {
(...)
this._triggerCallbacks();
}
_triggerCallbacks () {
// valime callbackid sõltuvalt allikast
}
_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) {
// on mis tahes disablingDemandChannels kontrollitud
}
}
ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'];
class ResearchDynamicFilter {
// Ma ei lihtsustanud neid kahte meetodi keha, et näidata praegust rakendamise keerukust.
_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) => {
// lülitab filtri olekut sõltuvalt 'shouldDisable'.
});
}
}
ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];
Me kasutame endiselt mõningaid 'toggle' mehhanism. On tõesti raske vahetada 4 hooba ja jõuda oodatud olekusse ja nüüd peab DynamicFilter teadma, millised mõõtmed ei ole ssp allikas.
Meil on olemas ResearchFormStateUpdater, miks ei peaks see olema vastutav?
Lõplik taotlus
Lisa veel üks filter 'Yield partner' jaoks
See on täpselt see hetk, mil me otsustasime need klassid ümber kujundada. Analüüsitavad kanalid ja filtrid on vaid väike osa probleemist. Siin on mitu vormilõiku ja kõigil neil on sama probleem. Meie refaktor peaks neutraliseerima vajaduse muuta nende klasside sisemised meetodid *selleks*, et lisada mõned uued kanalid või mõõtmed.
Järgmises katkendis jätsin põhiklassid peaaegu samaks, nagu need on meie tootmiskoodis, et näidata, kui lihtne on neid nüüd mõista.
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) {
// disable filtersToDisable
}
_enableFilters (filtersToDisable) {
const filtersToEnable = $(ResearchDynamicFilter.ALL_FILTERS).not(filtersToDisable).get();
// enable filtersToEnable
}
}
ResearchDynamicFilter.ALL_FILTERS = ['website', 'ad_unit', 'creative_size', 'geo', 'device', 'product', 'platform'];
Me tegime seda! Kas me tegime?
Nüüd on ainus asi, mida 'ResearchDynamicFilter' peab teadma, kõigi filtrite nimekiri - tundub õiglane. Ülejäänud loogika ja kontroll tuleb ülaltpoolt - mõned kõrgemad meetodid ja konstandid.
Proovime siis meie uut struktuuri, lisades filtri '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'];
Nagu näete, on tegemist mõne väärtuse lisamisega konstandidele ja mõnedele lisatingimustele.
Tänu "avatud-suletud põhimõttele" saame muuta vormi äriloogikat, lisades vaid mõned väärtused ja tingimused kõrgemal abstraktsioonitasemel. Meil ei ole vaja minna komponendi sisemusse ja midagi muuta. See refaktor puudutas kogu vormi ja seal oli rohkem sektsioone ning need kõik järgivad nüüd avatud-kinni põhimõtet.
Me ei vähendanud koodi hulka - tegelikult me isegi suurendasime seda (enne/pärast):
- ResearchFormStateUpdater - 211/282 rida
- ResearchDynamicFilter - 267/256 rida
See kõik on umbes kogumise konstandid -> see on meie avalik liides nüüd, meie konsool kontrollida protsessi ilma kümneid lülitid.
Loe ka: