Документація в ООП повинна уникати вказівки, виконує чи «обчислювач» якісь обчислення?


39

Програма CS моєї школи уникає будь-якої згадки об'єктно-орієнтованого програмування, тому я читав самостійно читання, щоб доповнити його - конкретно, Об'єктно-орієнтована побудова програмного забезпечення Бертрана Мейєра.

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

Наприклад, якщо у класу Personє атрибут age, він стверджує, що з позначень не може бути визначено , чи Person.ageвідповідає він чимось подібним return current_year - self.birth_dateабо просто return self.age, де self.ageвизначено як постійний атрибут. Це має для мене сенс. Однак він продовжує вимагати наступного:

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

тобто він стверджує, що навіть документація для класу повинна уникати вказівки на те, чи "Getter" виконує якісь обчислення чи ні.

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


1
Цікаве запитання. Я нещодавно запитав про щось дуже подібне: як би я створив інтерфейс таким, щоб було зрозуміло, які властивості можуть змінювати своє значення, а які залишатись постійними? . І я отримав хорошу відповідь, вказуючи на документацію, тобто саме те, на що Бертран Мейєр, схоже, сперечається.
stakx

Я не читав книги. Чи наводить Мейєр будь-які приклади стилю документації, який він рекомендує? Мені важко уявити, що ти описав, працюючи на будь-якій мові.
user16764

1
@PatrickCollins Я пропоную вам прочитати «страта в королівстві іменників» і тут опинитися поза поняттям дієслів та іменників. По-друге, OOP НЕ стосується геттерів та сетерів, я пропоную Алану Кей (винахіднику ООП): програмування та масштабування
AndreasScheinert

@AndreasScheinert - ти це посилаєшся ? Я посміхнувся на "все за бажанням підковоподібного цвяха", але, здається, це скандал про зло об'єктно-орієнтованого програмування.
Патрік Коллінз

1
@PatrickCollins так: steve-yegge.blogspot.com/2006/03/… ! Це дає деякі моменти для роздумів, інші - ви повинні перетворити ваші об'єкти в структури даних за допомогою (ab) за допомогою сетерів.
AndreasScheinert

Відповіді:


58

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

Але кодеру, який використовує ваш клас, не потрібно знати, чи реалізовано ви:

return currentAge;

або:

return getCurrentYear() - yearBorn;

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

Але це не завжди так, наприклад, припустимо, у вас є метод розміру на контейнері. Це може бути реалізовано:

return size;

або

return end_pointer - start_pointer;

або це може бути:

count = 0
for(Node * node = firstNode; node; node = node->next)
{
    count++
}
return count

Різниця між першими двома дійсно не має значення. Але останній може мати серйозні наслідки для продуктивності. Ось чому, наприклад, STL говорить, що .size()це O(1). Це не документує, як саме розраховується розмір, але це дає мені характеристики продуктивності.

Отже : проблеми з документацією. Не документуйте деталі впровадження. Мені все одно, як std :: sor сортує мої речі, якщо це робиться правильно та якісно. Ваш клас також не повинен документувати, як він обчислює речі, але якщо щось має несподіваний профіль продуктивності, документуйте це.


4
Більше того: спершу документуйте складність часу та простору, а потім дайте пояснення, чому функція має ці властивості. Напр .:// O(n) Traverses the entire user list.
Джон Перді,

