Useimmat kehittäjät ovat kuulleet avoimen ja suljetun periaatteen, joka on yksi Bob-sedän SOLID-periaatteista. Se kuulostaa järkevältä, mutta se voi silti olla hieman epäselvä, kunnes sitä käytetään ensimmäistä kertaa "elävässä" koodissa. Periaatteen täydellinen sisältö on seuraava: ohjelmistokokonaisuuksien (luokat, moduulit, funktiot jne.) pitäisi olla avoimia laajennuksille, mutta suljettuja muutoksille.
Mitä se siis todella tarkoittaa?
Törmäsimme kehitysongelmaan, joka on osoittanut meille, mistä avoin-suljettu-periaatteessa todella on kyse. Eräässä verkkosovelluksessamme oli lomake, jossa oli kaksi osiota (muun muassa):
- kysyntäkanavat
- dynaamiset suodattimet
Käyttäjät voivat lisätä niin monta suodatinta kuin haluavat, mutta on olemassa joitakin sääntöjä - suodattimien saatavuus riippuu valituista kanavista.
Kysyntäkanavat: ADVAIHTO, OTSIKKOTARJOUSPYYNTÖ, VARAUS, MUUT Dynaamiset suodattimet(ulottuvuudet): verkkosivusto, mainosyksikkö, geo, luovakoko, laite
Tämä artikkeli käsittelee pääasiassa koodin uudistamista, joten alla on paljon koodinpätkiä. Yritin vähentää sitä, mutta jonkin verran koodia on tarpeen, jotta voidaan osoittaa, että koodin uudelleenkäsittely. Sinun ei tarvitse ymmärtää jokaista koodin pientä osaa ymmärtääkseen pääajatuksen.
Ongelman ensimmäinen toteutus oli yksinkertainen:
luokka 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) { _shouldDisableFields (disablingDemandChannels) {
// onko jokin disablingDemandChannels-kohdista tarkistettu ?
}
}
ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'];
class ResearchDynamicFilter {
_setDynamicFilterDisableWebsitesEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => { {
// poista verkkosivujen suodattimet käytöstä
});
}
}
Kuten näet, verkkosivujen suodattimen ei pitäisi olla käytettävissä HEADERin osaltaBIDDING-, RESERVATION- ja OTHER-kanavat, joten se on käytettävissä vain AD:lle.Vaihtokanava.
Viimeinen asia, mitä koodista voi sanoa, on se, että se on pysyvää tai staattista. Asiakkaamme esittävät siis yhä enemmän pyyntöjä, mikä tekee näistä luokista suurempia ja monimutkaisempia.
Ominaisuuksien kehittäminen
Spoilerivaroitus -> kun komponentti on avoinna muutoksille, nimiä vaihdetaan tulevaisuudessa paljon. Emme kiinnitä tähän huomiota seuraavissa vaiheissa.
Lisää toinen suodatin sanalle 'Tuote' (Tuote suodattimen saatavuusjärjestelmä on sama kuin verkkosivustolla)
- ResearchDynamicFilter luokan on tarkistettava vielä yksi ulottuvuus, kun kentät poistetaan käytöstä tai otetaan käyttöön.
Mennään isommaksi ja lisätään kanavien yläpuolelle kytkin -> 'Source'. Kaikki tähän asti käyttämämme kysyntäkanavat ovat Ad Manager -lähteessä. Uudessa lähteessä - SSP - ei ole kysyntäkanavia, ja ainoa käytettävissä oleva suodatin on verkkosivusto.
Säännöt:
- Lähteen tiloja on kaksi: Ad Manager, SSP.
- Kaikki kysyntäkanavamme ovat saatavilla vain Ad Manager -lähteelle.
- SSP-lähteelle ei ole kysyntäkanavia
- 'Verkkosivusto' on ainoa käytettävissä oleva suodatin SSP-lähdettä varten.
Toteutus:
Lisää toinen suodatin sanalle 'Platform'
Säännöt:
- Alusta on käytettävissä vain, kun lähde on SSP.
Vaikeus:
- Nyt meillä on 'Verkkosivusto', joka on saatavilla AD_EXCHANGE-kanavalle Ad Managerissa ja Ssp:ssä, ja meillä on 'Alusta', joka on saatavilla Ssp:ssä mutta ei Ad Managerissa.
- Vaihtaminen lomakkeen tila voi olla todella hankala ja sekava.
Uusien toimintojen käyttöönotto:
Esittelen seuraavan pätkän lähinnä osoittaakseni koodin monimutkaisuuden. Voit vapaasti jättää sen piiloon.
luokka ResearchFormStateUpdater {
update () {
(...)
this._triggerCallbacks();
}
_triggerCallbacks () {
// valitse callbackit lähteen mukaan
}
_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) {
// onko jokin disablingDemandChannels tarkistettu.
}
}
ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'];
class ResearchDynamicFilter {
// En yksinkertaistanut näitä kahta metodirunkoa osoittaakseni nykyisen toteutuksen monimutkaisuuden.
_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) => {
// vaihda suodattimen tilaa 'shouldDisable'-arvon mukaan.
});
}
}
ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];
Käytämme edelleen joitakin 'toggle' mekanismi. On todella vaikeaa vaihtaa 4 vipua ja päästä odotettuun tilaan, ja nyt DynamicFilterin on tiedettävä, mitkä mitat eivät ole ssp-lähdettä varten.
Meillä on ResearchFormStateUpdater, miksi se ei voisi olla vastuussa?
Lopullinen pyyntö
Lisää toinen suodatin hakusanalle 'Yield partner'
Juuri sillä hetkellä päätimme muokata näitä luokkia. Analysoitavat kanavat ja suodattimet ovat vain pieni osa ongelmaa. Tässä on useita lomakeosioita, ja kaikissa niissä on sama ongelma. Refaktorointimme pitäisi poistaa tarve muuttaa näiden luokkien sisäisiä metodeja, jotta voidaan *lisätä* uusia kanavia tai ulottuvuuksia.
Seuraavassa pätkässä jätin pääluokat melkein sellaisiksi kuin ne ovat tuotantokoodissamme, jotta näet, kuinka helppotajuisia ne ovat nyt.
luokka 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) { _enableFilters (filtersToDisable) {
const filtersToEnable = $(ResearchDynamicFilter.ALL_FILTERS).not(filtersToDisable).get();
// ota filtersToEnable käyttöön
}
}
ResearchDynamicFilter.ALL_FILTERS = ['website', 'ad_unit', 'creative_size', 'geo', 'device', 'product', 'platform'];
Me teimme sen! Teimmekö?
Nyt 'ResearchDynamicFilter'in on tiedettävä vain luettelo kaikista suodattimista - vaikuttaa reilulta. Loput logiikasta ja valvonnasta tulee ylhäältä - joitakin korkeampia metodeja ja vakioita.
Kokeillaan uutta rakennetta lisäämällä suodatin 'Yield_partner':
luokka 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'];
Kuten näet, kyse on vain arvojen lisäämisestä vakioihin ja muutamista lisäehdoista.
Avoimen ja suljetun periaatteen ansiosta voimme muuttaa lomakkeen liiketoimintalogiikkaa vain lisäämällä joitakin arvoja ja ehtoja korkeammalla abstraktiotasolla. Meidän ei tarvitse mennä komponentin sisälle ja muuttaa mitään. Tämä refaktorointi vaikutti koko lomakkeeseen, ja siinä oli useampia osioita, ja ne kaikki noudattavat nyt avoimen ja suljetun periaatetta.
Emme vähentäneet koodin määrää - itse asiassa jopa lisäsimme sitä (ennen/jälkeen):
- ResearchFormStateUpdater - 211/282 riviä
- ResearchDynamicFilter - 267/256 riviä
Kyse on vain vakioiden kokoelmasta -> se on nyt julkinen käyttöliittymämme, konsolimme, jolla voimme hallita prosessia ilman kymmeniä kytkimiä.
Lue myös: