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


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 класів зі злегка різною логікою також почуваються незграбними.

В основному я думаю, що я потрапив у якийсь вузол OOP і не зовсім знаю хорошого способу розплутати його. Мені було цікаво, чи існує там схема, яка допомогла б цьому процесу?


Схоже, у вас є 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
Mateen Ulhaq

Поки немає занадто спеціалізованих варіантів, я б точно пішов цим шляхом. Якщо параметри серіалізуються у текстовий файл, він також дозволяє непрограмістам визначати нові варіанти / оновлювати існуючі без необхідності змінювати додаток.
Том

4
Це public class ProcessOptionsнасправді просто повинно бути [Flags] enum class ProcessOptions : int { ... }...
П'яна

І я думаю, якщо вони знадобляться, вони можуть мати карту країн ProcessOptions. Дуже зручно.
theonlygusti

24

Коли .NET Framework ставила собі справу з подібними проблемами, вона не моделювала все як string. Отже, у вас, наприклад, CultureInfoклас :

Забезпечує інформацію про певну культуру (так звану локаль для розробки керованого коду). Інформація включає назви культури, системи запису, використовуваного календаря, порядку сортування рядків та форматування для дат і чисел.

Тепер цей клас може не містити конкретних функцій, які вам потрібні, але ви, очевидно, можете створити щось аналогічне. А потім ви змінюєте свій Processметод:

public string Process(CountryInfo country, string text)

CountryInfoТоді ваш клас може мати bool RequiresCapitalizationвластивість тощо, що допомагає вашому Processметоду належним чином направити його обробку.


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метод нічого не повертає.


Я здогадуюсь, я намагався слідувати композиції над мантрою спадкування. Але так, це безумовно варіант, дякую за відповідь.
Джон Дарвілл

Справедливо. Я думаю, що спадкування виправдане в деяких випадках, але це дійсно залежить від того, як ви плануєте завантажувати / зберігати / викликати / змінювати свої методи та обробку в підсумку.
Корантен Pane

3
Іноді успадкування є правильним інструментом для роботи. Якщо у вас є процес, який буде вести себе в основному однаково в декількох різних ситуаціях, але також має кілька частин, які будуть вести себе по-різному в різних ситуаціях, це хороший знак, який слід розглянути, як використовувати спадщину.
Tanner Swett

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

Ось приклад робочої точкової скрипки ...

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

Примітка: Вам потрібно буде додати ...

using System.Assembly;

для того, щоб статичний метод створив екземпляр класу країни.


Чи не відображення надзвичайно повільне порівняно з відсутнім відображеним кодом? чи варто цього випадку?
jlvaquero

@jlvaquero Ні, рефлексія зовсім не надзвичайно повільна. Звичайно, є враження щодо продуктивності щодо визначення типу на час проектування, але це дійсно незначна різниця в продуктивності і помітна лише при надмірному використанні. Я впровадив великі системи обміну повідомленнями, побудовані навколо загальної обробки об'єктів, і у нас не було жодних підстав сумніватися в продуктивності, і це з величезною кількістю пропускної здатності. Без помітної різниці в продуктивності я завжди буду простий у підтримці коду, як це.
Відновіть Моніку Селліо

Якщо ви розмірковуєте, чи не хочете ви видаляти рядок країни з кожного дзвінка Process, а замість цього використати його один раз, щоб отримати правильний IProcessor? Зазвичай ви обробляєте багато тексту відповідно до правил тієї самої країни.
Девіслор

@Davislor Саме цим і займається цей код. При виклику Process("GBR", "text");він виконує статичний метод, який створює екземпляр процесора GBR і виконує метод Process на цьому. Він виконує лише один екземпляр для конкретного типу країни.
Відновіть Моніку Селліо

@Archer Right, тому в типовому випадку, коли ви обробляєте кілька рядків відповідно до правил для однієї країни, було б ефективніше створити екземпляр один раз - або шукати постійний екземпляр у хеш-таблиці / словнику та повертати посилання на це. Потім можна викликати перетворення тексту в тому ж екземплярі. Створення нового примірника для кожного дзвінка та його відкидання, а не повторне використання для кожного дзвінка, є марним.
Девіслор

3

Кілька версій тому було подано C # swtich повну підтримку відповідності зразком . Так що справу "збігаються декілька країн" легко зробити. Незважаючи на те, що він все ще не здатний до падіння здатності, один вхід може співставити декілька випадків зі збігом шаблонів. Можливо, це зробить цей спам трохи зрозумілішим.

Npw вимикач зазвичай можна замінити колекцією. Вам потрібно використовувати Делегати та словник. Процес можна замінити на.

public delegate string ProcessDelegate(string text);

Тоді ви можете скласти словник:

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

Я використовував functionNames для передачі в Делегат. Але ви можете використовувати синтаксис Lambda, щоб забезпечити там весь код. Таким чином ви могли просто приховати всю цю колекцію, як і будь-яку іншу велику колекцію. І код стає простим пошуку:

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, як зазначено вище в інших відповідях.

Але загалом і принципово вашою проблемою є те, що ви приймаєте такі процедурні конструкції, як "процесор", і застосовуєте їх до ОО. OO - це представлення реальних понять із бізнесу чи проблемної області в програмному забезпеченні. Процесор не перекладає нічого в реальному світі, крім самого програмного забезпечення. Щоразу, коли у вас є такі заняття, як "Процесор", "Менеджер" або "Губернатор", дзвони тривоги повинні дзвонити


0

Мені було цікаво, чи існує там схема, яка допомогла б цьому процесу

Мережа відповідальності - це те, що ви можете шукати, але в OOP дещо громіздкий ...

А як щодо більш функціонального підходу з 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 Class, ви можете використовувати інстальований клас або частково застосовану функцію;).

Ви можете створити Процес для кожної країни під час запуску, зберігати кожну у індексованій колекції та отримувати їх у разі необхідності із витратами O (1).


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 або словника CapitalisationAppvableCountries ви можете мати базу даних із записом для кожної країни та полем для кожного етапу обробки, а потім обробка могла б пройти через поля для даної країни та обробити відповідно. Тоді обслуговування ведеться в основному в базі даних, при цьому новий код необхідний лише тоді, коли потрібні нові кроки, і дані можуть бути зрозумілими для людини в базі даних. Це передбачає, що кроки є незалежними та не заважають один одному; якщо це не так, справи складні.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.