Programação Orientada a Objetos - como evitar duplicação em processos que diferem ligeiramente dependendo de uma variável


64

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 ifinstruçõ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, UsaPunctuationHandleretc ... 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 ifdeclaraçõ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?


Parece que você tem uma PreProcessfuncionalidade, que pode ser implementada de maneira diferente com base em alguns países, DetermineSeparatorpode estar lá para todos eles e a PostProcess. Todos eles podem ser protected virtual voidcom uma implementação padrão, e então você pode ter específicas Processorspor país
Icepickle

Seu trabalho é fazer, em determinado período de tempo, algo que funcione e possa ser mantido em um futuro previsível por você ou outra pessoa. Se várias opções puderem satisfazer ambas as condições, você poderá escolher qualquer uma delas, de acordo com seus gostos.
Dialecticus

2
Uma opção viável para você é ter uma configuração. Portanto, no seu código, você não procura por país específico, mas por opção de configuração específica. Mas cada país terá um conjunto específico dessas opções de configuração. Por exemplo, em vez de if (country == "DEU")você verificar if (config.ShouldRemovePunctuation).
Dialecticus

11
Se os países têm várias opções, por que countryuma string, em vez de uma instância de uma classe que modela essas opções?
Damien_The_Unbeliever

@Damien_The_Unbeliever - você poderia elaborar um pouco sobre isso? A resposta de Robert Brautigam está abaixo do que você sugere? - ah posso ver sua resposta agora, obrigado!
precisa

Respostas:


53

Eu sugeriria encapsular todas as opções em uma classe:

public class ProcessOptions
{
  public bool Capitalise { get; set; }
  public bool RemovePunctuation { get; set; }
  public bool Replace { get; set; }
  public char ReplaceChar { get; set; }
  public char ReplacementChar { get; set; }
  public char JoinChar { get; set; }
  public char SplitChar { get; set; }
}

e passe para o Processmétodo:

public string Process(ProcessOptions options, string text)
{
  if(options.Capitalise)
    text.Capitalise();

  if(options.RemovePunctuation)
    text.RemovePunctuation();

  if(options.Replace)
    text.Replace(options.ReplaceChar, options.ReplacementChar);

  var split = text.Split(options.SplitChar);

  string.Join(options.JoinChar, split);
}

4
Não sei por que algo assim não foi tentado antes de pular para CountrySpecificHandlerFactory... o_0
Mateen Ulhaq 9/09/19

Enquanto não houver opções muito especializadas, eu definitivamente seguiria esse caminho. Se as opções forem serializadas em arquivo de texto, também permitirá que não programadores definam novas variantes / atualizem as existentes sem precisar mudar para o aplicativo.
Tom

4
Isso public class ProcessOptionsdeve realmente ser apenas [Flags] enum class ProcessOptions : int { ... }...
Drunken Code Monkey

E eu acho que se eles precisarem, eles poderiam ter um mapa de países para ProcessOptions. Muito conveniente.
theonlygusti

24

Quando o framework .NET decidiu lidar com esses tipos de problemas, não modelou tudo como string. Então você tem, por exemplo, a CultureInfoclasse :

Fornece informações sobre uma cultura específica (chamada localidade para desenvolvimento de código não gerenciado). As informações incluem os nomes da cultura, o sistema de escrita, o calendário usado, a ordem de classificação das strings e a formatação das datas e números.

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 Processmétodo:

public string Process(CountryInfo country, string text)

Sua CountryInfoclasse pode ter uma bool RequiresCapitalizationpropriedade, etc., que ajude seu Processmétodo a direcionar seu processamento adequadamente.


13

Talvez você possa ter um Processorpor país?

public class FrProcessor : Processor {
    protected override string Separator => ".";

    protected override string ProcessSpecific(string text) {
        return text.Replace("é", "e");
    }
}

public class UsaProcessor : Processor {
    protected override string Separator => ",";

    protected override string ProcessSpecific(string text) {
        return text.Capitalise().RemovePunctuation();
    }
}

E uma classe base para lidar com partes comuns do processamento:

public abstract class Processor {
    protected abstract string Separator { get; }

