Programación orientada a objetos: cómo evitar la duplicación en procesos que difieren ligeramente dependiendo de una variable

64

Algo que surge bastante en mi trabajo actual es que hay un proceso generalizado que debe suceder, pero luego la parte extraña de ese proceso debe suceder de manera ligeramente diferente dependiendo del valor de una determinada variable, y no estoy Estoy bastante seguro de cuál es la forma más elegante de manejar esto.

Usaré el ejemplo que usualmente tenemos, que es hacer las cosas de manera ligeramente diferente dependiendo del país con el que estemos tratando.

Entonces tengo una clase, llamémosla 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);
    }
}

Excepto que solo algunas de esas acciones tienen que suceder en ciertos países. Por ejemplo, solo 6 países requieren el paso de capitalización. El personaje para dividirse puede cambiar según el país. Sustitución de lo acentuado'e' solo puede ser necesario dependiendo del país.

Obviamente, podrías resolverlo haciendo algo como esto:

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);
}

Pero cuando se trata de todos los países posibles del mundo, eso se vuelve muy engorroso. Y a pesar de todo, las ifdeclaraciones hacen que la lógica sea más difícil de leer (al menos, si imagina un método más complejo que el ejemplo), y la complejidad ciclomática comienza a ascender bastante rápido.

Así que por el momento estoy haciendo algo como esto:

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);
    }
}

Manejadores:

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; }
}

Lo que tampoco estoy seguro de que me guste. La lógica todavía está algo oscurecida por toda la creación de fábrica y no puede simplemente mirar el método original y ver qué sucede cuando se ejecuta un proceso "GBR", por ejemplo. También terminas creando muchas clases (en ejemplos más complejos que este) en el estilo GbrPunctuationHandler, UsaPunctuationHandleretc., lo que significa que tienes que mirar varias clases diferentes para descubrir todas las acciones posibles que podrían ocurrir durante la puntuación manejo. Obviamente no quiero una clase gigante con mil millonesif declaraciones, pero igualmente 20 clases con una lógica ligeramente diferente también se siente torpe.

Básicamente, creo que me he metido en algún tipo de nudo OOP y no conozco una buena manera de desenredarlo. Me preguntaba si había un patrón que ayudaría con este tipo de proceso.

John Darvill
fuente
Parece que tiene una PreProcessfuncionalidad, que podría implementarse de manera diferente en función de algunos de los países, DetermineSeparatorpuede estar disponible para todos ellos, y a PostProcess. Todos ellos pueden tener protected virtual voiduna implementación predeterminada, y luego puede tener una especificación Processorspor país
Icepickle,
Su trabajo es hacer que, en un período de tiempo determinado, funcione y que pueda ser mantenido en un futuro próximo por usted u otra persona. Si varias opciones pueden satisfacer ambas condiciones, entonces puede elegir cualquiera de ellas, de acuerdo con sus gustos.
Dialecticus el
2
Una opción viable para usted es tener una configuración. Entonces, en su código, no verifica el país específico, sino la opción de configuración específica. Pero cada país tendrá un conjunto específico de esas opciones de configuración. Por ejemplo, en lugar de if (country == "DEU")comprobar if (config.ShouldRemovePunctuation).
Dialecticus
11
Si los países tienen varias opciones, ¿por qué es countryuna cadena en lugar de una instancia de una clase que modela esas opciones?
Damien_The_Unbeliever
@Damien_The_Unbeliever: ¿podría explicar un poco más sobre esto? ¿La respuesta de Robert Brautigam está debajo de lo que sugieres? - Ah, puedo ver tu respuesta ahora, ¡gracias!
John Darvill el

Respuestas:

53

Sugeriría encapsular todas las opciones en una clase:

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; }
}

y pasarlo al 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);
}
Michał Turczyn
fuente
44
No estoy seguro de por qué algo así no se intentó antes de saltar a CountrySpecificHandlerFactory... o_0
Mateen Ulhaq
Mientras no haya opciones demasiado especializadas, definitivamente iría por este camino. Si las opciones se serializan en un archivo de texto, también permite a los no programadores definir nuevas variantes / actualizar las existentes sin necesidad de cambiar a la aplicación.
Tom
44
Eso public class ProcessOptionsrealmente debería ser [Flags] enum class ProcessOptions : int { ... }...
Drunken Code Monkey
Y supongo que si lo necesitan, podrían tener un mapa de países para ProcessOptions. Muy conveniente.
theonlygusti
24

Cuando .NET Framework se dispuso a manejar este tipo de problemas, no modeló todo como string. Entonces tienes, por ejemplo, la CultureInfoclase :

Proporciona información sobre una cultura específica (denominada configuración regional para el desarrollo de código no administrado). La información incluye los nombres de la cultura, el sistema de escritura, el calendario utilizado, el orden de clasificación de las cadenas y el formato de fechas y números.

Ahora, esta clase puede no contener las características específicas que necesita, pero obviamente puede crear algo análogo. Y luego cambias tu Processmétodo:

public string Process(CountryInfo country, string text)

Su CountryInfoclase puede tener una bool RequiresCapitalizationpropiedad, etc., que ayude a su Processmétodo a dirigir su procesamiento adecuadamente.

Damien_The_Unbeliever
fuente
13

¿Quizás podrías tener uno 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();
    }
}

Y una clase base para manejar partes comunes del procesamiento:

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);
    }
}

Además, debe volver a trabajar sus tipos de retorno porque no se compilará como los escribió, a veces un stringmétodo no devuelve nada.

Panel Corentin
fuente
Supongo que estaba tratando de seguir la composición sobre el mantra de la herencia. Pero sí, definitivamente es una opción, gracias por la respuesta.
John Darvill el
Lo suficientemente justo. Creo que la herencia está justificada en algunos casos, pero realmente depende de cómo planeas cargar / almacenar / invocar / cambiar tus métodos y procesamiento al final.
Corentin Pane
3
A veces, la herencia es la herramienta adecuada para el trabajo. Si tiene un proceso que se comportará principalmente de la misma manera en varias situaciones diferentes, pero también tiene varias partes que se comportarán de manera diferente en diferentes situaciones, es una buena señal que debería considerar usar la herencia.
Tanner Swett el
5

Puede crear una interfaz común con un Processmétodo ...

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

Luego lo implementas 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)";
        }
    }
}

Luego puede crear un método común para crear instancias y ejecutar cada clase relacionada con el 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);
}

Entonces solo necesita crear y usar los procesadores así ...

// 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."));

Aquí hay un ejemplo de violín dotnet que funciona ...

Coloca todo el procesamiento específico del país en cada clase de país. Cree una clase común (en la clase de Procesamiento) para todos los métodos individuales reales, de modo que cada procesador de país se convierta en una lista de otras llamadas comunes, en lugar de copiar el código en cada clase de país.

Nota: Deberá agregar ...

using System.Assembly;

para que el método estático cree una instancia de la clase de país.

Reinstalar a Monica Cellio
fuente
¿No es la reflexión notablemente lenta en comparación con ningún código reflejado? ¿Vale la pena para este caso?
jlvaquero
@jlvaquero No, la reflexión no es notablemente lenta en absoluto. Por supuesto, hay un impacto en el rendimiento sobre la especificación de un tipo en tiempo de diseño, pero en realidad es una diferencia de rendimiento insignificante y solo se nota cuando se usa en exceso. He implementado grandes sistemas de mensajería basados ​​en el manejo de objetos genéricos y no hemos tenido ninguna razón para cuestionar el rendimiento, y eso es con una gran cantidad de rendimiento. Sin una diferencia notable en el rendimiento, siempre usaré un código simple para mantener, como este.
Reinstale a Monica Cellio el
Si está reflexionando, ¿no le gustaría eliminar la cadena de país de cada llamada Processy utilizarla una vez para obtener el procesador de IP correcto? Por lo general, se procesará una gran cantidad de texto de acuerdo con las normas del mismo país.
Davislor
@Davislor Eso es exactamente lo que hace este código. Cuando lo llama Process("GBR", "text");, ejecuta el método estático que crea una instancia del procesador GBR y ejecuta el método de proceso en eso. Solo lo ejecuta en una instancia, para ese tipo de país específico.
Vuelva a instalar Monica Cellio el
@Archer Right, por lo que en el caso típico en el que está procesando varias cadenas de acuerdo con las reglas para el mismo país, sería más eficiente crear la instancia una vez, o buscar una instancia constante en una tabla hash / Diccionario y devolver una referencia a eso. Luego puede llamar a la transformación de texto en la misma instancia. Crear una nueva instancia para cada llamada y luego descartarlo, en lugar de reutilizarlo para cada llamada, es un desperdicio.
Davislor
3

Hace algunas versiones, se le dio el C # swtich soporte completo para la coincidencia de patrones . Para que el caso de "coincidencia de países múltiples" se haga fácilmente. Si bien aún no tiene capacidad de caída, una entrada puede coincidir con múltiples casos con coincidencia de patrones. Tal vez podría hacer que el correo no deseado sea un poco más claro.

