Объектно-ориентированное программирование - как избежать дублирования в процессах, которые немного отличаются в зависимости от переменной - PullRequest
47 голосов
/ 08 ноября 2019

Что-то, что встречается довольно часто в моей текущей работе, заключается в том, что существует обобщенный процесс, который должен произойти, но тогда нечетная часть этого процесса должна происходить немного по-другому, в зависимости от значения определенной переменной, и яЯ не совсем уверен, каков самый элегантный способ справиться с этим.

Я буду использовать пример, который у нас обычно есть, который делает вещи немного по-разному в зависимости от страны, с которой мы имеем дело.

Итак, у меня есть класс, назовем его 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);
    }
}

За исключением того, что для некоторых стран должны выполняться только некоторые из этих действий. Например, только 6 стран требуют шага капитализации. Персонаж для разделения может меняться в зависимости от страны. Замена акцентированного 'e' может потребоваться только в зависимости от страны.

Очевидно, вы можете решить эту проблему, выполнив что-то вроде этого:

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

Но когда вы имеете дело со всемивозможные страны в мире, это становится очень громоздким. И, тем не менее, операторы if затрудняют чтение логики (по крайней мере, если вы представите более сложный метод, чем в примере), и цикломатическая сложность начинает довольно быстро нарастать.

Так что нав тот момент, когда я делаю что-то вроде этого:

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

Обработчики:

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

Что, в равной степени, я не совсем уверен, что мне нравится. Логика все еще несколько скрыта при создании фабрики, и вы не можете просто взглянуть на оригинальный метод и посмотреть, что происходит, например, при выполнении процесса «GBR». Вы также в конечном итоге создаете много классов (в более сложных примерах, чем это) в стиле GbrPunctuationHandler, UsaPunctuationHandler и т. Д., Что означает, что вам нужно взглянуть на несколько разных классов, чтобы выяснить все возможныедействия, которые могут произойти во время обработки знаков препинания. Очевидно, я не хочу, чтобы один гигантский класс с миллиардом if утверждений, но в равной степени 20 классов с немного отличающейся логикой также чувствуют себя неуклюже.

В основном я думаю, что я попал в какой-то узел ООП иЯ не совсем знаю, как распутать это. Мне было интересно, был ли какой-то шаблон, который бы помог с этим типом процесса?

Ответы [ 10 ]

45 голосов
/ 08 ноября 2019

Я бы предложил инкапсулировать все опции в одном классе:

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

и передать его в метод 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);
}
22 голосов
/ 08 ноября 2019

Когда платформа .NET намеревалась решать подобные проблемы, она не моделировала все как string. Так, например, у вас есть CultureInfo класс :

Предоставляет информацию о конкретной культуре (называемой локалью для разработки неуправляемого кода). Информация включает в себя имена для культуры, систему письма, используемый календарь, порядок сортировки строк и форматирование для дат и чисел.

Теперь этот класс может не содержать конкретные функции, которые вам нужны, но вы, очевидно, можете создать нечто аналогичное. И затем вы меняете метод Process:

public string Process(CountryInfo country, string text)

Ваш класс CountryInfo может затем иметь свойство bool RequiresCapitalization и т. Д., Что помогает вашему методу Process соответствующим образом направлять его обработку.

11 голосов
/ 08 ноября 2019

Может быть, у вас может быть один Processor на страну?

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

И один базовый класс для обработки общих частей обработки:

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

Кроме того, вам следует переделать свойвозвращайте типы, потому что они не будут компилироваться так, как вы их написали - иногда метод string ничего не возвращает.

4 голосов
/ 08 ноября 2019

Вы можете создать общий интерфейс с помощью метода Process ...

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

Затем вы реализуете его для каждой страны ...

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

Затем вы можете создатьобщий метод для создания и выполнения каждого класса, связанного со страной ...

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

Тогда вам просто нужно создать и использовать процессоры, такие как ...

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

Вот пример рабочего скрипта dotnet ...

Вы размещаете все специфичные для страны обработки в каждом классе страны. Создайте общий класс (в классе Processing) для всех реальных отдельных методов, чтобы каждый процессор страны становился списком других общих вызовов, а не копировал код в каждом классе страны.

Примечание: Вам нужно будет добавить ...

using System.Assembly;

, чтобы статический метод создал экземпляр класса страны.

2 голосов
/ 08 ноября 2019

Несколько версий назад, C # swtich была предоставлена ​​ полная поддержка сопоставления с шаблоном . Так что дело «совпадение нескольких стран» легко сделать. Хотя у него все еще нет способности к провалу, один вход может сопоставить несколько случаев с сопоставлением с образцом. Это может сделать этот спам немного понятнее.

Npw, переключатель обычно можно заменить на коллекцию. Вы должны использовать делегатов и словарь. Процесс может быть заменен на.

public delegate string ProcessDelegate(string text);

Затем вы можете создать словарь:

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

Я использовал functionNames, чтобы передать делегат. Но вы можете использовать лямбда-синтаксис для предоставления всего кода там. Таким образом, вы можете просто спрятать всю коллекцию, как любую другую большую коллекцию. И код становится простым поиском:

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

Это почти два варианта. Возможно, вы захотите использовать перечисления вместо строк для сопоставления, но это небольшая деталь.

2 голосов
/ 08 ноября 2019

Возможно, я бы (в зависимости от деталей вашего варианта использования) выбрал бы Country как "настоящий" объект вместо строки. Ключевое слово «полиморфизм».

Таким образом, в основном это будет выглядеть так:

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

Тогда вы сможете создавать специализированные страны для тех, кто вам нужен. Примечание: вам не нужно создавать Country объект для всех стран, вы можете иметь LatinlikeCountry или даже GenericCountry. Там вы можете собрать то, что должно быть сделано, даже повторно используя другие, например:

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

или подобное. Country может быть на самом деле Language, я не уверен насчет варианта использования, но я вас понимаю.

Кроме того, метод, конечно, не должен быть Process(), это должен бытьто, что вам действительно нужно сделать. Как Words() или как угодно.

1 голос
/ 10 ноября 2019

Вы хотите поручить (кивнуть цепочке ответственности) что-то, что знает о его собственной культуре. Поэтому используйте или создайте конструкцию типа Country или CultureInfo, как упомянуто выше в других ответах.

Но, в общем и целом, ваша проблема заключается в том, что вы берете процедурные конструкции, такие как «процессор», и применяете их к ОО. ОО представляет собой представление концепций реального мира из сферы бизнеса или проблем в программном обеспечении. Процессор не переводит ничего в реальный мир, кроме самого программного обеспечения. Всякий раз, когда у вас есть классы, такие как «Процессор», «Менеджер» или «Управляющий», должны прозвучать сигналы тревоги.

0 голосов
/ 14 ноября 2019

Мне кажется, что информация о странах должна храниться в данных, а не в коде. Таким образом, вместо класса CountryInfo или словаря CapitalisationApplicableCountries у вас может быть база данных с записью для каждой страны и поле для каждого шага обработки, а затем обработка может проходить через поля для данной страны и обрабатываться соответствующим образом. В этом случае обслуживание ведется главным образом в базе данных, а новый код необходим только тогда, когда требуются новые шаги, а данные могут быть удобочитаемыми в базе данных. Это предполагает, что шаги независимы и не мешают друг другу;если это не так, все сложно.

0 голосов
/ 14 ноября 2019

Мне очень жаль, что я давно ввел термин "объекты" для этой темы, потому что он заставляет многих сосредоточиться на меньшей идее. Большая идея - обмен сообщениями .

~ Алан Кей, В обмене сообщениями

Я бы просто внедрил подпрограммы Capitalise, RemovePunctuation и т. д. как подпроцессы, которые могут передаваться с параметрами text и country и возвращать обработанный текст.

Использовать словари для группировки стран, которые соответствуют определенному атрибуту (если вы предпочитаете списки, тобудет работать так же, только с небольшими затратами производительности). Например: CapitalisationApplicableCountries и 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 голосов
/ 08 ноября 2019

Мне было интересно, существует ли модель, которая бы помогла с этим типом процесса

Цепочка ответственности это то, чем вы можете бытьискать, но в ООП это несколько громоздко ...

А как насчет более функционального подхода с 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;
      }
    }
  }
}

ПРИМЕЧАНИЕ. Конечно, он не обязательно должен быть статичным. Если класс Process нуждается в состоянии, вы можете использовать экземплярный класс или частично примененную функцию;).

При запуске вы можете построить процесс для каждой страны, сохранить каждый в индексированной коллекции и извлекать их при необходимости с помощью O (1) стоимость.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...