Як сперечатися з цією «повністю публічною» умовою дизайну класу бізнес-об’єктів


9

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

Приклад класу, який я не шанувальник:

public class Foo
{
    private string field1;
    private string field2;
    private string field3;
    private string field4;
    private string field5;

    public Foo() { }

    public Foo(string in1, string in2)
    {
        field1 = in1;
        field2 = in2;
    }

    public Foo(string in1, string in2, string in3, string in4)
    {
        field1 = in1;
        field2 = in2;
        field3 = in3;
    }

    public Prop1
    { get { return field1; } }
    { set { field1 = value; } }

    public Prop2
    { get { return field2; } }
    { set { field2 = value; } }

    public Prop3
    { get { return field3; } }
    { set { field3 = value; } }

    public Prop4
    { get { return field4; } }
    { set { field4 = value; } }

    public Prop5
    { get { return field5; } }
    { set { field5 = value; } }

}

У "реальному" класі вони не всі рядки, але в деяких випадках у нас є 30 резервних полів для повністю загальнодоступних властивостей.

Я ненавиджу цей клас, і не знаю, чи я просто прискіплива. Кілька речей:

  • Приватні резервні поля, що не мають логіки у властивостях, здаються непотрібними та роздувають клас
  • Кілька конструкторів (дещо нормально), але в поєднанні з
  • всі властивості, які мають публічний сетер, я не шанувальник.
  • Потенційно жодним властивостям не буде призначено значення через порожній конструктор, якщо абонент не знає, ви потенційно можете отримати дуже небажані та важкі для перевірки поведінку.
  • Це занадто багато властивостей! (у 30 випадках)

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

Мені просто не подобається та / або божевільний, що хочу класу, який "простий у використанні", на відміну від "простого для написання (все загальнодоступне)"? Такі класи, як вище, кричать на мене, я не знаю, як це використовувати, тому я просто збираюся оприлюднити все на всякий випадок .

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



3
Об'єкт завжди повинен бути у дійсному стані; тобто вам не потрібно мати частково об'єктивовані об'єкти . Деяка інформація не є частиною основного стану об'єкта, тоді як інша інформація (наприклад, таблиця повинна мати ніжки, але полотно необов’язково); необов'язкова інформація може бути надана після інстанції, основна інформація повинна бути надана при створенні. Якщо якась інформація є основною, але вона не відома при створенні даних, то я б сказав, що класи потребують рефакторингу. Складно сказати більше, не знаючи контексту класу.
Марвін

1
Говорячи, що "ми можемо не мати необхідної інформації, щоб встановити Prop5 під час будівництва об'єкта", змушує мене запитати "ти кажеш, що буде інколи, що ти матимеш цю інформацію під час будівництва та в інші часи, коли ти не будеш маєте цю інформацію? " Мене змушує замислитися, чи є насправді дві різні речі, які цей один клас намагається представляти, чи, можливо, цей клас слід розділити на менші класи. Але якщо це робиться «про всяк випадок», це теж погано, ІМО; це говорить мені, що не існує чіткого дизайну для того, як об’єкти створюються / заселяються.
Учень доктора Вілі

3
Чи є для приватних полів резервного копіювання властивості, що не мають логіки у властивостях ?
Carson63000

2
@Kritner вся суть автоматичних властивостей полягає в тому, що якщо вам в майбутньому вам потрібна фактична логіка, ви можете легко додавати її замість авто getабо set:-)
Carson63000

Відповіді:


10

Повністю публічні класи мають виправдання для певних ситуацій, як і для інших крайніх, класів лише з одним публічним методом (і, мабуть, безліччю приватних методів). І заняття з деякими публічними, а також з приватними методами.

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

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

Щоб вирішити інші питання:

  • "приватні резервні поля без властивостей логіки": Так, ви праві, для тривіальних геттерів та сеттерів це просто непотрібний "шум". Щоб уникнути такого типу "роздуття", у C # є скорочений синтаксис методів get / set властивостей:

Тож замість

   private string field1;
   public string Prop1
   { get { return field1; } }
   { set { field1 = value; } }

писати

   public string Prop1 { get;set;}

або

   public string Prop1 { get;private set;}
  • "Кілька конструкторів": це сама по собі не проблема. Проблема виникає, коли там є непотрібне дублювання коду, як показано у вашому прикладі, або якщо ієрархія виклику зведена. Це можна легко вирішити, переробляючи загальні частини в окрему функцію та організуючи ланцюг конструктора однонаправленим способом

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

  • "Це занадто багато властивостей! (У 30 випадку)": так, якщо ви вільні спроектувати такий клас в "зеленому полі", 30 - це занадто багато, я згоден. Однак не у кожного з нас є така розкіш (ти не писав у коментарі нижче, що це спадщина?). Іноді доводиться зіставляти записи з існуючої бази даних, файлу чи даних з сторонніх API в вашу систему. Тому для цих випадків 30 атрибутів можуть бути чимось, з чим треба жити.


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

@Kritner: дивіться мою редакцію
Doc Brown

Так, зазвичай, коли я створюю абсолютно новий клас, я йду шляхом власності авто. Наявність приватних резервних полів "просто тому" є (на мій погляд) зайвим і навіть викликає проблеми. Коли ви маєте ~ 30 властивостей для класу і 100+ класів, не так вже й складно сказати, що буде якесь налаштування властивостей або потрапляння з неправильного поля резервного копіювання ... Я вже кілька разів стикався з цим у нашому рефакторингу, насправді :)
Крітнер

дякую, значення за замовчуванням для рядка nullце не так? тож якщо одна з властивостей, яка насправді використовується в a .ToUpper(), але значення для неї ніколи не встановлюється, це призведе до винятку виконання. Це ще один хороший приклад , чому необхідні дані для класу повинні мати бути встановлені під час будівництва об'єкта. Не просто залишайте це користувачеві. ЗАВДЯКИ
Kritner

Крім занадто багато невстановлених властивостей, основна проблема цього класу полягає в тому, що він має нульову логіку (ZRP) і є архетипом для анемічної моделі. Без потреби в цьому, що можна виразити словами, такими як DTO, це погана конструкція.
user949300

2
  1. Сказавши, що "Приватні резервні поля без властивостей логіки у властивостях здаються непотрібними та роздувають клас", у вас уже є гідний аргумент.

  2. Здається, тут не проблема багатьох конструкторів.

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

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

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


1
Дякую - №1 та №4, я, безумовно, відчуваю себе найзручніше, коли висловлюю свої аргументи. Не зовсім впевнений, що ви маєте на увазі під №3. # 5 Більшість властивостей класів складають первинний ключ на db. Ми використовуємо конкурентні ключі, іноді до 8 стовпців - але це вже інше питання. Я думав над тим, щоб спробувати вкласти частини, що складають ключ, у свій клас / структуру, що дозволило б усунути безліч властивостей (принаймні в цьому класі) - але це застаріла програма з тисячами рядків коду з декількома абоненти. Тож я думаю, що мені багато про що подумати :)
Критнер

Е-е. Якщо клас був незмінним, не було б причини писати код синхронізації всередині класу.
RubberDuck

@RubberDuck Чи цей клас незмінний? Що ви маєте на увазі?
Снуп

3
Це точно не є незмінним @StevieV. Будь-який клас із налаштуваннями властивостей змінюється.
RubberDuck

1
@Kritner Як властивості використовуються як основні ключі? Це, ймовірно, вимагає певної логіки (нульові перевірки) або правил (наприклад, NAME має пріоритет над AGE). За даними OOP, цей код належить до цього класу. Чи можете ви використовувати це як аргумент?
user949300

2

Як ви мені сказали, Fooце бізнес-об’єкт. Він повинен інкапсулювати деяку поведінку в методах відповідно до вашого домену, і його дані повинні залишатися максимально капсульованими.