Npw un interruptor generalmente se puede reemplazar con una colección. Necesitas estar usando Delegados y un Diccionario. El proceso puede ser reemplazado por.

public delegate string ProcessDelegate(string text);

Entonces podrías hacer un diccionario:

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

Usé functionNames para entregar el Delegado. Pero podría usar la sintaxis de Lambda para proporcionar el código completo allí. De esa manera, podría ocultar toda esa Colección como lo haría con cualquier otra colección grande. Y el código se convierte en una simple búsqueda:

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

Esas son más o menos las dos opciones. Es posible que desee considerar el uso de Enumeraciones en lugar de cadenas para la coincidencia, pero eso es un detalle menor.

Christopher
fuente
2

Tal vez (dependiendo de los detalles de su caso de uso) iría con el Country ser un objeto "real" en lugar de una cadena. La palabra clave es "polimorfismo".

Así que básicamente se vería así:

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

Luego puede crear países especializados para aquellos que necesita. Nota: no tiene que crear Countryobjetos para todos los países, puede tener LatinlikeCountry, o incluso GenericCountry. Allí puede recopilar lo que debe hacerse, incluso reutilizar otros, como:

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

O similar. Countrypuede ser en realidad Language, no estoy seguro sobre el caso de uso, pero entiendo el punto.

Además, el método, por supuesto, no Process()debería ser lo que realmente necesita hacer. Me gusta Words()o lo que sea.

Robert Bräutigam
fuente
1
Escribí algo más verbal, pero creo que esto es básicamente lo que más me gusta. Si el caso de uso necesita buscar estos objetos en función de una cadena de país, puede usar la solución de Christopher con esto. La implementación de las interfaces podría incluso ser una clase cuyas instancias establezcan rasgos como en la respuesta de Michal, para optimizar el espacio en lugar del tiempo.
Davislor el
1

Desea delegar (asentir a la cadena de responsabilidad) algo que sepa sobre su propia cultura. Por lo tanto, use o cree una construcción de tipo Country o CultureInfo, como se mencionó anteriormente en otras respuestas.

Pero en general y fundamentalmente su problema es que está tomando constructos de procedimiento como 'procesador' y aplicándolos a OO. OO se trata de representar conceptos del mundo real desde un dominio comercial o problemático en software. El procesador no se traduce en nada en el mundo real aparte del software en sí. Siempre que tenga clases como Procesador o Gerente o Gobernador, deben sonar las alarmas.

Franco
fuente
0

Me preguntaba si había un patrón que ayudaría con este tipo de proceso.

La cadena de responsabilidad es el tipo de cosas que puede estar buscando, pero en OOP es algo engorroso ...

¿Qué pasa con un enfoque más funcional con 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: No tiene que ser todo estático, por supuesto. Si la clase de proceso necesita un estado, puede usar una clase instanciada o una función parcialmente aplicada;).

Puede crear el Proceso para cada país al inicio, almacenar cada uno en una colección indexada y recuperarlos cuando sea necesario con un costo O (1).

jlvaquero
fuente
0

Lamento haber acuñado hace mucho tiempo el término "objetos" para este tema porque hace que muchas personas se centren en la idea menor. La gran idea es la mensajería .

~ Alan Kay, sobre mensajería

Simplemente implementaría rutinas Capitalise, RemovePunctuationetc. , como subprocesos que pueden enviarse mensajes con un texty countryparámetros, y devolvería un texto procesado.

Use diccionarios para agrupar países que se ajusten a un atributo específico (si prefiere listas, eso funcionaría tan bien con solo un ligero costo de rendimiento). Por ejemplo: CapitalisationApplicableCountriesy 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.
}
Igwe Kalu
fuente
0

Siento que la información sobre los países debe mantenerse en datos, no en código. Entonces, en lugar de una clase CountryInfo o un diccionario CapitalisationApplicableCountries, podría tener una base de datos con un registro para cada país y un campo para cada paso de procesamiento, y luego el procesamiento podría pasar por los campos de un país determinado y procesar en consecuencia. El mantenimiento se realiza principalmente en la base de datos, con un nuevo código solo necesario cuando se necesitan nuevos pasos, y los datos pueden ser legibles por humanos en la base de datos. Esto supone que los pasos son independientes y no interfieren entre sí; si eso no es así, las cosas son complicadas.

Steve J
fuente