A maioria dos programadores já ouviu falar do princípio aberto - fechado - um dos princípios SOLID do Tio Bob. Parece razoável, mas ainda pode ser um pouco confuso até à primeira utilização em código "vivo". O estado completo do princípio é: as entidades de software (classes, módulos, funções, etc.) devem ser abertas para extensão, mas fechadas para modificação.
O que é que isso significa realmente?
Deparámo-nos com um problema de desenvolvimento que mostrou nós o que é, de facto, um princípio aberto-fechado. Numa das nossas aplicações Web, tínhamos um formulário com duas secções (entre outras):
- canais de procura
- filtros dinâmicos
Os utilizadores podem adicionar tantos filtros quantos desejarem, mas existem algumas regras - a disponibilidade dos filtros depende dos canais escolhidos.
Canais de procura: ADTROCA, CABEÇALHOLICITAÇÃO, RESERVA, OUTROS Filtros dinâmicos (dimensões): sítio Web, anúnciounidade, geo, criativotamanho, dispositivo
Este artigo é maioritariamente sobre refactorização de código, pelo que haverá muitos excertos de código abaixo. Tentei reduzi-los, mas é necessário algum código para mostrar refacção de código. Não é necessário compreender todas as pequenas partes do código para compreender a ideia principal.
A primeira implementação do problema era simples:
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) {
// algum dos disablingDemandChannels está selecionado?
}
}
ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'];
classe ResearchDynamicFilter {
_setDynamicFilterDisableWebsitesEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => {
// desativar filtros de sítios Web
});
}
}
Como se pode ver, o filtro do sítio Web não deve estar disponível para HEADEROs canais LICITAÇÃO, RESERVA e OUTROS estão disponíveis apenas para os canais ADCanal EXCHANGE.
A última coisa que se pode dizer sobre o código é que ele é permanente ou estático. Assim, temos mais pedidos do nosso cliente, tornando estas classes maiores e mais complexas.
Desenvolvimento de funcionalidades
Alerta de spoiler -> quando um componente está aberto a alterações, haverá muitas mudanças de nome no futuro. Não vamos prestar atenção a isto nos próximos passos.
Adicionar outro filtro para 'Produto' (Produto o esquema de disponibilidade do filtro é o mesmo do sítio Web)
- ResearchDynamicFilter a classe tem de verificar se existe mais uma dimensão ao desativar/desativar campos
Vamos aumentar e adicionar um comutador acima dos canais -> 'Source'. Todos os canais de procura que tínhamos até agora estão na fonte do Ad Manager. A nova fonte - SSP - não tem canais de procura e o único filtro disponível é o website.
Regras:
- Existem dois estados de origem: Ad Manager, SSP.
- Todos os nossos canais de procura estão disponíveis apenas para a fonte Ad Manager.
- Não existem canais de procura para a fonte SSP
- 'Website' é o único filtro disponível para a fonte SSP.
Implementação:
Adicionar outro filtro para "Plataforma
Regras:
- A plataforma só está disponível quando a fonte é a SSP
Dificuldade:
- Agora temos "Website", que está disponível para o canal AD_EXCHANGE do Ad Manager e para o Ssp e temos "Platform" que está disponível para o Ssp mas não para o Ad Manager
- Comutação o estado do formulário pode ser muito complicado e confuso
Implementação com novas funcionalidades:
Apresento-lhe o próximo excerto principalmente para mostrar a complexidade do código. Pode deixá-lo à vontade para o ocultar.
classe ResearchFormStateUpdater {
update () {
(...)
this._triggerCallbacks();
}
_triggerCallbacks () {
// escolher callbacks dependendo da fonte
}
_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 algum dos disablingDemandChannels for verificado
}
}
ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'];
class ResearchDynamicFilter {
// Não simplifiquei o corpo destes dois métodos para mostrar a complexidade atual da implementação
_setDefaultDynamicFiltersToggleEvent () {
$(this._getBody()).on('dynamicFilter:enableSspFilters', (event, shouldEnableSspOptions) => {
this._setDefaultFiltersOptionDisabledState(shouldEnableSspOptions);
const selectedFilterDimension = this._getFiltersDimension().find('option:selected').val();
Se (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();
se ($.inArray(selectedFilterDimension, ['website', 'product']) >= 0) {
this._toggleChosenFilterDisabledState(shouldDisableWebsitesAndProducts);
}
this._setMethodSelectWebsiteAndProductOptionDisabledState(shouldDisableWebsitesAndProducts);
});
}
_toggleNonSspFilters (dimensionSelect, shouldDisable) {
$.each(ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS, (_, option) => {
// alternar o estado do filtro em função de 'shouldDisable'
});
}
}
ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];
Ainda usamos alguns 'alternar' mecanismo. É realmente difícil mudar 4 alavancas e chegar ao estado esperado e agora o DynamicFilter tem de saber quais as dimensões que não são para a fonte ssp.
Temos o ResearchFormStateUpdater, porque não há-de ser ele o responsável?
Pedido final
Adicionar outro filtro para "Parceiro de rendimento
Foi nesse exato momento que decidimos refactorizar essas classes. Os canais e filtros que estão a ser analisados são apenas uma pequena parte do problema. Existem várias secções de formulários aqui e todas elas têm o mesmo problema. A nossa refacção deve neutralizar a necessidade de alterar os métodos internos dessas classes *para* adicionar alguns novos canais ou dimensões.
No snippet seguinte, deixei as classes principais quase como estão no nosso código de produção para mostrar como são fáceis de entender agora.
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;
se (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'];
classe ResearchDynamicFilter {
_setDynamicFiltersToggleEvent () {
$(this._getBody()).on('dynamicFilter:toggleDynamicFilters', (event, disabledFilters) => {
this._disableFilters(disabledFilters.split(','));
this._enableFilters(disabledFilters.split(',')));
});
}
_disableFilters (filtersToDisable) {
// desativar filtersToDisable
}
_enableFilters (filtersToDisable) {
const filtersToEnable = $(ResearchDynamicFilter.ALL_FILTERS).not(filtersToDisable).get();
// ativar filtersToEnable
}
}
ResearchDynamicFilter.ALL_FILTERS = ['website', 'ad_unit', 'creative_size', 'geo', 'device', 'product', 'platform'];
Conseguimos! Conseguimos?
Agora, a única coisa que o 'ResearchDynamicFilter' tem de saber é uma lista de todos os filtros - parece-me justo. O resto da lógica e do controlo vem de cima - alguns métodos e constantes superiores.
Vamos então experimentar a nossa nova estrutura adicionando um filtro para "Yield_partner":
classe ResearchFormStateUpdater {
_dynamicFiltersDimensionsToBeDisabled () {
(...)
se (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'];
Como pode ver, trata-se apenas de acrescentar alguns valores a constantes e algumas condições adicionais.
Graças ao "princípio aberto-fechado", podemos alterar a lógica comercial do formulário acrescentando apenas alguns valores e condições a um nível de abstração mais elevado. Não precisamos de entrar no componente e alterar nada. Esta refacção afectou todo o formulário e havia mais secções, e todas elas obedecem agora ao princípio aberto-fechado.
Não reduzimos a quantidade de código - de facto, até a aumentámos (antes/depois):
- ResearchFormStateUpdater - 211/282 linhas
- ResearchDynamicFilter - 267/256 linhas
É tudo sobre a coleção de constantes -> é a nossa interface pública agora, a nossa consola para controlar o processo sem dezenas de comutadores.
Leia também: