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 if
declaraciones 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
, UsaPunctuationHandler
etc., 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.
PreProcess
funcionalidad, que podría implementarse de manera diferente en función de algunos de los países,DetermineSeparator
puede estar disponible para todos ellos, y aPostProcess
. Todos ellos pueden tenerprotected virtual void
una implementación predeterminada, y luego puede tener una especificaciónProcessors
por paísif (country == "DEU")
comprobarif (config.ShouldRemovePunctuation)
.country
una cadena en lugar de una instancia de una clase que modela esas opciones?Respuestas:
Sugeriría encapsular todas las opciones en una clase:
y pasarlo al
Process
método:fuente
CountrySpecificHandlerFactory
... o_0public class ProcessOptions
realmente debería ser[Flags] enum class ProcessOptions : int { ... }
...ProcessOptions
. Muy conveniente.Cuando .NET Framework se dispuso a manejar este tipo de problemas, no modeló todo como
string
. Entonces tienes, por ejemplo, laCultureInfo
clase :Ahora, esta clase puede no contener las características específicas que necesita, pero obviamente puede crear algo análogo. Y luego cambias tu
Process
método:Su
CountryInfo
clase puede tener unabool RequiresCapitalization
propiedad, etc., que ayude a suProcess
método a dirigir su procesamiento adecuadamente.fuente
¿Quizás podrías tener uno
Processor
por país?Y una clase base para manejar partes comunes del procesamiento:
Además, debe volver a trabajar sus tipos de retorno porque no se compilará como los escribió, a veces un
string
método no devuelve nada.fuente
Puede crear una interfaz común con un
Process
método ...Luego lo implementas para cada país ...
Luego puede crear un método común para crear instancias y ejecutar cada clase relacionada con el país ...
Entonces solo necesita crear y usar los procesadores así ...
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 ...
para que el método estático cree una instancia de la clase de país.
fuente
Process
y 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.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.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.
Entonces podrías hacer un diccionario:
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:
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.
fuente
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í:
Luego puede crear países especializados para aquellos que necesita. Nota: no tiene que crear
Country
objetos para todos los países, puede tenerLatinlikeCountry
, o inclusoGenericCountry
. Allí puede recopilar lo que debe hacerse, incluso reutilizar otros, como:O similar.
Country
puede ser en realidadLanguage
, 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 gustaWords()
o lo que sea.fuente
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.
fuente
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 #?
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).
fuente
Simplemente implementaría rutinas
Capitalise
,RemovePunctuation
etc. , como subprocesos que pueden enviarse mensajes con untext
ycountry
pará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:
CapitalisationApplicableCountries
yPunctuationRemovalApplicableCountries
.fuente
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.
fuente