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


10

Наприклад, у класі зазвичай є члени та методи класу, наприклад:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public void printInfo(){
        System.out.println("Name:"+this.name+",weight:"+this.weight);
    }

    public void draw(){
        //some draw code which uses this.image
    }
}

Але прочитавши про принцип єдиної відповідальності та відкритий закритий принцип, я віддаю перевагу розділити клас на DTO та helper class лише статичними методами, наприклад:

public class CatData{
    public String name;
    public int weight;
    public Image image;
}

public class CatMethods{
    public static void printInfo(Cat cat){
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat){
        //some draw code which uses cat.image
    }
}

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

Моє запитання: добре це чи анти-модель?


15
Робити що-небудь наосліп скрізь, тому що ви засвоїли нову концепцію, це завжди анти-модель.
Каяман

4
Який би сенс занять, якщо завжди краще мати дані окремо від методів, що їх модифікують? Це звучить як не об’єктно-орієнтоване програмування (що, безумовно, має своє місце). Оскільки це позначено "об'єктно-орієнтованим", я припускаю, що ви хочете використовувати інструменти, які постачає OOP?
користувач1118321

4
Ще одне питання, що підтверджує, що СРП настільки неправильно зрозуміла, що її слід заборонити. Зберігання даних у купі публічних полів не є обов'язком .
user949300

1
@ user949300, якщо клас відповідає за ці поля, то переміщення їх в іншому додатку, безумовно, матиме нульовий вплив. Або кажучи інакше, ви помиляєтесь: вони на 100% є відповідальністю.
Девід Арно

2
@DavidArno та R Schmitz: Зміст полів не вважається відповідальністю для цілей СРП. Якби це було, не може бути OOP, оскільки все було б DTO, і тоді окремі класи міститимуть процедури роботи над даними. Єдина відповідальність покладається на одного зацікавленого учасника . (Хоча це, здається, трохи змінюється за кожну ітерацію головного)
user949300

Відповіді:


10

Ви показали дві крайності ("все приватне та всі (можливо непов'язані) методи в одному об'єкті" проти "все загальнодоступне і жоден метод всередині об'єкта"). ІМХО гарне моделювання ОО - це жодне з них , солодке місце десь посередині.

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

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

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public String getInfo(){
        return "Name:"+this.name+",weight:"+this.weight;
    }
    public Image getImage(){
        return image;
    }
}

Тепер Catоб’єкт забезпечує достатню логіку, щоб навколишній код легко реалізовувався printInfoі draw, не піддаючи публічним атрибутам. Правильне місце для цих двох методів, швидше за все, не є класом богів CatMethods(оскільки printInfoі draw, мабуть, різні питання, тому я думаю, що малоймовірно, що вони належать до одного класу).

Я можу уявити, CatDrawingControllerщо реалізує draw(і, можливо, використовує ін'єкцію залежності для отримання об'єкта Canvas). Я також можу уявити собі інший клас, який реалізує певний консольний вихід та використання getInfo(тому він printInfoможе застаріти в цьому контексті). Але для прийняття обґрунтованих рішень щодо цього потрібно знати контекст і як Catнасправді буде використовуватися клас.

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

Зверніть увагу також на приклад вище, залишається місце для прийняття рішення про (не) змінності. Якщо Catклас не буде виставляти жодних сеттерів і Imageє самим непорушним, ця конструкція дозволить зробити Catнепорушними (чого підхід DTO не буде). Але якщо ви вважаєте, що незмінність не потрібна або не корисна для вашої справи, ви також можете піти в цьому напрямку.


Спустіть щасливих потоків, це стає нудно. Якщо у вас є деякі критики, будь ласка, повідомте мене про це. Я буду радий прояснити будь-яке непорозуміння, якщо у вас є такі.
Док Браун

Хоча я, як правило, погоджуюся з вашою відповіддю ... Я думаю, що getInfo()хтось, хто задає це питання, може ввести в оману. Він тонко змішує обов'язки щодо презентації, як printInfo()і, перемагаючи мету DrawingControllerзаняття
Адріано Репетті

@AdrianoRepetti Я не бачу, що це перемагає мету DrawingController. Я згоден з цим getInfo()і printInfo()жахливими іменами. Поміркуйте toString()іprintTo(Stream stream)
candied_orange

@candid справа не тільки в назві, а в тому, що вона робить. Цей код стосується лише деталізації презентації, і ми фактично просто абстрагували вихід не обробленого контенту та його логіки.
Адріано Репетті

