Як вирішувати, чи слід створити тип об'єкта даних таким, що є незмінним?


23

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

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

Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

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

Як ви вирішите, чи слід об'єкт типу проектувати як незмінний? Чи є гарний набір критеріїв, за якими можна судити?

В даний час я обговорюю питання про те, чи переключити кілька типів даних у власному проекті на незмінні, але мені доведеться виправдовувати це своїм колегам, і дані цих типів можуть (ДУЖЕ рідко) змінюватися - в цей час ви, звичайно, можете змінити це незмінний спосіб (створити новий, копіюючи властивості зі старого об'єкта, за винятком тих, які потрібно змінити). Але я не впевнений, чи це лише моя любов до непорушних проявів, чи є фактична потреба в / вигоді від них.


1
Програма в Ерланге, і вся проблема вирішена, все незмінне
Zachary K

1
@ZacharyK Я насправді думав згадати щось про функціональне програмування, але утримався через мій обмежений досвід роботи з функціональними мовами програмування.
Ricket

Відповіді:


27

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


11

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

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


15
Користь незмінних предметів полягає в тому, що про них легше міркувати. У багатопотоковому середовищі це жахливо простіше; у звичайних однониткових алгоритмах все одно простіше. Наприклад, вам ніколи не потрібно дбати про те, чи відповідає стан, чи передав об'єкт всі мутації, необхідні для використання в певному контексті тощо. Найкращі об'єкти, що змінюються, також дозволяють вам мало піклуватися про їх точний стан: наприклад, кеші.
9000

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

4
@Doval: Нема з чим погоджуватися. 9000 абсолютно вірно; незмінні об'єкти є легше розмірковувати про. Це частково те, що робить їх настільки корисними для одночасного програмування. Інша частина полягає в тому, що вам не потрібно турбуватися про зміну стану. Передчасна оптимізація тут не має значення; використання незмінних об'єктів стосується дизайну, а не продуктивності, а поганий вибір структур даних наперед - це передчасна песимізація.
Роберт Харві

@RobertHarvey Я не впевнений, що ви маєте на увазі під "поганим вибором структури даних наперед". Існують незмінні версії більшості змінних структур даних, що забезпечують аналогічні показники. Якщо вам потрібен список, і у вас є вибір використання незмінного списку та змінного списку, ви переходите з тим, хто непорушний, поки не знаєте, що це вузьке місце у вашій програмі, і змінна версія буде краще.
Доваль

2
@Doval: Я читав Окасакі. Ці структури даних часто не використовуються, якщо ви не використовуєте мову, яка повністю підтримує функціональну парадигму, наприклад Haskell або Lisp. І я заперечую те, що незмінні структури є вибором за замовчуванням; переважна більшість бізнес-обчислювальних систем все ще розроблена навколо змінних структур (тобто реляційних баз даних). Починати з непорушних структур даних - це приємна ідея, але це все ще дуже башта слонової кістки.
Роберт Харві

6

Є два основні способи вирішити, чи є предмет незмінним.

а) Виходячи з характеру Об'єкта

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

б) Виходячи з вибору дизайну, зовнішніх факторів

Це схоже на приклад java.lang.String. Струни можуть фактично змінюватися з часом, але за задумом вони зробили це непорушним через фактори кешування / пулу / паралельності. Подібне кешування / сумісність тощо може зіграти хорошу роль у створенні об'єкта незмінним, якщо кешування / сумісність та пов'язана з цим продуктивність є життєво важливим для програми. Але це рішення слід приймати дуже обережно після придушення всіх наслідків.

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


4

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

Мінімізація стану програми є дуже вигідною.

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

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


4

Відповідь дещо залежить від мови. Ваш код схожий на Java, де ця проблема є настільки складною, наскільки це можливо. У Java об’єкти можна передавати лише за посиланням, а клон повністю розбитий.

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

Тож безумовно зробіть об'єкти невеликої цінності незмінними та застосуйте конструктор копій. Забудьте про Cloneable, він настільки погано розроблений, що марний.

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


Дуже схоже, як обирати стек та купу під час написання C або C ++ :)
Ricket

@Ricket: не так багато ІМО. Стек / купа залежить від терміну експлуатації об'єкта. У C ++ дуже часто зустрічатися змінні об'єкти на стеку.
кевін клайн

1

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

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

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

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

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


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

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

0

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

Наприклад, це може бути надзвичайно дорогим:

/// @return A new mesh whose vertices have been transformed
/// by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

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

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

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

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

/// @return The v1 * v2.
Vector3f vec_mul(Vector3f v1, Vector3f v2);

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


-1
Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Якщо Foo має лише дві властивості, то легко написати конструктор, який бере два аргументи. Але припустимо, ви додали ще одну властивість. Потім потрібно додати ще один конструктор:

public Foo(String a, String b, SomethingElse c)

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


4
Чи конструктор з десятьма аргументами гірший за NullPointerException, який виникає, коли властивість7 не була встановлена? Чи є шаблон конструктора складнішим за код для перевірки неініціалізованих властивостей у кожному методі? Краще один раз перевірити, коли об’єкт побудований.
кевін клайн

2
Як було сказано десятиліття тому саме для цього випадку, "Якщо у вас є процедура з десятьма параметрами, ви, певно, деякі пропустили".
9000

1
Чому б вам не захотіти конструктор з 10 аргументами? У який момент ви проводите лінію? Я думаю, що конструктор з 10 аргументами - це невелика ціна, яку потрібно заплатити за переваги незмінності. Плюс, або у вас є один рядок з 10 речей, розділених комами (необов’язково розкладіть на кілька рядків або навіть 10 рядків, якщо це виглядає краще), або у вас все одно є 10 рядків під час налаштування кожної власності окремо ...
Ricket

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

4
Якщо у вас є конструктор з 10 аргументами, можливо, настав час подумати про інкапсуляцію цих аргументів у власний клас (або класи) та / або критично подивитися на дизайн вашого класу.
Адам Лір
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.