Większość programistów słyszała o zasadzie open - closed - jednej z zasad SOLID wujka Boba. Brzmi to rozsądnie, ale może być nieco niewyraźne do czasu pierwszego użycia na "żywym" kodzie. Pełna treść zasady brzmi: jednostki oprogramowania (klasy, moduły, funkcje itp.) powinny być otwarte na rozszerzenia, ale zamknięte na modyfikacje.
Co to tak naprawdę oznacza?
Natknęliśmy się na problem programistyczny, który pokazał nam, na czym tak naprawdę polega zasada open-closed. W jednej z naszych aplikacji webowych mieliśmy formularz z dwiema sekcjami (między innymi):
- kanały popytu
- filtry dynamiczne
Użytkownicy mogą dodawać dowolną liczbę filtrów, ale istnieją pewne zasady - dostępność filtrów zależy od wybranych kanałów.
Kanały popytu: ADWYMIANA, NAGŁÓWEKBIDDING, RESERVATION, OTHER Dynamiczne filtry (wymiary): strona internetowa, ogłoszenieunit, geo, creativerozmiar, urządzenie
Ten artykuł dotyczy głównie refaktoryzacji kodu, więc poniżej będzie dużo fragmentów kodu. Starałem się je zredukować, ale pewna ilość kodu jest niezbędna do pokazania refaktoryzacja kodu. Nie musisz rozumieć każdej małej części kodu, aby zrozumieć główną ideę.
Pierwsza implementacja problemu była prosta:
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) {
// czy którykolwiek z disablingDemandChannels jest zaznaczony?
}
}
ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'];
class ResearchDynamicFilter {
_setDynamicFilterDisableWebsitesEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => {
// wyłącz filtry stron internetowych
});
}
}
Jak widać, filtr witryny powinien być niedostępny dla HEADERKanały BIDDING, RESERVATION i OTHER są więc dostępne tylko dla ADKanał EXCHANGE.
Ostatnią rzeczą, jaką można powiedzieć o kodzie, jest to, że jest stały lub statyczny. Mamy więc więcej żądań od naszych klientów, dzięki czemu te klasy są większe i bardziej złożone.
Rozwój funkcji
Uwaga spoiler -> gdy komponent jest otwarty na zmiany, w przyszłości będzie wiele zmian nazw. Nie będziemy zwracać na to uwagi w następnych krokach.
Dodaj kolejny filtr dla "Produktu (Produkt Schemat dostępności filtrów jest taki sam jak na stronie internetowej)
- ResearchDynamicFilter klasa musi sprawdzać jeszcze jeden wymiar podczas wyłączania/włączania pól
Przejdźmy na większy poziom i dodajmy przełącznik nad kanałami -> "Źródło". Wszystkie kanały popytu, które mieliśmy do tej pory, znajdują się w źródle Ad Managera. Nowe źródło - SSP - nie ma kanałów popytu, a jedynym dostępnym filtrem jest strona internetowa.
Zasady:
- Istnieją dwa stany źródła: Ad Manager, SSP.
- Wszystkie nasze kanały popytu są dostępne tylko dla źródła Ad Manager.
- Nie ma kanałów popytu dla źródła SSP
- "Strona internetowa" jest jedynym filtrem dostępnym dla źródła SSP.
Wdrożenie:
Dodaj kolejny filtr dla "Platformy
Zasady:
- Platforma jest dostępna tylko wtedy, gdy źródłem jest SSP
Trudność:
- Teraz mamy "Witrynę", która jest dostępna dla kanału AD_EXCHANGE z Menedżera reklam i dla Ssp oraz mamy "Platformę", która jest dostępna dla Ssp, ale nie dla Menedżera reklam.
- Przełączanie stan formularza może być naprawdę skomplikowany i mylący
Wdrożenie z nową funkcjonalnością:
Przedstawiam następny snippet głównie w celu pokazania złożoności kodu. Zapraszam do pozostawienia go ukrytego.
class ResearchFormStateUpdater {
update () {
(...)
this._triggerCallbacks();
}
_triggerCallbacks () {
// wybierz wywołania zwrotne w zależności od źródła
}
_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) {
// czy którykolwiek z disablingDemandChannels jest zaznaczony?
}
}
ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'];
class ResearchDynamicFilter {
// Nie uprościłem tych dwóch metod, aby pokazać obecną złożoność implementacji.
_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) => {
// przełączanie stanu filtra w zależności od "shouldDisable
});
}
}
ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];
Nadal używamy niektórych 'toggle' mechanizm. Naprawdę trudno jest przełączyć 4 dźwignie i osiągnąć oczekiwany stan, a teraz DynamicFilter musi wiedzieć, które wymiary nie są dla źródła ssp.
Mamy ResearchFormStateUpdater, dlaczego nie miałby być odpowiedzialny?
Wniosek końcowy
Dodaj kolejny filtr dla "Yield partner
Dokładnie w tym momencie zdecydowaliśmy się na refaktoryzację tych klas. Analizowane kanały i filtry to tylko niewielka część problemu. Jest tu wiele sekcji formularzy i wszystkie mają ten sam problem. Nasz refactor powinien zneutralizować potrzebę zmiany metod wewnątrz tych klas *aby* dodać nowe kanały lub wymiary.
W następnym fragmencie pozostawiłem główne klasy prawie tak, jak są w naszym kodzie produkcyjnym, aby pokazać, jak łatwo je teraz zrozumieć.
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) {
// wyłącz filtryToDisable
}
_enableFilters (filtersToDisable) {
const filtersToEnable = $(ResearchDynamicFilter.ALL_FILTERS).not(filtersToDisable).get();
// włącz filtersToEnable
}
}
ResearchDynamicFilter.ALL_FILTERS = ['website', 'ad_unit', 'creative_size', 'geo', 'device', 'product', 'platform'];
Udało nam się! Naprawdę?
Teraz jedyną rzeczą, którą "ResearchDynamicFilter" musi znać, jest lista wszystkich filtrów - wydaje się sprawiedliwe. Reszta logiki i kontroli pochodzi z góry - niektóre wyższe metody i stałe.
Wypróbujmy więc naszą nową strukturę, dodając filtr dla "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'];
Jak widać, wszystko polega na dodaniu pewnych wartości do stałych i kilku dodatkowych warunków.
Dzięki zasadzie "open-closed" jesteśmy w stanie zmienić logikę biznesową formularza, dodając jedynie pewne wartości i warunki na wyższym poziomie abstrakcji. Nie musimy wchodzić do komponentu i zmieniać czegokolwiek. Ten refaktoring wpłynął na cały formularz i było więcej sekcji, a wszystkie są teraz zgodne z zasadą open-closed.
Nie zmniejszyliśmy ilości kodu - w rzeczywistości nawet ją zwiększyliśmy (przed/po):
- ResearchFormStateUpdater - 211/282 linie
- ResearchDynamicFilter - 267/256 linii
Chodzi o kolekcję w stałych -> to teraz nasz publiczny interfejs, nasza konsola do kontrolowania procesu bez dziesiątek przełączników.
Czytaj także: