Enum-switch антипаттерн

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

Як виглядає реалізація даного шаблону, і чим він небезпечний? Давайте розберемося.

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

Для зберігання списку підтримуваних мов створюється перерахування

enum Language
public enum Language
{
Java,
CSharp,
TSQL
}


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

GetExtensions(Language lang)
List < string> GetExtensions(Language lang)
{
switch (lang)
{
case Language.Java:
{
List<string> result = new List < string>();
result.Add("java");
return result;
}
case Language.CSharp:
{
List<string> result = new List < string>();
result.Add("cs");
return result;
}
case Language.TSQL:
{
List<String> result = new List < string>();
result.Add("sql");
return result;
}
default:
throw new InvalidOperationException("Мова " + lang + " не підтримується");
}
}


IsCaseSensitive(Language lang)
bool IsCaseSensitive(Language lang)
{
switch (lang)
{
case Language.Java:
case Language.CSharp:
return true;
case Language.TSQL:
return false;
default:
throw new InvalidOperationException("Мова " + lang + " не підтримується");
}
}


GetIconFile(Language lang)
string GetIconFile(Language lang)
{
switch (lang)
{
case Language.Java:
return "bean.svg";
case Language.CSharp:
return "cs.svg";
case Language.TSQL:
return "tsql.svg";
default:
throw new InvalidOperationException("Мова " + lang + " не підтримується");
}
}


Викидати виняток в кінці змушує компілятор, яким потрібно забезпечити гарантований результат кожної функції, а проконтролювати повноту покриття множини значень оператором switch компілятор C# не може. І якщо іконку для будь-якого нової мови програмування можна заздалегідь придумати, то визначити, чи невідомий мову регистрозависимым, і яке розширення буде біля його джерел, заздалегідь не можна, тому викидається виняток.

В результаті спочатку виникає досить проста картина. Всі підтримувані мови зібрані в одне місце. Там, де треба визначити що-те, що залежить від мови, вставляється switch. Одному розробнику реалізувати підтримку 2-3 мов одночасно дуже легко. Але от згодом з підтримкою і розвитком програми, заснованої на цьому шаблоні, будуть серйозні проблеми.

Недоліки використання enum-switch
Справа в тому, що такий підхід створює god-об'єкти (god object). Сам enum і кожен switch відіграють роль god-об'єктів. Будь-яка зміна, пов'язане з одним з підтримуваних редактором мов програмування, що вимагає внесення зміни в усі god-об'єкти. Працюючи над підтримкою Java, можна зламати код, що відноситься до C# або TransactSQL.

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

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

За допомогою підходу enum-switch розробники поєднує жорсткими зв'язками сутності, які в реальності між собою практично не пов'язані. Між TransactSQL і Java взагалі може не бути нічого спільного крім того, що хтось захотів відкрити їх в одному текстовому редакторі. Але в коді програми TransactSQL і Java опинилися в одному типі enum.

Це прояв антипаттерна god-object.

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

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

Оператор switch часто визначає зв'язок між сутностями з різних множин. У нашому прикладі це зв'язок між мовою програмування і іконкою. Однак зв'язок між сутностями — це теж сутність, і з нею треба звертатися як з усіма іншими даними, наприклад, зберегти у стовпці таблиці. Якщо ж немає можливості використовувати зовнішнє сховище, то хоча б записати зв'язок у Dictionary.

Словник
Dictionary<Language, string> icons = new Dictionary<Language, string>();
icons[Language.Java] = "bean.svg";
icons[Language.CSharp] = "cs.svg";
icons[Language.TSQL] = "tsql.svg";


При цьому в оператора switch є ще один неприємний побічний ефект. Він не тільки визначає зв'язок між об'єктами, але і сам є зв'язком. Щоб було зрозуміло, про що мова, розглянемо такий приклад:

switch (lang)
{
case Language.TSQL:
case Language.PLSQL:
return "sql.svg";
...
}

Двом діалектів SQL поставлена у відповідність іконка sql.svg. Тепер у мови не тільки є іконка, але і є неявне властивість, означає, що у мов TransactSQL і PL-SQL кахлі повинні бути однаковими. Розробник, який захоче поміняти іконку для PL-SQL буде вирішувати питання, чи варто йому змінювати іконку і для TransactSQL. У більшості випадків це небажано.

І нарешті, антипаттерн enum-switch сприяє прояву помилки типу «Дане значення з enum не передбачено», тому що складно проконтролювати при додаванні нового значення в enum повне покриття всіх операторів switch.

Вихід
Як же слід діяти, щоб уникнути використання даного шаблону?

У будь незрозумілої ситуації заводите інтерфейс. Щоб не використовувати enum, заведіть інтерфейс. Інтерфейс повинен повертати інформацію про властивості описуваного об'єкта з безлічі. Додайте в цей інтерфейс ім'я, яке раніше зберігалося в константі в enum.

Інтерфейс
interface Language
{
string GetName();
bool IsCaseSensitive();
string GetIconFile();
List<string> GetExtensions();
}


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

Провайдер
class LanguageProvider
{
List<Language> GetSupportedLanguages() {
...
}
Language DetectLanguageByFile(string fileName) {
...
}
Language GetDefaultLanguage() {
....
}
}


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

Тепер всі функції, що містять switch, якщо вони у вас були, видаліть. Вони вам більше не знадобляться, тому що код обробляє не конкретні об'єкти, а їх властивості.

У наведеному прикладі, після того як текстовий редактор буде підтримувати 10-15 різних мов програмування, додавання ще однієї мови буде зводитися до перерахування налаштувань зі списку вже реалізованих раніше. Адже, хоча мов програмування існує дуже багато, більшість нюансів, що впливають на редагування вихідного, у них загальні.

Навіщо потрібен enum
Навіщо ж все-таки в більшості мов програмування існує такий тип як enum?

Використовувати його зручно, якщо робити це з певною обережністю. Насамперед enum можна застосувати там, де кількість об'єктів невелика. Допустимий межа кожен розробник визначає на свій розсуд. Я б не став об'єднувати в enum більше 20 констант.

Описуване безліч повинно складатися з об'єктів, відмінності між якими можуть бути параметризовані. Наприклад, дні тижня відрізняються один від одного тільки порядковим номером, тому вони добре описуються через enum. А ось які-небудь погодні явища перераховувати в enum-е швидше не варто, тому що у них дуже мало спільного.

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

Характерні приклади застосування enum:

  • enum Boolean {True, False}
  • дні тижня, місяці
  • стану кінцевого автомата
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.