Не оголошуйте інтерфейси для незмінних об'єктів


27

Не оголошуйте інтерфейси для незмінних об'єктів

[EDIT] Якщо об'єкти, про які йдеться, представляють об'єкти передачі даних (DTO) або звичайні старі дані (POD)

Це розумна настанова?

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

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

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

Це може мати наслідки щодо тестування одиниць, але, окрім цього, чи здається це розумним орієнтиром?

Або є інша модель, яку я повинен використовувати, щоб уникнути проблеми "розширювальний інтерфейс", яку я бачу?

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

[EDIT]

Щоб отримати набагато більше контексту для моїх причин, які хочуть зробити об'єкти непорушними, дивіться цю публікацію в блозі від Еріка Ліпперта:

http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/

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

Також Джошуа Блох рекомендує використовувати незмінні предмети у своїй книзі «Ефективна Java» .


Слідувати

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

Є деякі інші питання, пов'язані з цим, про які я хочу запитати; зокрема, я називаю "Глибоку чи дрібну незмінність" (номенклатуру, яку я вкрав із глибокого та мілкого клонування) - але це питання для іншого разу.


3
+1 велике запитання. Поясніть, будь ласка, чому для вас так важливо, що об'єкт є саме цього незмінного класу, а не того, що просто реалізує той самий інтерфейс. Цілком можливо, що ваші проблеми вкорінені в іншому місці. Інжекція залежностей надзвичайно важлива для тестування одиниць, але вона має багато інших важливих переваг, які все минають, якщо ви змушуєте ваш код вимагати певного непорушного класу.
Стівен Доггарт

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

3
Я розгублений, чому б просто не залишити сеттер від інтерфейсу? Але з іншого боку, я намагаюся бачити інтерфейси для об'єктів, які є дійсно доменними об'єктами та / або DTO, що вимагають фабрик для цих об'єктів ... викликає когнітивний дисонанс для мене.
Майкл Браун

2
Як щодо інтерфейсів для класів, які все незмінні? Наприклад, в Java Номер класу дозволяє , щоб визначити , List<Number>який може містити Integer, Float, Long, BigDecimalі т.д. ... Все це незмінні самі.

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

Відповіді:


17

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

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

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

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

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


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

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

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

@Kevin Я думаю, але, маючи сучасну зручність автоматичних властивостей, зробити їх властивостями так просто, чому б і ні? Я гадаю, якщо продуктивність та ефективність викликають найбільше занепокоєння, то, можливо, це має значення. У будь-якому випадку, питання полягає в тому, який рівень "логіки" ви допускаєте в DTO? Навіть якщо ви введете якусь логіку перевірки у його властивості, не було б проблем перевірити її, доки у неї не було залежностей, які потребували б глузування. Поки DTO не використовує жодних бізнес-об'єктів залежності, тоді можна безпечно вбудувати в них трохи логіки, оскільки вони все ще будуть перевірятими.
Стівен Доггарт

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

7

Раніше я робив великий шум за те, щоб зробити свій код невразливим для неправильного використання. Я робив інтерфейси лише для читання для приховування членів, що мутують, додав багато обмежень для моїх загальних підписів тощо, тощо. Виявилося, що більшість часу я приймав дизайнерські рішення, тому що не довіряв своїм уявним колегам. "Можливо, колись вони найнять нового хлопця початкового рівня, і він не знатиме, що клас XYZ не може оновити DTO ABC. О, ні!" Інший раз я зосереджувався на неправильній проблемі - ігноруючи очевидне рішення - не бачити лісу крізь дерева.

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

Мій висновок полягав у пошуку найпростішої речі, яка працює. Додаткова складність створення безпечних інтерфейсів просто витрачає час на розробку і ускладнює інакше простий код. Турбуйтеся про такі речі, коли у вас є 10 000 розробників, які використовують ваші бібліотеки. Повірте, це позбавить вас від безлічі напруги.


Зазвичай я зберігаю інтерфейси, коли мені потрібно введення залежності для тестування одиниць.
Travis Parks

5

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

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


2

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

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

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

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


1
Чи можете ви навести приклад здійснення незмінності як декоратора?
vaughandroid

Я не думаю, що це банально, але це відносно проста думка - якщо MutableObjectє nметоди, що змінюють стан, і mметоди, які повертають стан, ImmutableDecoratorможуть продовжувати викривати методи, які повертають стан ( m), і, залежно від оточення, стверджувати або кинути виняток, коли викликається один із змінних методів.
Джонатан Річ

Мені дуже не подобається перетворювати визначеність часу компіляції у можливість виключень під час запуску ...
Меттью Уотсон

Гаразд, але як ImmutableDecorator може знати, чи певний метод змінює стан чи ні?
vaughandroid

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

0

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

У C #, метод, який очікує, що об'єкт типу "непорушний" не повинен мати параметр типу інтерфейсу, оскільки інтерфейси C # не можуть визначати договір про незмінність. Тому керівництво, яке ви пропонуєте, безглуздо для C #, оскільки ви не можете цього зробити в першу чергу (а майбутні версії мов навряд чи дозволять вам це зробити).

Ваше запитання випливає з тонкого нерозуміння статей Еріка Ліпперта про незмінність. Ерік не визначив інтерфейси IStack<T>та IQueue<T>вказав контракти на незмінні стеки та черги. Вони цього не роблять. Він визначив їх для зручності. Ці інтерфейси дозволяли йому визначати різні типи для порожніх стеків та черг. Ми можемо запропонувати іншу конструкцію та реалізацію незмінного стека, використовуючи один тип, не вимагаючи інтерфейсу або окремого типу для представлення порожнього стека, але отриманий код не буде виглядати таким чистим і був би трохи менш ефективним.

Тепер приступимо до дизайну Еріка. Метод, який вимагає незмінного стека, повинен мати параметр типу, Stack<T>а не загальний інтерфейс, IStack<T>який представляє абстрактний тип даних стека в загальному розумінні. Не очевидно, як це зробити, використовуючи незмінний стек Еріка, і він не обговорював це у своїх статтях, але це можливо. Проблема полягає у типі порожнього стека. Ви можете вирішити цю проблему, переконавшись, що ви ніколи не отримаєте порожній стек. Це можна забезпечити, натиснувши на макет фіксованого значення як перше значення і ніколи не вискочивши його. Таким чином, ви можете сміливо подавати результати Pushі Popдо Stack<T>.

Наявність Stack<T>знаряддя IStack<T>можуть бути корисними. Ви можете визначити методи, які вимагають стека, будь-якого стека, не обов'язково незмінного стека. Ці методи можуть мати параметр типу IStack<T>. Це дає змогу передавати їй незмінні стеки. В ідеалі - IStack<T>це частина стандартної бібліотеки. У .NET його немає IStack<T>, але є й інші стандартні інтерфейси, які незмінний стек може реалізовувати, роблячи тип більш корисним.

Альтернативна розробка та реалізація незмінного стека, про який я згадував раніше, використовує інтерфейс під назвою IImmutableStack<T>. Звичайно, вставлення "Immutable" в ім'я інтерфейсу не робить кожен тип, який реалізує його, непорушним. Однак у цьому інтерфейсі договір незмінності є просто словесним. Хороший розробник повинен це поважати.

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

Я хотів би додати, оскільки ви позначили питання C #, що в специфікації C # немає такого поняття, як DTO і POD. Тому їх відкидання або точне визначення покращує питання.

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