Візерунок для класу, який робить лише одне


24

Скажімо, у мене є процедура, яка виконує завдання :

void doStuff(initalParams) {
    ...
}

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

class StuffDoer {
    private someInternalState;

    public Start(initalParams) {
        ...
    }

    // some private helper procedures here
    ...
}

І тоді я називаю це так:

new StuffDoer().Start(initialParams);

або так:

new StuffDoer(initialParams).Start();

І це те, що відчуває себе неправильно. Під час використання .NET або Java API я ніколи не дзвоню new SomeApiClass().Start(...);, через що я підозрюю, що я роблю це неправильно. Звичайно, я можу зробити конструктор StuffDoer приватним і додати статичний помічник:

public static DoStuff(initalParams) {
    new StuffDoer().Start(initialParams);
}

Але тоді я мав би клас, зовнішній інтерфейс якого складається лише з одного статичного методу, який також виглядає дивно.

Звідси моє запитання: чи існує чітко встановлена ​​закономірність для цього типу класів, яка

  • мають лише одну точку входу та
  • не мають "зовнішньо впізнаваного" стану, тобто стан екземпляра потрібен лише під час виконання цієї однієї точки входу?

Мені відомо, що я робив такі речі, як bool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");коли мені було все байдуже - це конкретна інформація, а методи розширення LINQ недоступні. Добре працює і вписується всередину if()умови, не потрібно стрибати через обручі. Звичайно, ви хочете зібраний сміттям мову, якщо ви пишете такий код.
CVn

Якщо це мова-агностик , навіщо також додавати java та c # ? :)
Стівен Євріс

Мені цікаво, яка функціональність цього «одного методу»? Це дуже допомогло б визначити, який найкращий підхід у конкретному сценарії, але все ж цікаве питання!
Стівен Євріс

@StevenJeuris: Я натрапив на це питання в різних ситуаціях, але в поточному випадку це метод, який імпортує дані в локальну базу даних (завантажує їх з веб-сервера, робить деякі маніпуляції та зберігає їх у базі даних).
Хайнзі

1
Якщо ви завжди викликаєте метод під час створення екземпляра, чому б не зробити його логікою ініціалізації всередині конструктора?
NoChance

Відповіді:


29

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

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


1
Це звучить точно так, як я роблю. Дякую, принаймні у мене зараз це ім’я. :-)
Хайнці

2
Дуже відповідне посилання! Це пахне як анти-візерунок для мене. Якщо ваш метод такий довгий, можливо, його частини можна витягнути в класах багаторазового використання або взагалі краще відремонтувати краще? Я також раніше не чув про цю закономірність, а також не можу знайти багато інших ресурсів, що змушує мене повірити, що вона, як правило, не так багато використовується.
Стівен Євріс


1
@StevenJeuris Я не думаю, що це анти-модель. Антидіаграфом було б захаращувати відновлені методи з параметрами партії замість того, щоб зберігати цей стан, який є загальним між цими методами, у змінній примірника.
Альфредо Озоріо

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

13

Я б сказав, що розглянутий клас (використовуючи єдину точку входу) добре розроблений. Він простий у використанні та розширенні. Досить твердий.

Те саме для no "externally recognizable" state . Це добре капсулювання.

Я не бачу жодної проблеми з кодом, як-от:

var doer = new StuffDoer(initialParams);
var result = doer.Calculate(extraParams);

1
І звичайно, якщо вам більше не потрібно використовувати те саме doer, це зводиться до var result = new StuffDoer(initialParams).Calculate(extraParams);.
CVn

Розрахунок повинен бути перейменований на doStuff!
shabunc

1
Я додам, що якщо ви не хочете ініціалізувати (новий) свій клас там, де ви його використовуєте (можливо, ви хочете скористатися Factory), у вас є можливість створити об'єкт десь у вашій бізнес-логіці та передати параметри безпосередньо до єдиного загальнодоступного методу. Інший варіант - використовувати Factory і мати на ньому метод make, який отримує параметри і створює об'єкт з усіма параметрами через його конструктор. Тоді як ви називаєте свій публічний метод без парами звідки завгодно.
Паткос Чаба

2
Ця відповідь надмірно спрощена. Чи StringComparerклас, який ви використовуєте, new StringComparer().Compare( "bleh", "blah" )добре розроблений? Що з створення держави, коли цілком немає потреби? Гаразд, поведінка добре інкапсульована (ну, принаймні, користувачеві класу, докладніше про це у моїй відповіді ), але це не робить його «гарним дизайном» за замовчуванням. Що стосується вашого прикладу "виконати результат". Це має сенс лише в тому випадку, якщо ви коли-небудь використовуватимете doerокремо від result.
Стівен Євріс

1
Це не одна причина існування, це one reason to change. Різниця може здатися тонкою. Ви повинні зрозуміти, що це означає. Він ніколи не створить одного класу методів
jgauffin

8

З точки зору принципів SOLID, відповідь jgauffin має сенс. Однак ви не повинні забувати про загальні принципи дизайну, наприклад приховування інформації .

