Чи є якась причина використовувати класи "простих старих даних"?


43

У застарілому коді я час від часу бачу класи, які є не що інше, як обгортки для даних. щось на зразок:

class Bottle {
   int height;
   int diameter;
   Cap capType;

   getters/setters, maybe a constructor
}

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

Чи є випадок, коли такі об’єкти були б необхідні? Якщо це використовується часто, чи робить це дизайн підозрілим?


1
Це не зовсім відповідає на ваше запитання, але здається, що все-таки є актуальним: stackoverflow.com/questions/36701/struct-like-objects-in-java
Адам Лір

2
Я думаю, що термін, який ви шукаєте, - це POD (Plain Old Data).
Гаурав

9
Це типовий приклад структурованого програмування. Не обов'язково погано, просто не орієнтовано на об’єкти.
Конрад Рудольф

1
чи не повинно це бути переповнення стека?
Muad'Dib

7
@ Muad'Dib: технічно мова йде про програмістів. Ваш компілятор не хвилює, чи використовуєте ви прості старі структури даних. Ваш процесор, ймовірно, йому подобається (у своєму роді "Я люблю запах даних, свіжих із кеша"). Це люди , які зациклюються на «це робить мою методику менш чистим?» запитання.
Shog9

Відповіді:


67

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

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

ніж

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

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

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

У деяких мовах є й інші міркування. Наприклад, у класах C # і структури поводяться по-різному, оскільки структури є типом значення, а класи - еталонними типами.


25
Ні. DoStuffWithповинен бути метод з до Bottleкласу в ООП (і , швидше за все , повинні бути незмінні, теж). Те, що ви написали вище, не є гарним зразком у мові ОО (якщо ви не взаємодієте зі застарілим API). Це є , однак, дійсний дизайн в середовищі без ГО.
Конрад Рудольф

11
@Javier: Тоді і Java не найкраща відповідь. Наголос Яви на ОО є надзвичайним, мова в основному нічого хорошого.
Конрад Рудольф

11
@JohnL: Я, звичайно, спрощував. Але загалом об'єкти в ОО інкапсулюють стан , а не дані. Це тонка, але важлива відмінність. Сенс ООП полягає в тому, щоб не було багато даних, що сидять навколо. Це відправка повідомлень між державами, які створюють новий стан. Я не бачу, як ви можете надсилати повідомлення або до методу, або від нього. (І це оригінальне визначення ООП, тому я б не вважав його недосконалим.)
Конрад Рудольф

13
@Konrad Rudolph: Ось чому я явно зробив коментар усередині методу. Я погоджуюся, що методи, які впливають на екземпляри Bottle, повинні відповідати цьому класу. Але якщо іншому об’єкту потрібно змінити його стан на основі інформації в пляшці, то я думаю, що моя конструкція була б досить справедливою.
Адам Лір

10
@Konrad, я не погоджуюся з тим, що doStuffWithBottle повинен перейти в клас пляшок. Чому пляшка повинна знати, як робити речі з собою? doStuffWithBottle вказує на те, що щось ще зробить щось із пляшкою. Якщо б у пляшці було це, це була б щільна муфта. Однак, якщо у класі Bottle був метод isFull (), це було б цілком доречно.
Немі

25

Класи даних є дійсними в деяких випадках. DTO - це один хороший приклад, який згадувала Анна Лір. Взагалі, ви повинні розглядати їх як насіння класу, методи якого ще не проросли. І якщо ви зіштовхуєтеся з ними в старому коді, ставитесь до них як до сильного кодового запаху. Їх часто використовують старі програмісти C / C ++, які ніколи не переходили на програмування OO і є ознакою процедурного програмування. Покладаючись на геттерів та сетерів увесь час (або ще гірше, на прямий доступ приватних членів), ви можете зіткнутися з вами, перш ніж ви це дізнаєтесь. Розглянемо приклад зовнішнього методу, який потребує інформації Bottle.

Ось Bottleклас даних):

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Ось ми привели Bottleдеяку поведінку):

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Перший метод порушує принцип Tell-Don't-Ask, і, зберігаючи Bottleнімий, ми дозволили неявні знання про пляшки, наприклад, що робить один фрагмент (the Cap), ковзаючи в логіку, яка знаходиться поза Bottleкласом. Ви повинні бути на ногах, щоб не допустити подібних "витоків", коли ви звично покладаєтесь на геттерів.

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

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


1
Не можу повірити, що ця відповідь не має голосів (ну, у вас зараз є). Це може бути простим прикладом, але при недостатньому зловживанні OO ви отримуєте класи обслуговування, які перетворюються на кошмари, що містять багато функціональних можливостей, які повинні були бути інкапсульовані в класи.
Альб

"старими програмами C / C ++, які ніколи не здійснювали транзит (sic) на ОО"? Програмісти на C ++ зазвичай є досить OO, оскільки це мова OO, тобто вся суть використання C ++ замість C.
nappyfalcon

7

Якщо це потрібна річ, це добре, але будь ласка, будь ласка, зробіть це так, як

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

замість чогось подібного

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

Будь ласка.


14
Єдина проблема з цим полягає в тому, що немає перевірки. Будь-яка логіка щодо того, що є дійсною пляшкою, повинна бути у класі пляшок. Однак, використовуючи запропоновану вами реалізацію, я можу мати пляшку з від’ємною висотою та діаметром - немає способу застосувати будь-які бізнес-правила щодо об’єкта, не перевіряючи кожного разу, коли об’єкт використовується. Використовуючи другий метод, я можу переконатися, що якщо у мене є об'єкт Bottle, він був, є і завжди буде дійсним об'єктом пляшки згідно мого договору.
Томас Оуенс