    protected virtual string ProcessSpecific(string text) { }

    private string ProcessCommon(string text) {
        var split = text.Split(Separator);
        return string.Join("|", split);
    }

    public string Process(string text) {
        var s = ProcessSpecific(text);
        return ProcessCommon(s);
    }
}

Além disso, você deve refazer seus tipos de retorno, porque eles não serão compilados conforme você os escreveu - algumas vezes, um stringmétodo não retorna nada.


Acho que estava tentando seguir a composição sobre o mantra da herança. Mas sim, é definitivamente uma opção, obrigado pela resposta.
John John Darvill

Justo. Eu acho que a herança é justificada em alguns casos, mas realmente depende de como você planeja carregar / armazenar / invocar / alterar seus métodos e processamento no final.
Corentin Pane

3
Às vezes, a herança é a ferramenta certa para o trabalho. Se você tem um processo que vai se comportar em sua maioria da mesma forma em várias situações diferentes, mas também tem várias peças que se comportam de maneira diferente em diferentes situações, isso é um bom sinal de que você deve considerar o uso de herança.
quer

5

Você pode criar uma interface comum com um Processmétodo ...

public interface IProcessor
{
    string Process(string text);
}

Então você o implementa para cada país ...

public class Processors
{
    public class GBR : IProcessor
    {
        public string Process(string text)
        {
            return $"{text} (processed with GBR rules)";
        }
    }

    public class FRA : IProcessor
    {
        public string Process(string text)
        {
            return $"{text} (processed with FRA rules)";
        }
    }
}

Você pode criar um método comum para instanciar e executar cada classe relacionada ao país ...

// also place these in the Processors class above
public static IProcessor CreateProcessor(string country)
{
    var typeName = $"{typeof(Processors).FullName}+{country}";
    var processor = (IProcessor)Assembly.GetAssembly(typeof(Processors)).CreateInstance(typeName);
    return processor;
}

public static string Process(string country, string text)
{
    var processor = CreateProcessor(country);
    return processor?.Process(text);
}

Então você só precisa criar e usar os processadores assim ...

// create a processor object for multiple use, if needed...
var processorGbr = Processors.CreateProcessor("GBR");
Console.WriteLine(processorGbr.Process("This is some text."));

// create and use a processor for one-time use
Console.WriteLine(Processors.Process("FRA", "This is some more text."));

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 ...

using System.Assembly;

para que o método estático crie uma instância da classe de país.


A reflexão não é notavelmente lenta em comparação com nenhum código refletido? vale a pena para este caso?
jlvaquero

@jlvaquero Não, a reflexão não é notavelmente lenta. É claro que há um impacto no desempenho ao especificar um tipo em tempo de design, mas é realmente uma diferença insignificante de desempenho e só é perceptível quando você o utiliza em excesso. Implementei grandes sistemas de mensagens construídos com base no manuseio genérico de objetos e não tivemos motivos para questionar o desempenho, e isso gera enormes quantidades de taxa de transferência. Sem nenhuma diferença perceptível no desempenho, sempre seguirei um código simples de manutenção, como este.
Reintegrar Monica Cellio

Se você está refletindo, não deseja remover a sequência do país de cada chamada para Processe usá-la uma vez para obter o IProcessor correto? Você normalmente processaria muito texto de acordo com as regras do mesmo país.
Davislor 8/08/19

@ Davidislor Isso é exatamente o que esse código faz. Quando você o chama 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.
Reponha Monica Cellio em

@Archer Certo, então, no caso típico em que você está processando várias seqüências de acordo com as regras do mesmo país, seria mais eficiente criar a instância uma vez - ou procurar uma instância constante em uma tabela de hash / dicionário e retornar uma referência a isso. Você pode chamar a transformação de texto na mesma instância. Criar uma nova instância para cada chamada e depois descartá-la, em vez de reutilizá-la para todas as chamadas, é um desperdício.
Davislor 9/11/19

3

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.

public delegate string ProcessDelegate(string text);

Então você pode fazer um dicionário:

var Processors = new Dictionary<string, ProcessDelegate>(){
  { "USA", EnglishProcessor },
  { "GBR", EnglishProcessor },
  { "DEU", GermanProcessor }
}

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:

ProcessDelegate currentProcessor = Processors[country];
string processedString = currentProcessor(country);

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.


2

Talvez eu (dependendo dos detalhes do seu caso de uso) fosse Countryum objeto "real" em vez de uma string. A palavra-chave é "polimorfismo".

Então, basicamente, seria assim:

public interface Country {
   string Process(string text);
}

Então você pode criar países especializados para aqueles que você precisa. Nota: você não precisa criar Countryobjetos para todos os países, pode ter LatinlikeCountry, ou até mesmo GenericCountry. Lá você pode coletar o que deve ser feito e até reutilizar outros, como:

public class France {
   public string Process(string text) {
      return new GenericCountry().process(text)
         .replace('a', 'b');
   }
}

Ou similar. Countrypode ser Languageque 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. Gosto Words()ou o que seja.


11
Eu escrevi algo mais elaborado, mas acho que isso é basicamente o que eu mais gosto. Se o caso de uso precisar procurar esses objetos com base em uma sequência de país, ele poderá usar a solução de Christopher com isso. A implementação das interfaces pode até ser uma classe cujas instâncias definem características como na resposta de Michal, para otimizar o espaço em vez do tempo.
Davislor

1

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.


0

Fiquei me perguntando se havia um padrão lá fora, que ajudaria com esse tipo de processo

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 #?

using System;


namespace Kata {

  class Kata {


    static void Main() {

      var text = "     testing this thing for DEU          ";
      Console.WriteLine(Process.For("DEU")(text));

      text = "     testing this thing for USA          ";
      Console.WriteLine(Process.For("USA")(text));

      Console.ReadKey();
    }

    public static class Process {

      public static Func<string, string> For(string country) {

        Func<string, string> baseFnc = (string text) => text;

        var aggregatedFnc = ApplyToUpper(baseFnc, country);
        aggregatedFnc = ApplyTrim(aggregatedFnc, country);

        return aggregatedFnc;

      }

      private static Func<string, string> ApplyToUpper(Func<string, string> currentFnc, string country) {

        string toUpper(string text) => currentFnc(text).ToUpper();

        Func<string, string> fnc = null;

        switch (country) {
          case "USA":
          case "GBR":
          case "DEU":
            fnc = toUpper;
            break;
          default:
            fnc = currentFnc;
            break;
        }
        return fnc;
      }

      private static Func<string, string> ApplyTrim(Func<string, string> currentFnc, string country) {

        string trim(string text) => currentFnc(text).Trim();

        Func<string, string> fnc = null;

        switch (country) {
          case "DEU":
            fnc = trim;
            break;
          default:
            fnc = currentFnc;
            break;
        }
        return fnc;
      }
    }
  }
}

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).


0

Sinto muito que, há muito tempo, cunhei o termo "objetos" para este tópico porque leva muitas pessoas a se concentrarem na idéia menor. A grande idéia é de mensagens .

~ Alan Kay, em mensagens

Eu simplesmente implementar rotinas Capitalise, RemovePunctuationetc., subprocessos que pode ser enviado mensagens com um texte countryparâ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: CapitalisationApplicableCountriese PunctuationRemovalApplicableCountries.

/// Runs like a pipe: passing the text through several stages of subprocesses
public string Process(string country, string text)
{
    text = Capitalise(country, text);
    text = RemovePunctuation(country, text);
    // And so on and so forth...

    return text;
}

private string Capitalise(string country, string text)
{
    if ( ! CapitalisationApplicableCountries.ContainsKey(country) )
    {
        /* skip */
        return text;
    }

    /* do the capitalisation */
    return capitalisedText;
}

private string RemovePunctuation(string country, string text)
{
    if ( ! PunctuationRemovalApplicableCountries.ContainsKey(country) )
    {
        /* skip */
        return text;
    }

    /* do the punctuation removal */
    return punctuationFreeText;
}

private string Replace(string country, string text)
{
    // Implement it following the pattern demonstrated earlier.
}

0

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.