Я бачу кілька проблем із даним підходом:

  • Як ви самі зазначали, люди не очікують використання "нового" ключового слова при створенні об'єкта не керує жодним станом . Ваш дизайн відображає його намір. Люди, які використовують ваш клас, можуть заплутатися в тому, який стан він управляє, і чи можуть наступні виклики методу спричинити різну поведінку.
  • З точки зору людини, яка використовує клас, внутрішній стан добре прихований, але, бажаючи внести зміни в клас або просто зрозуміти його, ви робите речі більш складними. Я вже багато писав про проблеми, які я бачу з методами розщеплення, щоб зменшити їх менше , особливо при переході стану до області класу. Ви змінюєте спосіб використання вашого API саме для того, щоб мати менші функції! Це, на мій погляд, безумовно, забирає це занадто далеко.

Деякі пов'язані посилання

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

Ще одна відповідна посилання, що стосується цієї теми: "Розділіть свої програми на методи, що виконують одне ідентифіковане завдання. Зберігайте всі операції методу на одному рівні абстракції ." - Кент Бек Клю тут "той самий рівень абстракції". Це не означає "одне", як це часто трактують. Цей рівень абстракції повністю відповідає контексту, для якого ви проектуєте.

То який правильний підхід?

Не знаючи вашого конкретного випадку використання, важко сказати. Існує сценарій, в якому я іноді (не часто) використовую подібний підхід. Коли я хочу обробити набір даних, не бажаючи робити цю функціональність доступною для всього діапазону класу. Я писав про це в блозі, як лямбдаси можуть ще більше покращити інкапсуляцію . Я також почав запитання на цю тему про програмістів . Далі - останній приклад того, де я використовував цю техніку.

new TupleList<Key, int>
{
    { Key.NumPad1, 1 },
            ...
    { Key.NumPad3, 16 },
    { Key.NumPad4, 17 },
}
    .ForEach( t =>
    {
        var trigger = new IC.Trigger.EventTrigger(
                        new KeyInputCondition( t.Item1, KeyInputCondition.KeyState.Down ) );
        trigger.ConditionsMet += () => AddMarker( t.Item2 );
        _inputController.AddTrigger( trigger );
    } );

Оскільки той самий "локальний" код в ForEach не використовується більше ніде, я можу просто зберігати його в тому місці, де він є актуальним. Викладення коду таким чином, що код, який спирається один на одного, сильно згрупований між собою, робить його більш зрозумілим на мою думку.

Можливі альтернативи

  • У C # ви можете використовувати методи розширення. Тож працюйте над аргументом безпосередньо ви переходите до цього методу «одне».
  • Подивіться, чи справді ця функція не належить до іншого класу.
  • Зробіть це статичною функцією в статичному класі . Це, швидше за все, є найбільш підходящим підходом, що також відображається в загальних API, на які ви посилалися.

Я знайшов можливу назву цього анти-шаблону, як викладено в іншій відповіді, Полтергейсти .
Стівен Євріс

4

Я б сказав, що це досить добре, але те, що ваш API все-таки повинен бути статичним методом статичного класу, оскільки це очікує користувач. Те, що статичний метод використовує newдля створення вашого помічника і виконання роботи, - це деталь реалізації, яку слід приховати від того, хто її називає.


Оце Так! Вам вдалося дістатись до суті цього набагато коротше, ніж я. :) Спасибі. (звичайно, я заходжу трохи далі, де ми можемо не погодитися, але я згоден із загальною умовою)
Стівен Євріс

2
new StuffDoer().Start(initialParams);

Виникає проблема з незрозумілими розробниками, які могли використовувати спільний екземпляр і використовувати його

  1. кілька разів (добре, якщо попереднє (можливо, часткове) виконання не зіпсує подальше виконання)
  2. з декількох потоків (не в порядку, ви прямо сказали, що він має внутрішній стан).

Тому для цього потрібна чітка документація про те, що вона не є безпечною для потоків, і якщо вона легка (створюючи її швидко, не пов’язуючи зовнішні ресурси та багато пам’яті), це нормально інстанціювати на льоту.

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

Якщо у нього є якась дорога ініціалізація, може бути корисним підготувати ініціалізацію її один раз та використовувати її кілька разів, створивши інший клас, який міститиме лише стан та зробить його клоніруемим. Ініціалізація створює стан, в якому буде зберігатися doer.start()клонував би його і передав би внутрішнім методам.

Це також дозволило б зробити інші речі, такі як стан часткового виконання. (Якщо потрібні тривалі зовнішні фактори, наприклад, відмова електропостачання, це може перервати виконання.) Але ці додаткові фантазійні речі зазвичай не потрібні, тому не варто.


Хороші бали. Сподіваюся, ви не заперечуєте проти прибирання.
Стівен Євріс

2

Окрім моєї більш детальної, персоналізованої іншої відповіді , я вважаю, що наступне спостереження заслуговує на окрему відповідь.

Є вказівки на те, що ви можете дотримуватися анти-схеми Полтергейстів .

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

Симптоми та наслідки

  • Надлишкові навігаційні шляхи.
  • Тимчасові асоціації.
  • Класи без громадянства.
  • Тимчасові, короткочасні об'єкти та заняття.
  • Класи з одноопераційними операціями, які існують лише для «виведення» або «виклику» інших класів через тимчасові асоціації.
  • Класи з "керуючими" іменами операцій, такими як start_process_alpha.

Реконструйований розчин

Прихильники привидів вирішують Полтергейстів, цілком видаляючи їх з ієрархії класів. Однак після їх видалення функціонал, який "надав" полтергейсту, повинен бути замінений. Це легко за допомогою простого налаштування для виправлення архітектури.


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