6
Це одна область, де .NET має невеликі переваги над властивостями, оскільки ви можете додати аксесуар властивості з логікою перевірки, з тим же синтаксисом, як якщо б ви отримували доступ до поля. Ви також можете визначити властивість, де класи можуть отримати значення властивості, але не встановити його.
ДжонЛ

3
@ user9521 Якщо ви впевнені, що ваш код не спричинить смертельну помилку з "поганими" значеннями, тоді перейдіть до свого методу. Однак якщо вам потрібна додаткова перевірка, або можливість використовувати ледачу завантаження, або інші перевірки, коли дані читаються чи записуються, то використовуйте явні геттери та сетери. Особисто я прагну зберігати свої змінні приватними і використовую геттери та сетери для узгодженості. Таким чином, всі мої змінні трактуються однаково, незалежно від валідації та / або інших "просунутих" методів.
Джонатан

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

7
Я б робив поля остаточними, коли це можливо. IMHO Я б вважав за краще, щоб поля були остаточними за підсумками і мали ключове слово для змінних полів. напр. var
Пітер Лорі

6

Як сказала @Anna, безумовно, не зла. Впевнені, що ви можете розміщувати операції (методи) у класах, але тільки якщо ви хочете . Вам не доведеться .

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

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

  2. Вирішіть проблему 1, ввівши методи сповіщення , щоб, якщо частина A була змінена, вона намагається поширити необхідні зміни до частин B і C. Це головна причина, чому рекомендується використовувати методи отримання та встановлення доступу. Оскільки це є рекомендованою практикою, вона, як видається, виправдовує проблему 1, спричиняючи більше проблеми 1 і більше рішення 2. Це призводить не тільки до помилок через неповноцінну реалізацію сповіщень, але до проблеми з скороченням продуктивності униклих сповіщень. Це не нескінченні обчислення, просто дуже довгі.

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

Ось що я намагаюся зробити:

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

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


3

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

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

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


3

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


3

Погодьтеся з Анною Лір,

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

Іноді люди забувають читати конвенції Java про кодування 1999 року, які дозволяють зрозуміти, що подібне програмування є прекрасним. Насправді, якщо ви цього уникаєте, то ваш код буде пахнути! (занадто багато геттерів / сетер)

З Java Code Conventions 1999: Одним прикладом відповідних змінних публічних екземплярів є випадок, коли клас по суті є структурою даних, без поведінки. Іншими словами, якщо ви використовували б структуру замість класу (якщо Java підтримувала структуру), то доречно зробити загальні змінні екземплярів класу. http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177

При правильному використанні ПОД (звичайні старі структури даних) кращі, ніж POJO, точно як POJO часто краще, ніж EJB.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures


3

Структури мають своє місце навіть на Яві. Використовувати їх слід лише в тому випадку, якщо такі дві речі справжні:

  • Вам просто потрібно агрегувати дані, які не мають жодної поведінки, наприклад передавати як параметр
  • Не має значення один біт, які значення мають сукупні дані

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

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

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

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

Ваш приклад «Пляшка», швидше за все, провалить обидва тести. Ви можете мати (надуманий) код, який виглядає приблизно так:

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

Натомість має бути

double volume = bottle.calculateVolumeAsCylinder();

Якби ви змінили висоту і діаметр, це була б однакова пляшка? Напевно, ні. Вони повинні бути остаточними. Чи від’ємне значення для діаметра? Чи повинна ваша пляшка бути вищою, ніж широка? Чи може Cap недійсний? Немає? Як ви це підтверджуєте? Припустимо, що клієнт або дурний, або злий. ( Неможливо визначити різницю. ) Вам потрібно перевірити ці значення.

Ось як може виглядати ваш новий клас пляшок:

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}

0

ІМХО часто не вистачає таких класів у сильно об'єктно-орієнтованих системах. Мені потрібно ретельно це кваліфікувати.

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

Але є багато випадків, коли такі поля даних матимуть дуже вузьку сферу застосування. Дуже простий приклад - приватний Nodeклас структури даних. Він часто може значно спростити код, зменшивши кількість діючих взаємодій, якщо сказане Nodeможе просто складатися з необроблених даних. Це служить механізмом розв'язки, оскільки альтернативна версія може вимагати двонаправленого з’єднання з, скажімо, Tree->Nodeі Node->Treeна відміну від простого Tree->Node Data.

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

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

введіть тут опис зображення

... на відміну від цього:

введіть тут опис зображення

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


-1

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

SceneMember 
Message extends SceneMember
Port extends SceneMember
InputPort extends Port
OutputPort extends Port

Отже, SceneMember є базовим класом, він виконує всю роботу, яка повинна з'явитися на сцені: додавання, видалення, отримання ідентифікатора set тощо. Повідомлення - це зв'язок між портами, у нього є власне життя. InputPort і OutputPort містять свої власні функції вводу / виводу.

Порт порожній. Він просто використовується для групування InputPort та OutputPort разом для, скажімо, списку портів. Це порожньо, тому що SceneMember містить усі матеріали, які призначені для участі у складі члена, а InputPort та OutputPort містять завдання, визначені портом. InputPort та OutputPort настільки різні, що у них немає спільних (крім SceneMember) функцій. Отже, Порт порожній. Але це законно.

Можливо, це антипатерн , але мені це подобається.

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


1
Чому вашим портам потрібно розширити SceneMember? чому б не створити класи портів для роботи на SceneMembers?
Майкл К

1
То чому б не використати стандартний шаблон інтерфейсу маркера? В основному ідентичний вашому порожньому абстрактному базовому класу, але це набагато частіша ідіома.
TMN

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

1
@TMN: Типи SceneMember (похідні) мають метод getMemberType (), є кілька випадків, коли сценарій сканується на підмножину об'єктів SceneMember.
ern0

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