У прикладі, який ви показуєте, він Fooбільше нагадує DTO і суперечить усім принципам OOP (див. Анемічну модель домену )!

Як покращення я б запропонував:

  • Зробіть цей клас максимально незмінним . Як ви вже говорили, ви хочете зробити тестування на одиницю. Однією з обов'язкових можливостей одиничного тестування є детермінізм , і детермінізм може бути досягнутий через незмінність, оскільки він вирішує проблеми побічних ефектів .
  • Розбийте цей клас богів на кілька класів, які роблять по 1 штуці кожен (див. SRP ). Тест одиниці 30 атрибутів класу в дійсно PITA .
  • Видаліть надмірність конструктора, маючи лише 1 головний конструктор, а інші викликають головний конструктор.
  • Видаліть непотрібні геттери, я серйозно сумніваюся, що всі геттери дійсно корисні.
  • Поверніть ділову логіку в бізнес-класах, саме тут вона належить!

Це може бути потенційний ремонтний клас із цими зауваженнями:

public sealed class Foo
{
    private readonly string field1;
    private readonly string field2;
    private readonly string field3;
    private readonly string field4;

    public Foo() : this("default1", "default2")
    { }

    public Foo(string in1, string in2) : this(in1, in2, "default3", "default4")
    { }

    public Foo(string in1, string in2, string in3, string in4)
    {
        field1 = in1;
        field2 = in2;
        field3 = in3;
        field4 = in4;
    }

    //Methods with business logic

    //Really needed ?
    public Prop1
    { get; }

    //Really needed ?
    public Prop2
    { get; }

    //Really needed ?
    public Prop3
    { get; }

    //Really needed ?
    public Prop4
    { get; }

    //Really needed ?
    public Prop5
    { get; }
}

2

Як сперечатися з цією «повністю публічною» умовою дизайну класу бізнес-об’єктів

  • "Існує кілька методів, розкиданих у класі клієнтів, які виконують більше, ніж" просто DTO ". Вони належать разом."
  • "Це може бути" DTO ", але він має ідентичність бізнесу - нам потрібно перемогти рівних."
  • "Нам потрібно сортувати це - нам потрібно реалізувати IComparable"
  • "Ми поєднуємо декілька цих властивостей. Давайте переосмислимо ToString, щоб кожен клієнт не повинен писати цей код."
  • "У нас є декілька клієнтів, які роблять те саме, що і для цього класу. Нам потрібно НАДУВИТИ код".
  • "Клієнт маніпулює колекцією цих речей. Очевидно, що це повинен бути спеціальний клас колекції; і багато з попередніх пунктів зроблять цю власну колекцію більш функціональною, ніж зараз".
  • "Подивіться на всі виснажливі, викриті, жорсткі маніпуляції! Інкапсулюючи, що у відповідних для бізнесу методах підвищиться наша продуктивність кодування".
  • "Якщо об'єднати ці методи разом у клас, це зробить їх перевірятими, оскільки нам не доведеться мати справу з більш складним класом клієнтів."
  • "Тепер ми можемо перевірити ці способи відновлення. Так як це є, з причин x, y, z клієнт не перевіряється.

  • Наскільки будь-який із наведених аргументів може бути викладений у сукупності:

    • Функціональність стає придатними для повторного використання.
    • СУХО
    • Його відключають від клієнтів.
    • кодування проти класу швидше і менш схильне до помилок.
  • "Добре зроблений клас приховує стан і розкриває функціональність. Це початкова пропозиція для проектування класів ОО".
  • "Кінцевий результат виконує набагато кращу роботу щодо вираження ділової сфери; різних організацій та їх явної взаємодії".
  • "Цей" накладний "функціонал (тобто не чіткі функціональні вимоги, але це потрібно зробити) чітко виділяється і дотримується єдиного принципу відповідальності.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.