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

64

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

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

Итак, у меня есть класс, давайте назовем его 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 классов со слегка отличающейся логикой также чувствуют себя неуклюже.

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

Джон Дарвилл
источник
Похоже, у вас есть PreProcessфункциональность, которая может быть реализована по-разному в зависимости от некоторых стран, DetermineSeparatorможет быть доступна для всех из них, и a PostProcess. Все они могут быть protected virtual voidс реализацией по умолчанию, а затем вы можете иметь конкретную для Processorsкаждой страны
Icepickle
Ваша задача состоит в том, чтобы в определенный период времени сделать то, что работает и может быть поддержано в обозримом будущем вами или кем-то еще. Если несколько вариантов могут удовлетворить оба условия, вы можете выбрать любой из них в соответствии с вашими предпочтениями.
Диалектик
2
Жизнеспособный вариант для вас - иметь конфигурацию. Таким образом, в вашем коде вы проверяете не конкретную страну, а конкретный вариант конфигурации. Но каждая страна будет иметь определенный набор этих параметров конфигурации. Например, вместо if (country == "DEU")вас проверить if (config.ShouldRemovePunctuation).
Диалектик
11
Если страны имеют различные варианты, почему строка , а не экземпляр класса , который моделирует эти варианты? country
Damien_The_Unbeliever
@Damien_The_Unbeliever - не могли бы вы подробнее остановиться на этом? Соответствует ли ответ Роберта Браутигама ниже тому, что вы предлагаете? - Ах, теперь вижу твой ответ, спасибо!
Джон Дарвилл

Ответы:

53

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

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

и передать его в 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);
}
Михал Турчин
источник
4
Не уверен, почему что-то подобное не было испробовано, прежде чем прыгнуть на CountrySpecificHandlerFactory... o_0
Матеин Улхак,
Пока нет слишком специализированных вариантов, я бы определенно пошел по этому пути. Если параметры сериализуются в текстовый файл, это также позволяет непрограммистам определять новые варианты / обновлять существующие без необходимости изменения в приложении.
Том
4
Это public class ProcessOptionsдействительно должно быть просто [Flags] enum class ProcessOptions : int { ... }...
Пьяный Код Обезьяны
И я думаю, что если они нуждаются, у них может быть карта стран ProcessOptions. Очень удобно.
theonlygusti
24

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

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

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

public string Process(CountryInfo country, string text)

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

Damien_The_Unbeliever
источник
13

Может быть, вы могли бы иметь один 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метод ничего не возвращает.

Корентин Пан
источник
Я думаю, что я пытался следовать за композицией по мантре наследования. Но да, это определенно вариант, спасибо за ответ.
Джон Дарвилл
Справедливо. Я думаю, что наследование оправдано в некоторых случаях, но оно действительно зависит от того, как вы планируете загружать / хранить / вызывать / изменять свои методы и обработку в конце.
Корентин Пейн
3
Иногда наследование является правильным инструментом для работы. Если у вас есть процесс, который будет вести себя в основном одинаково в нескольких разных ситуациях, но также имеет несколько частей, которые будут вести себя по-разному в разных ситуациях, это хороший признак, вы должны рассмотреть использование наследования.
Таннер Светт
5

Вы можете создать общий интерфейс с 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;

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

Восстановить Монику Челлио
источник
Разве отражение не слишком медленное по сравнению с отсутствием отраженного кода? стоит ли это в этом случае?
Jlvaquero
@jlvaquero Нет, размышление не удивительно медленное. Конечно, производительность снижается из-за указания типа во время разработки, но на самом деле это незначительная разница в производительности, и она заметна только при чрезмерном использовании. Я внедрил большие системы обмена сообщениями, основанные на общей обработке объектов, и у нас не было причин ставить под сомнение производительность, и это с огромной пропускной способностью. Без заметной разницы в производительности, я всегда буду идти с простым в обслуживании кодом, как этот.
Восстановите Монику
Если вы размышляете, не хотите ли вы удалять строку страны из каждого вызова Processи вместо этого использовать ее один раз, чтобы получить правильный IP-процессор? Обычно вы обрабатываете много текста в соответствии с правилами той же страны.
Дэвислор
@Davislor Это именно то, что делает этот код. При вызове Process("GBR", "text");он выполняет статический метод, который создает экземпляр процессора GBR и выполняет метод Process для этого. Он выполняет его только в одном экземпляре для данного типа страны.
Восстановить Монику
@Archer Правильно, поэтому в типичном случае, когда вы обрабатываете несколько строк в соответствии с правилами для одной и той же страны, было бы эффективнее создать экземпляр один раз или найти постоянный экземпляр в хэш-таблице / словаре и вернуть ссылка на это. Затем вы можете вызвать преобразование текста в том же экземпляре. Создание нового экземпляра для каждого вызова, а затем его отбрасывание, а не повторное использование для каждого вызова, расточительно.
Дэвислор
3

Несколько версий назад, в 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

Возможно, я бы (в зависимости от деталей вашего варианта использования) выбрал 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
Я написал что-то более объемное, но я думаю, что это в основном то, что мне нравится больше всего. Если сценарий использования должен искать эти объекты на основе строки страны, он может использовать решение Кристофера с этим. Реализация интерфейсов могла бы даже быть классом, экземпляры которого устанавливают черты, как в ответе Михала, для оптимизации для пространства, а не времени.
Дэвислор
1

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

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

Фрэнк
источник
0

Мне было интересно, есть ли шаблон, который поможет с этим типом процесса

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

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

jlvaquero
источник
0

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

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

Я бы просто реализовать подпрограммы 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

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

Стив Дж
источник