@AdrianoRepetti: якщо чесно, це лише надуманий приклад, щоб підходити до оригінального питання і продемонструвати мою точку зору. У реальній системі, звичайно, буде оточуючий контекст, який дозволить, звичайно, приймати рішення щодо кращих методів та назв.
Док Браун

6

Пізня відповідь, але я не можу встояти.

Чи є більшість X класів на Y хорошим чи анти-зразком?

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

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

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

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

Коли я вперше подивився на DTO, він мене помилив. Роки збитків, які Java завдала моєму мозку, змусили мене відскочити на публічних полях. Але ми працювали в C #. C # не потребує передчасних жителів та сетерів, щоб зберегти ваше право зробити дані непорушними або інкапсульованими пізніше. Не змінюючи інтерфейс, ви можете додавати їх коли завгодно. Можливо, просто так можна встановити точку перерви. Все, не кажучи про це своїм клієнтам. Так C #. Бу Java.

Так я тримав язик. Я спостерігав, як він використовував мої статичні методи, щоб ініціалізувати цю річ перед її використанням. У нас їх було близько 14. Це було потворно, але у нас не було причин для турботи.

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

Я неохоче запитав: "Чи вважаєте ви предмет?"

Він озирнувся до свого ДОТ і розгублено вивернув обличчя. "Це об'єкт".

"Я маю на увазі реальний об'єкт"

"А?"

"Дозвольте мені щось показати. Ви вирішите, чи це корисно"

Я вибрав нове ім’я і швидко проскочив щось таке, що виглядало приблизно так:

public class Cat{
    CatData(string catPage) {
        this.catPage = catPage
    }
    private readonly string catPage;

    public string name() { return chop("name prefix", "name suffix"); }
    public string weight() { return chop("weight prefix", "weight suffix"); }
    public string image() { return chop("image prefix", "image suffix"); }

    private string chop(string prefix, string suffix) {
        int start = catPage.indexOf(prefix) + prefix.Length;
        int end = catPage.indexOf(suffix);
        int length = end - start;
        return catPage.Substring(start, length);
    }
}

Це нічого не робило, ніж статичні методи вже не робили. Але тепер я всмоктав 14 статичних методів у клас, де вони могли бути на самоті з даними, над якими працювали.

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

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

Я кажу, що ви завжди повинні дотримуватися об'єктів OO над DTO? Ні. Блиск DTO, коли вам потрібно перейти межу, яка не дає вам рухатися методами. DTO мають своє місце.

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


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

Наприклад, у класі зазвичай є члени та методи класу, наприклад:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public void printInfo(){
        System.out.println("Name:"+this.name+",weight:"+this.weight);
    }

    public void draw(){
        //some draw code which uses this.image
    }
}

Де твій конструктор? Це не показує мене достатньо, щоб знати, чи це корисно.

Але прочитавши про принцип єдиної відповідальності та відкритий закритий принцип, я віддаю перевагу розділити клас на DTO та helper class лише статичними методами, наприклад:

public class CatData{
    public String name;
    public int weight;
    public Image image;
}

public class CatMethods{
    public static void printInfo(Cat cat){
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat){
        //some draw code which uses cat.image
    }
}

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

Ви можете зробити багато дурних речей в ім'я Принципу єдиної відповідальності. Я можу стверджувати, що котячі струни та котячі вставки повинні бути розділені. У всіх методах малювання та зображень має бути свій клас. За вашу запущену програму покладається одна відповідальність, тому вам слід мати лише один клас. : P

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

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

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

Моє запитання: добре це чи анти-модель?

Ну чорт, як я маю знати? Подивіться, робити це в будь-якому випадку має переваги та витрати. Коли ви відокремлюєте код від даних, ви можете змінити або без перекомпілювання іншого. Можливо, це для вас критично важливо. Можливо, це просто робить ваш код безглуздо складним.

Якщо ви відчуваєте себе краще, ви далеко не те, що Мартін Фаулер називає об'єктом параметра . Не потрібно брати у свій об’єкт лише примітивів.

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


2

Ви натрапили на тему дискусій, яка створює аргументи серед розробників вже більше десяти років. У 2003 році Мартін Фаулер створив фразу "Анемічна модель домену" (ADM), щоб описати поділ даних та функціональності. Він - та інші, хто погоджується з ним - стверджують, що "Моделі багатого домену" (змішування даних та функціональних можливостей) є "належним OO", і що ADM-підхід є анти-зразком, що не є OO.

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

Незалежно від того, на якій стороні огорожі ви сидите (я сиджу дуже твердо на "Мартін Фаулер говорить навантаженням старої туші" на стороні BTW), ваше використання статичних методів printInfoі drawмайже повсюдно нахмурене. Статичні методи важко знущатися, коли пишуть одиничні тести. Отже, якщо вони мають побічні ефекти (наприклад, друк або малюнок на якомусь екрані чи іншому пристрої), вони не повинні бути статичними або повинні містити вихідне місце як параметр.

Отже, у вас може бути інтерфейс:

public interface CatMethods {
    void printInfo(Cat cat);
    void draw(Cat cat);
}

І реалізація, яка впроваджується в іншу частину вашої системи під час виконання програми (для тестування використовуються інші реалізації):

internal class CatMethodsForScreen implements CatMethods {
    public void printInfo(Cat cat) {
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public void draw(Cat cat) {
        //some draw code which uses cat.image
    }
}

Або додайте додаткові параметри, щоб усунути побічні ефекти від цих методів:

public static class CatMethods {
    public static void printInfo(Cat cat, OutputHandler output) {
        output.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat, Canvas canvas) {
        //some draw code which uses cat.image and draws it on canvas
    }
}

Але чому б ви намагалися перетворити мову OO, як Java, на функціональну мову, а не переходити на функціональну мову з самого початку? Це якесь "я ненавиджу Java, але всі її використовують так, що мені доведеться, але, принаймні, я буду це писати по-своєму". Якщо ви не стверджуєте, що парадигма ОО є певним чином застарілою і застарілою. Я підписуюся на школу думки "Якщо ти хочеш Х, ти знаєш, де його взяти".
Каяман

@Kayaman, " Я підписуюся на" Якщо ви хочете X, ви знаєте, де взяти його "школа думки ". Я не. Я вважаю це ставлення як недалекоглядним, так і злегка образливим. Я не використовую Java, але використовую C # і ігнорую деякі "справжні OO" функції. Мені не байдуже про спадщину, стаціонарні предмети тощо, і я пишу багато статичних методів та закритих (заключних) класів. Інструментарій та розмір спільноти навколо C # є другим. Для F # - це спосіб бідніший. Тож я підписуюся на школу думки "Якщо ви хочете X, попросіть її додати до вашої поточної інструментальної школи".
Девід Арно

1
Я б сказав, що ігнорування аспектів мови сильно відрізняється від спроб змішати та співставити функції інших мов. Я також не намагаюся писати "справжній ОО", так як намагаються просто слідувати правилам (або принципам) у неточній науці, як програмування не працює. Звичайно, ви повинні знати правила, щоб ви могли їх порушити. Сліпо зайнявшись функціями, може бути погано для розвитку мови. Без образи, але IMHO частина сцени FP, здається, відчуває, що "FP - це найбільше, оскільки нарізаний хліб, чому люди не зрозуміють цього". Я ніколи не скажу (або думаю), що OO або Java - "найкращі".
Каяман

1
@Kayaman, але що, мабуть, означає "намагатися змішати та співставити функції інших мов"? Ви стверджуєте, що до Java не слід додавати функціональні функції, оскільки "Java - мова ОО"? Якщо так, то, на мій погляд, це короткозорий аргумент. Нехай мова розвивається із бажаннями розробників. Я вважаю, що примусити їх перейти на іншу мову - це неправильний підхід. Впевнені, що деякі "прихильники ПП" перейдуть до початку про те, що ПП є найкращою річчю. І деякі «прихильники ООС» переходять до вершини про те, що ОО «виграло битву, тому що це явно вище». Ігноруйте їх обох.
Девід Арно

1
Я не буду проти ФП. Я використовую його на Java, де це корисно. Але якщо програміст каже "Я хочу це", це як клієнт, який каже "Я хочу цього". Програмісти не мовні дизайнери, а клієнти - не програмісти. Я підтримав вашу відповідь, оскільки ви говорите, що "рішення" ОП нахмуриться. Коментар у цьому питанні є більш лаконічним, тобто він переробив процедурне програмування мовою ОО. Загальна ідея, правда, має сенс, як ви показали. Неанемічна модель домену не означає, що дані та поведінка є виключними, і сторонні рендери або презентатори будуть заборонені.
Каяман

0

DTO - об'єкти транспортування даних

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

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


-1

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

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

Ось кілька питань, які ви можете задати собі щодо програми:

