c# oop

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



oop (10)

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. Reemplazar la ''e'' acentuada 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. E independientemente, las declaraciones if 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 aumentar bastante rápidamente.

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 se 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 , UsaPunctuationHandler , etc., lo que significa que tienes que mirar varias clases diferentes para descubrir todas las acciones posibles que podrían ocurrir. durante el manejo de puntuación. Obviamente no quiero una clase gigante con mil millones de declaraciones if , 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.


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 , RemovePunctuation , etc. como subprocesos que pueden enviarse mensajes con un text y parámetros de country , 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: CapitalisationApplicableCountries y 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. }


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


¿Quizás podría tener un Processor por 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 método de string no devuelve nada.


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

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 método de Process :

public string Process(CountryInfo country, string text)

Su clase CountryInfo puede tener una propiedad bool RequiresCapitalization , etc., que ayuda a su método de Process dirigir su procesamiento de manera apropiada.


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.


Hace algunas versiones, el C # swtich recibió 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.


Puede crear una interfaz común con un método de Process ...

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


Quizás (dependiendo de los detalles de su caso de uso) iría con el Country como 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 un objeto Country 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. Country puede ser en realidad Language , no estoy seguro sobre el caso de uso, pero entiendo el punto.

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


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.


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

y pasarlo al método de Process :

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