Something that comes up quite a lot in my current work is that there is a generalised process that needs to happen, but then the odd part of that process needs to happen slightly differently depending on the value of a certain variable, and I'm not quite sure what's the most elegant way to handle this.
I'll use the example that we usually have, which is doing things slightly differently depending on the country we're dealing with.
So I have a class, let's call it 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); } }
Except that only some of those actions need to happen for certain countries. For example, only 6 countries require the capitalisation step. The character to split on might change depending on the country. Replacing the accented 'e'
might only be required depending on the country.
Obviously you could solve it by doing something like this:
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); }
But when you're dealing with all the possible countries in the world, that becomes very cumbersome. And regardless, the if
statements make the logic harder to read (at least, if you imagine a more complex method than the example), and the cyclomatic complexity starts to creep up pretty fast.
So at the moment I'm sort of doing something like this:
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); } }
Handlers:
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; } }
Which equally I'm not really sure I like. The logic is still somewhat obscured by all of the factory creation and you can't simply look at the original method and see what happens when a "GBR" process is executed, for example. You also end up creating a lot of classes (in more complex examples than this) in the style GbrPunctuationHandler
, UsaPunctuationHandler
, etc... which means that you have to look at several different classes to figure out all of the possible actions that could happen during punctuation handling. Obviously I don't want one giant class with a billion if
statements, but equally 20 classes with slightly differing logic also feels clunky.
Basically I think I've got myself into some sort of OOP knot and don't quite know a good way of untangling it. I was wondering if there was a pattern out there that would help with this type of process?
Don't Repeat Yourself (DRY): Using DRY or Do not Repeat Yourself principle, you make sure that you stay away from duplicate code as often as you can. Rather you replace the duplicate code with abstractions or use data normalization. To reduce duplicity in a function, one can use loops and trees.
The Issue With Code DuplicationDuplication greatly decreases the maintainability of your code. Ideally, introducing a change in business logic should require your team to change one class or function, and no more. Otherwise, a developer has to spend extra effort hunting down all these extra occurrences.
There are three major features in object-oriented programming that makes them different than non-OOP languages: encapsulation, inheritance and polymorphism.
A lot of the confusion when learning OOP comes from trying to pick the right relationship between objects and classes of objects, particularly whether: Object contains Some other Object (or Object1 has an Object2 ) Object is an instance of Class.
I would suggest encapsulating all options in one class:
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 bool SplitAndJoin { get; set; } public char JoinChar { get; set; } public char SplitChar { get; set; } }
and pass it into the Process
method:
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); if(options.SplitAndJoin) { var split = text.Split(options.SplitChar); return string.Join(options.JoinChar, split); } return text; }
When the .NET framework set out to handle these sorts of problems, it didn't model everything as string
. So you have, for instance, the CultureInfo
class:
Provides information about a specific culture (called a locale for unmanaged code development). The information includes the names for the culture, the writing system, the calendar used, the sort order of strings, and formatting for dates and numbers.
Now, this class may not contain the specific features that you need, but you can obviously create something analogous. And then you change your Process
method:
public string Process(CountryInfo country, string text)
Your CountryInfo
class can then have a bool RequiresCapitalization
property, etc, that helps your Process
method direct its processing appropriately.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With