  • Чи мені коли-небудь потрібно надати альтернативний спосіб надання зображення Cat? Якщо це так, чи спадкування не стане зручним способом зробити це?
  • Чи потрібно моєму класу Cat успадковувати інший клас чи задовольняти інтерфейс?

Відповідь "так" на будь-яке з них може призвести до більш складної конструкції з окремими DTO та функціональними класами.


-1

Це гарний зразок?

Ви, мабуть, отримаєте різні відповіді, тому що люди слідують за різними школами думки. Отже, перш ніж я навіть почати: Ця відповідь відповідає об'єктно-орієнтованому програмуванню, як описано Робертом К. Мартіном у книзі "Чистий код".


"Справжній" ООП

Наскільки мені відомо, є 2 тлумачення ООП. Для більшості людей воно зводиться до "об’єкт - клас".

Однак мені інша школа думки була більш корисною: "Об'єкт - це те, що щось робить". Якщо ви працюєте з класами, кожен клас буде однією з двох речей: " власник даних " або об'єкт . Власники даних зберігають дані (duh), об'єкти роблять речі. Це не означає, що власник даних не може мати методи! Подумайте list: A listнасправді нічого не робить , але у нього є методи, щоб застосувати спосіб зберігання даних .

Власники даних

Ви Catє прекрасним прикладом власника даних. Звичайно, поза вашою програмою кішка - це щось, що щось робить , але всередині вашої програми Cat- це струна name, int weightта зображення image.

Те, що власники даних нічого не роблять, також не означає, що вони не містять ділової логіки! Якщо у вашому Catкласі є конструктор, який вимагає name, weightі imageви успішно склали ділове правило, що у кожної кішки є такі. Якщо ви можете змінити weightпісля створення Catкласу, це інше ділове правило, укладене в капсули. Це дуже основні речі, але це просто означає, що дуже важливо не помилитися з ними - і таким чином ви гарантуєте, що не можна зрозуміти це неправильно.

Об'єкти

Об'єкти щось роблять. Якщо ви хочете, щоб очистити * об’єкти, ми можемо змінити це на "Об'єкти роблять одне ".

Друк інформації про котів - це завдання об'єкта. Наприклад, ви можете назвати цей об'єкт CatInfoLoggerзагальнодоступним методом Log(Cat). Це все, що нам потрібно знати ззовні: Цей об’єкт записує інформацію про кішку, і для цього вона потрібна Cat.

Зсередини цей об'єкт має посилання на все, що йому потрібно, щоб виконати свою єдину відповідальність за лісозаготівлю. У цьому випадку, це тільки посилання Systemна System.out.println, але в більшості випадків це будуть приватні посилання на інші об'єкти. Якщо логіка форматування виводу перед друком стає занадто складною, CatInfoLoggerможе просто отримати посилання на новий об’єкт CatInfoFormatter. Якщо пізніше кожен файл також повинен бути записаний у файл, ви можете додати CatToFileLoggerоб'єкт, який це робить.

Власники даних проти об'єктів - підсумок

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

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

Перевищення?

Це рішення може здатися трохи зайвим. Дозвольте мені бути відвертим: Це . Якщо вся ваша програма настільки велика, як приклад, не робіть жодного з перерахованих вище - просто виберіть будь-який із запропонованих способів за допомогою кидання монети. Там немає ніякої небезпеки в оману іншого коду , якщо немає ніякого іншого коду.

Однак будь-яке "серйозне" (= більше, ніж сценарій або 2) програмне забезпечення, ймовірно, досить велике, що може отримати прибуток від такої структури.


Відповідь

Чи є розділення більшості класів на DTO та допоміжні класи [...] хорошим чи антидіаграмою?

Перетворення «більшості класів» (без будь-якої подальшої кваліфікації) у DTO / власники даних не є антидіаграмою, оскільки це насправді не будь-яка закономірність - вона більш-менш випадкова.

Однак тримання даних та об’єктів розглядається як хороший спосіб зберегти чистий код *. Іноді вам знадобиться лише плоский, «анемічний» DTO, і це все. В іншому випадку вам потрібні методи для забезпечення способу зберігання даних. Просто не покладайте на вашу функціональність програми дані.


* Це "Очистити" з великою літерою на зразок назви книги, тому що - пам’ятайте перший абзац - це про одну школу думки, тоді як є й інші.

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