2
= (Щось таке тривіальне, як це lenне вдається Python ... (Принаймні, в деяких ситуаціях, це O(n), як ми дізналися в проекті в коледжі, коли я запропонував зберігати довжину, а не перераховувати її на кожну ітерацію циклу)
Izkata

@Izkata, цікаво. Ви пам’ятаєте, яка структура була O(n)?
Вінстон Еверт

@WinstonEwert На жаль, ні. Це було 4 роки тому в проекті Data Mining, і я лише запропонував це своєму другові на переконання, бо працював з C в іншому класі.
Izkata

1
@JonPurdy Я додам, що в звичайному бізнес-коді, мабуть, немає сенсу вказувати складність big-O. Наприклад, доступ до бази даних O (1), швидше за все, буде набагато повільнішим, ніж обхід списку пам'яті O (n), тому документуйте важливе значення. Але, безумовно, є випадки, коли складність документування є дуже важливою (колекції або інший алгоритм-важкий код).
svick

16

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

Однак більшість реальних програм страждає від "Закону про протікання абстракцій" Джоела Спольського , який говорить

"Всі нетривіальні абстракції, певною мірою, є герметичними".

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

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


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

12

Ви можете написати, якщо даний дзвінок дорогий чи ні. Краще, використовувати угоду про іменах , як getAgeдля швидкого доступу і loadAgeчи fetchAgeдля дорогого пошуку. Ви обов'язково хочете повідомити користувача, якщо метод виконує будь-який IO.

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


1
+1 для згадки про те, що документація є стільки ж частиною договору класу, скільки інтерфейсом.
Барт ван Інген Шенау

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

9

Якби я розробляв базу даних, наповнену об'єктами Person, чи не важливо було б знати, чи є Person.age дорогим дзвінком?

Так.

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

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


4
+1: ця конвенція є ідіоматичною у досить багатьох місцях. Крім того, документація повинна бути виконана на рівні інтерфейсу - тоді ви не знаєте, як реалізується Person.Age.
Теластин

@Telastyn: Я ніколи не замислювався над документацією в такому вигляді; тобто це слід робити на рівні інтерфейсу. Зараз це здається очевидним. +1 за цей цінний коментар.
stakx

Мені ця відповідь дуже подобається. Ідеальний приклад того, що ви описуєте, що продуктивність не викликає занепокоєння для самої програми, якби Person був сутністю, отриманим із сервісу RESTful. GET властивий, але не очевидно, чи це буде дешево чи дорого. Звичайно, це не обов'язково OOP, але справа в тому ж.
maple_shaft

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

Звідки береться ця конвенція? Думаючи про Яву, я б очікував, що це навпаки: getметод еквівалентний доступу до атрибутів і тому не дорогий.
сімфорси

3

Важливо зазначити, що перше видання цієї книги було написане в 1988 році, в перші дні ООП. Ці люди працювали з більш чисто об'єктно орієнтованими мовами, які широко використовуються сьогодні. Наші найпопулярніші сьогодні мови OO - C ++, C # & Java - мають досить суттєві відмінності від того, як працювали перші, більш чисто OO, мови.

У такій мові, як C ++ та Java, ви повинні розрізняти доступ до атрибута та виклик методу. Там цілий світ різниці між instance.getter_methodі instance.getter_method(). Один насправді отримує вашу цінність, а інший - ні.

Працюючи з більш чистою мовою OO, з переконання Smalltalk або Ruby (як видно, мова Ейфелева, що використовується в цій книзі), вона стає абсолютно достовірною порадою. Ці мови будуть неявно називати вас методами. Не стає різниці між instance.attributeі instance.getter_method.

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


1
Дуже важливий момент щодо розгляду року, в якому було зроблено пропозицію. Nit: Smalltalk та Simula відносяться до 60-х та 70-х років, тому 88 навряд чи є "ранніми днями".
luser droog

2

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

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


3
Чи завжди так буває, що обчислювально-дорогий метод - помилка? Як тривіальний приклад, скажімо, що я переймаюся підсумовуванням довжин масиву рядків. Всередині я не знаю, чи мої рядки в мові Pascal або C. У першому випадку, оскільки рядки "знають" їх довжину, я можу очікувати, що мій цикл підсумовування довжини займе лінійний час залежно від кількості рядків. Я також повинен знати, що операції, що змінюють довжину рядків, матимуть накладні витрати, пов'язані з ними, оскільки string.lengthбудуть перераховані щоразу, коли вони змінюються.
Патрік Коллінз

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

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

Я бачу. Тож відповідь ОО «Як я можу витіснити додаткову продуктивність із свого коду, спираючись на конкретну реалізацію?» "Ви не можете". А відповідь на "Мій код повільніший, ніж я б очікував, що це буде, чому?" є "Це потрібно переписати". Це більш-менш ідея?
Патрік Коллінз

2
@PatrickCollins Відповідь ОО покладається на інтерфейси, а не на реалізацію. Не використовуйте інтерфейс, який не включає гарантії продуктивності як частини визначення інтерфейсу (наприклад, приклад C ++ 11 List.size, що гарантується O (1)). Він не вимагає включення деталей реалізації в визначення інтерфейсу. Якщо ваш код повільніше, ніж вам хотілося б, чи є інша відповідь, ніж вам доведеться змінити його на швидший (після профілювання для визначення вузьких місць)?
каменеметал

2

Будь-яка документація, орієнтована на програмування, яка не інформує програмістів про вартість складних процедур / методів, є помилкою.

  • Ми шукаємо методи без побічних ефектів.

  • Якщо виконання методу має складну часову складність та / або складність пам’яті, окрім O(1), у середовищах, обмежених пам'яттю або обмеженими часом, це може вважатися побічним ефектом .

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


1

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

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


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

1
@Blrfl: так, кешування потрібно робити на Personуроці, але я думаю, що питання було призначене як загальне, і це Person.ageбув лише приклад. Напевно, є деякі випадки, коли виклику абоненту було б більше сенсу вибирати - можливо, у виклику є два різні алгоритми для обчислення одного і того ж значення: один швидкий, але неточний, один набагато повільніше, але більш точний (3D-рендерінг приходить в голову як одне місце де це може статися), і в документації має бути зазначено це.
FrustratedWithFormsDesigner

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

0

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

Якщо властивості не мають точного відносини (наприклад , тому що вони floatабо doubleзамість int), то це може виявитися необхідним документом , які властивості «визначити» значення класу. Наприклад, навіть якщо Leftплюс Widthмає бути рівним Right, математика з плаваючою комою часто не є точною. Наприклад, припустимо, Rectangleщо тип, який використовує, Floatприймає Leftі Widthяк параметри конструктора будуються з Leftзаданими як 1234567fі Widthяк 1.1f. Найкраще floatпредставлення суми - 1234568.125 [яке може відображатися як 1234568.13]; наступний менший float- 1234568.0. Якщо клас насправді зберігає LeftіWidth, він може повідомити про значення ширини, як було зазначено. Однак, якщо конструктор обчислюється на Rightпідставі переданих Leftі Width, а потім обчислюються на Widthпідставі Leftі Rightбуло б повідомити ширину, 1.25fа не переданий 1.1f.

З мінливими класами все може бути ще цікавішим, оскільки зміна одного із взаємопов'язаних значень передбачає зміну хоча б одне одного, але не завжди може бути зрозуміло, яке саме. У деяких випадках це може бути краще , щоб уникнути методів , які «набір» одна властивість , як такої, але замість того, щоб або мати методи, наприклад , до SetLeftAndWidthабо SetLeftAndRight, або дати зрозуміти , які властивості уточнюються і які змінюються (наприклад MoveRightEdgeToSetWidth, ChangeWidthToSetLeftEdgeчи MoveShapeToSetRightEdge) .

Іноді може бути корисним клас, який відстежує, які значення властивостей були визначені та які були обчислені з інших. Наприклад, клас "момент у часі" може включати абсолютний час, місцевий час та зміщення часового поясу. Як і для багатьох таких типів, з урахуванням будь-яких двох відомостей, один може обчислити третій. Знаючи якийІнформація, яка була обчислена, однак, іноді може бути важливою. Наприклад, припустимо, що подія записана як така, що сталася в "17:00 UTC, часовий пояс -5, місцевий час 12:00 вечора", а одна пізніше виявляє, що часовий пояс повинен був бути -6. Якщо відомо, що UTC був записаний із сервера, запис слід виправити на "18:00 UTC, часовий пояс -6, місцевий час 12:00 вечора"; якщо хтось ввімкнув місцевий час поза годинником, він повинен бути "17:00 UTC, часовий пояс -6, місцевий час 11:00". Не знаючи, чи слід глобальний чи локальний час вважати "більш правдоподібним", однак неможливо знати, яку корекцію слід застосувати. Якщо, однак, записаний запис, який час був визначений, зміни часового поясу могли залишити цей один у спокої, змінивши інший.


0

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

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

Ось таке, що я бачу багато:

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

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

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

Тож ідіть фігурою.


0

Додавання деталі реалізації , як «вартість чи ні» або «Інформація про виконання» роблять його більш difficuilt зберегти код і документ в синхронізації .

Приклад:

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

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


0

Коли прийнята відповідь приходить до висновку:

Отже: проблеми з виконанням документа.

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

Так що до сих пір Person.ageдля return current_year - self.birth_dateале якщо метод використовує цикл для обчислення віку (та):Person.calculateAge()

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