Algo que aparece bastante no meu trabalho atual é que existe um processo generalizado que precisa acontecer, mas a parte estranha desse processo precisa acontecer de maneira um pouco diferente, dependendo do valor de uma determinada variável, e eu não sou tenho certeza de qual é a maneira mais elegante de lidar com isso.
Usarei o exemplo que geralmente temos, que está fazendo as coisas de maneira ligeiramente diferente, dependendo do país com o qual estamos lidando.
Então, eu tenho uma aula, vamos chamá-lo Processor
:
public class Processor
{
public string Process(string country, string text)
{
text.Capitalise();
text.RemovePunctuation();
text.Replace("é", "e");
var split = text.Split(",");
string.Join("|", split);
}
}
Exceto que apenas algumas dessas ações precisam acontecer para determinados países. Por exemplo, apenas 6 países exigem a etapa de capitalização. O caractere no qual dividir pode mudar dependendo do país. A substituição do acentuado 'e'
pode ser necessária apenas dependendo do país.
Obviamente, você poderia resolvê-lo fazendo algo assim:
public string Process(string country, string text)
{
if (country == "USA" || country == "GBR")
{
text.Capitalise();
}
if (country == "DEU")
{
text.RemovePunctuation();
}
if (country != "FRA")
{
text.Replace("é", "e");
}
var separator = DetermineSeparator(country);
var split = text.Split(separator);
string.Join("|", split);
}
Mas quando você lida com todos os países possíveis do mundo, isso se torna muito complicado. E, independentemente disso, as if
instruções tornam a lógica mais difícil de ler (pelo menos, se você imaginar um método mais complexo que o exemplo), e a complexidade ciclomática começa a surgir rapidamente.
Então, no momento, estou fazendo algo assim:
public class Processor
{
CountrySpecificHandlerFactory handlerFactory;
public Processor(CountrySpecificHandlerFactory handlerFactory)
{
this.handlerFactory = handlerFactory;
}
public string Process(string country, string text)
{
var handlers = this.handlerFactory.CreateHandlers(country);
handlers.Capitalier.Capitalise(text);
handlers.PunctuationHandler.RemovePunctuation(text);
handlers.SpecialCharacterHandler.ReplaceSpecialCharacters(text);
var separator = handlers.SeparatorHandler.DetermineSeparator();
var split = text.Split(separator);
string.Join("|", split);
}
}
Manipuladores:
public class CountrySpecificHandlerFactory
{
private static IDictionary<string, ICapitaliser> capitaliserDictionary
= new Dictionary<string, ICapitaliser>
{
{ "USA", new Capitaliser() },
{ "GBR", new Capitaliser() },
{ "FRA", new ThingThatDoesNotCapitaliseButImplementsICapitaliser() },
{ "DEU", new ThingThatDoesNotCapitaliseButImplementsICapitaliser() },
};
// Imagine the other dictionaries like this...
public CreateHandlers(string country)
{
return new CountrySpecificHandlers
{
Capitaliser = capitaliserDictionary[country],
PunctuationHanlder = punctuationDictionary[country],
// etc...
};
}
}
public class CountrySpecificHandlers
{
public ICapitaliser Capitaliser { get; private set; }
public IPunctuationHanlder PunctuationHanlder { get; private set; }
public ISpecialCharacterHandler SpecialCharacterHandler { get; private set; }
public ISeparatorHandler SeparatorHandler { get; private set; }
}
Do mesmo modo, não tenho certeza se gosto. A lógica ainda é um pouco obscurecida por toda a criação da fábrica e você não pode simplesmente olhar para o método original e ver o que acontece quando um processo "GBR" é executado, por exemplo. Você também acaba criando muitas classes (em exemplos mais complexos que isso) no estilo GbrPunctuationHandler
, UsaPunctuationHandler
etc ... o que significa que você precisa observar várias classes diferentes para descobrir todas as ações possíveis que podem ocorrer durante a pontuação tratamento. Obviamente, não quero uma classe gigante com um bilhão de if
declarações, mas igualmente 20 classes com lógica ligeiramente diferente também parecem desajeitadas.
Basicamente, acho que me envolvi em algum tipo de nó OOP e não conheço uma boa maneira de desenredá-lo. Fiquei me perguntando se havia um padrão lá fora, que ajudaria com esse tipo de processo?
PreProcess
funcionalidade, que pode ser implementada de maneira diferente com base em alguns países,DetermineSeparator
pode estar lá para todos eles e aPostProcess
. Todos eles podem serprotected virtual void
com uma implementação padrão, e então você pode ter específicasProcessors
por paísif (country == "DEU")
você verificarif (config.ShouldRemovePunctuation)
.country
uma string, em vez de uma instância de uma classe que modela essas opções?Respostas:
Eu sugeriria encapsular todas as opções em uma classe:
e passe para o
Process
método:fonte
CountrySpecificHandlerFactory
... o_0public class ProcessOptions
deve realmente ser apenas[Flags] enum class ProcessOptions : int { ... }
...ProcessOptions
. Muito conveniente.Quando o framework .NET decidiu lidar com esses tipos de problemas, não modelou tudo como
string
. Então você tem, por exemplo, aCultureInfo
classe :Agora, essa classe pode não conter os recursos específicos de que você precisa, mas obviamente você pode criar algo análogo. E então você altera seu
Process
método:Sua
CountryInfo
classe pode ter umabool RequiresCapitalization
propriedade, etc., que ajude seuProcess
método a direcionar seu processamento adequadamente.fonte
Talvez você possa ter um
Processor
por país?E uma classe base para lidar com partes comuns do processamento:
Além disso, você deve refazer seus tipos de retorno, porque eles não serão compilados conforme você os escreveu - algumas vezes, um
string
método não retorna nada.fonte
Você pode criar uma interface comum com um
Process
método ...Então você o implementa para cada país ...
Você pode criar um método comum para instanciar e executar cada classe relacionada ao país ...
Então você só precisa criar e usar os processadores assim ...
Aqui está um exemplo de violino dotnet em funcionamento ...
Você coloca todo o processamento específico do país em cada classe de país. Crie uma classe comum (na classe Processing) para todos os métodos individuais reais, para que cada processador de país se torne uma lista de outras chamadas comuns, em vez de copiar o código em cada classe de país.
Nota: você precisará adicionar ...
para que o método estático crie uma instância da classe de país.
fonte
Process
e usá-la uma vez para obter o IProcessor correto? Você normalmente processaria muito texto de acordo com as regras do mesmo país.Process("GBR", "text");
, executa o método estático que cria uma instância do processador GBR e executa o método Process. Ele o executa apenas em uma instância, para esse tipo de país específico.Algumas versões atrás, o C # swtich recebeu suporte total para correspondência de padrões . Para que o caso "correspondência de vários países" seja feito com facilidade. Embora ainda não exista capacidade de queda, uma entrada pode corresponder a vários casos com correspondência de padrões. Talvez isso torne o if-spam um pouco mais claro.
Agora, um switch geralmente pode ser substituído por uma coleção. Você precisa usar Delegados e um Dicionário. O processo pode ser substituído por.
Então você pode fazer um dicionário:
Eu usei functionNames para entregar o Delegado. Mas você pode usar a sintaxe Lambda para fornecer o código inteiro lá. Dessa forma, você poderia simplesmente ocultar toda a coleção como faria com qualquer outra coleção grande. E o código se torna uma pesquisa simples:
Essas são praticamente as duas opções. Você pode considerar o uso de Enumerações em vez de seqüências de caracteres para a correspondência, mas esse é um detalhe menor.
fonte
Talvez eu (dependendo dos detalhes do seu caso de uso) fosse
Country
um objeto "real" em vez de uma string. A palavra-chave é "polimorfismo".Então, basicamente, seria assim:
Então você pode criar países especializados para aqueles que você precisa. Nota: você não precisa criar
Country
objetos para todos os países, pode terLatinlikeCountry
, ou até mesmoGenericCountry
. Lá você pode coletar o que deve ser feito e até reutilizar outros, como:Ou similar.
Country
pode serLanguage
que não esteja certo sobre o caso de uso, mas você entendeu.Além disso, o método do curso não
Process()
deve ser o que você realmente precisa fazer. GostoWords()
ou o que seja.fonte
Você deseja delegar (aceno para cadeia de responsabilidade) algo que saiba sobre sua própria cultura. Portanto, use ou crie uma construção do tipo Country ou CultureInfo, conforme mencionado acima em outras respostas.
Mas, em geral e fundamentalmente, o seu problema é que você está pegando construções processuais como 'processador' e aplicando-as ao OO. OO trata-se de representar conceitos do mundo real de um domínio comercial ou de problema em software. O processador não se traduz em nada no mundo real além do próprio software. Sempre que você tiver aulas como Processador, Gerente ou Governador, os alarmes deverão tocar.
fonte
Cadeia de responsabilidade é o tipo de coisa que você pode estar procurando, mas no OOP é um pouco complicado ...
Que tal uma abordagem mais funcional com C #?
NOTA: Não precisa ser totalmente estático, é claro. Se a classe Process precisar de estado, você poderá usar uma classe instanciada ou uma função parcialmente aplicada;).
Você pode criar o processo para cada país na inicialização, armazenar cada um em uma coleção indexada e recuperá-los quando necessário com o custo O (1).
fonte
Eu simplesmente implementar rotinas
Capitalise
,RemovePunctuation
etc., subprocessos que pode ser enviado mensagens com umtext
ecountry
parâmetros, e retornaria um texto processado.Use dicionários para agrupar países que se encaixam em um atributo específico (se você preferir listas, isso também funcionaria com apenas um pequeno custo de desempenho). Por exemplo:
CapitalisationApplicableCountries
ePunctuationRemovalApplicableCountries
.fonte
Eu sinto que as informações sobre os países devem ser mantidas em dados, não em código. Portanto, em vez de uma classe CountryInfo ou um dicionário CapitalisationApplicableCountries, você pode ter um banco de dados com um registro para cada país e um campo para cada etapa do processamento e, em seguida, o processamento poderá percorrer os campos de um determinado país e processar adequadamente. A manutenção é então principalmente no banco de dados, com o novo código necessário apenas quando novas etapas são necessárias, e os dados podem ser legíveis por humanos no banco de dados. Isso pressupõe que as etapas sejam independentes e não interfiram entre si; se não é assim, as coisas são complicadas.
fonte