Většina vývojářů už slyšela o principu otevřeného a uzavřeného přístupu - jednom z principů SOLID strýčka Boba. Zní to rozumně, ale až do prvního použití v "živém" kódu to může být trochu nejasné. Úplný stav principu zní: softwarové entity (třídy, moduly, funkce atd.) by měly být otevřené pro rozšíření, ale uzavřené pro modifikaci.
Co to vlastně znamená?
Narazili jsme na vývojový problém, který se projevil nás o čem princip otevřeného a uzavřeného systému skutečně je. V jedné z našich webových aplikací jsme měli formulář se dvěma sekcemi (mimo jiné):
- poptávkové kanály
- dynamické filtry
Uživatelé mohou přidávat libovolný počet filtrů, ale existují určitá pravidla - dostupnost filtrů závisí na zvolených kanálech.
Poptávkové kanály: ADVÝMĚNA, ZÁHLAVÍNABÍDKA, REZERVACE, OSTATNÍ Dynamické filtry(rozměry): webové stránky, reklamajednotka, geo, kreativnívelikost, zařízení
Tento článek je převážně o refaktorizaci kódu, takže níže bude uvedeno mnoho úryvků kódu. Snažil jsem se je zredukovat, ale určité množství kódu je nutné ukázat. refaktoring kódu. Nemusíte rozumět každé malé části kódu, abyste pochopili hlavní myšlenku.
První implementace problému byla jednoduchá:
třída 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 (vypnutíPoptávkovýchKanálů) {
// je některý z disablingDemandChannels zaškrtnutý ?
}
}
ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'];
třída ResearchDynamicFilter {
_setDynamicFilterDisableWebsitesEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => {
// vypnout filtry webových stránek
});
}
}
Jak vidíte, filtr webových stránek by měl být nedostupný pro HEADER.Nabídky, rezervace a ostatní kanály, takže je k dispozici pouze pro ADKanál EXCHANGE.
Poslední věc, kterou můžete o kódu říci, je, že je stálý nebo statický. Proto máme od klienta další požadavky, díky kterým jsou tyto třídy větší a složitější.
Vývoj funkcí
Pozor, spoiler -> když je komponenta otevřená pro změny, bude se v budoucnu hodně měnit její název. V dalších krocích tomu nebudeme věnovat pozornost.
Přidání dalšího filtru pro položku "Produkt (Produkt schéma dostupnosti filtrů je stejné jako u webových stránek)
- ResearchDynamicFilter třída musí při vypínání/povolování polí zkontrolovat ještě jeden rozměr.
Zvětšeme a přidáme nějaký přepínač nad kanály -> "Zdroj". Všechny kanály poptávky, které jsme dosud měli, jsou ve zdroji Správce reklam. Nový zdroj - SSP - nemá žádné poptávkové kanály a jediný dostupný filtr je webová stránka.
Pravidla:
- Existují dva stavy zdroje: Ad Manager, SSP.
- Všechny naše poptávkové kanály jsou k dispozici pouze pro zdroj Ad Manager.
- Neexistují žádné poptávkové kanály pro zdroj SSP
- "Webová stránka" je jediný filtr dostupný pro zdroj SSP.
Provádění:
Přidání dalšího filtru pro 'Platform'
Pravidla:
- Platforma je k dispozici pouze v případě, že zdrojem je SSP.
Obtížnost:
- Nyní máme "Web", který je k dispozici pro kanál AD_EXCHANGE ze Správce reklam a pro Ssp, a máme "Platform", který je k dispozici pro Ssp, ale ne pro Správce reklam.
- Přepínání stav formuláře může být opravdu složitý a matoucí.
Implementace s novými funkcemi:
Další ukázku vám předkládám hlavně proto, abych ukázal složitost kódu. Klidně ho nechte skrytý.
třída ResearchFormStateUpdater {
update () {
(...)
this._triggerCallbacks();
}
_triggerCallbacks () {
// zvolte zpětná volání v závislosti na zdroji
}
_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) {
// je zaškrtnuto některé z disablingDemandChannels
}
}
ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'];
třída ResearchDynamicFilter {
// Tělo těchto dvou metod jsem nezjednodušil, abych ukázal současnou složitost implementace
_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) => {
// přepnutí stavu filtru v závislosti na 'shouldDisable'
});
}
}
ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];
Stále používáme některé 'toggle' mechanismus. Je opravdu těžké přepnout 4 páky a dostat se do očekávaného stavu a nyní musí DynamicFilter vědět, které rozměry nejsou pro zdroj ssp.
Máme ResearchFormStateUpdater, proč by to neměl mít na starosti?
Závěrečná žádost
Přidat další filtr pro "partner pro výnosy
To je přesně ten okamžik, kdy jsme se rozhodli tyto třídy refaktorovat. Analyzované kanály a filtry jsou jen malou částí problému. Je zde více částí formuláře a všechny mají stejný problém. Náš refaktor by měl neutralizovat potřebu měnit metody uvnitř těchto tříd *pro* přidání některých nových kanálů nebo dimenzí.
V dalším úryvku jsem ponechal hlavní třídy téměř tak, jak jsou v našem produkčním kódu, abych vám ukázal, jak jsou nyní snadno pochopitelné.
třída 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'];
třída 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'];
Dokázali jsme to! Opravdu?
Nyní musí 'ResearchDynamicFilter' znát pouze seznam všech filtrů - zdá se to být spravedlivé. Zbytek logiky a řízení pochází shora - některé vyšší metody a konstanty.
Vyzkoušejme tedy naši novou strukturu přidáním filtru pro "Yield_partner":
třída 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"];
Jak vidíte, jde o přidání některých hodnot ke konstantám a některých dalších podmínek.
Díky principu "otevřeno-uzavřeno" můžeme měnit obchodní logiku formuláře pouze přidáním některých hodnot a podmínek na vyšší úrovni abstrakce. Nemusíme vstupovat dovnitř komponenty a nic měnit. Tento refaktor se dotkl celého formuláře, přibylo více sekcí a všechny se nyní řídí principem open-closed.
Množství kódu jsme nesnížili - ve skutečnosti jsme ho dokonce zvýšili (před/po):
- ResearchFormStateUpdater - 211/282 řádků
- ResearchDynamicFilter - 267/256 řádků
Jde o kolekci v konstantách -> nyní je to naše veřejné rozhraní, naše konzola pro ovládání procesu bez desítek přepínačů.
Přečtěte si také: