Документация в ООП должна избегать указания, выполняет ли «получатель» какие-либо вычисления?


39

Программа CS моей школы избегает каких-либо упоминаний об объектно-ориентированном программировании, поэтому я немного читал сам, чтобы дополнить его - в частности, конструкцию объектно-ориентированного программного обеспечения Бертрана Мейера.

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

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

Будет разработана стандартная клиентская документация для класса, известная как краткая форма класса, чтобы не раскрывать, является ли данная функция атрибутом или функцией (в тех случаях, когда она может быть любой).

то есть он утверждает, что даже в документации для класса следует избегать указания, выполняет ли «получатель» какие-либо вычисления.

Этого я не понимаю. Разве документация не является единственным местом, где было бы важно информировать пользователей об этом различии? Если бы я проектировал базу данных, заполненную Personобъектами, не было бы важно знать, Person.ageявляется ли это дорогостоящим вызовом, или нет , поэтому я мог бы решить, следует ли реализовывать какой-либо кеш для него? Я неправильно понял, что он говорит, или он просто особенно яркий пример философии дизайна ООП?


1
Интересный вопрос. Недавно я спросил о чем-то очень похожем: как бы я спроектировал интерфейс таким образом, чтобы было понятно, какие свойства могут изменить их значение, а какие останутся постоянными? , И я получил хороший ответ, указывающий на документацию, то есть именно то, против чего, кажется, выступает Бертран Мейер.
stakx

Я не читал книгу. Приводит ли Мейер какие-либо примеры стиля документации, который он рекомендует? Мне трудно представить, что вы описали, работая на любом языке.
user16764

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

@AndreasScheinert - вы имеете в виду это ? Я посмеивался над «все из-за подковообразного гвоздя», но, похоже, разглагольствовал на зло объектно-ориентированного программирования.
Патрик Коллинз

1
@PatrickCollins да вот это: steve-yegge.blogspot.com/2006/03/… ! Это дает некоторые моменты для размышления, другие: вы должны превратить ваши объекты в структуры данных с помощью (ab) с помощью сеттеров.
AndreasScheinert

Ответы:


58

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

Но кодер, использующий ваш класс, не должен знать, реализовали ли вы:

return currentAge;

или:

return getCurrentYear() - yearBorn;

Характеристики производительности между этими двумя подходами настолько минимальны, что это не должно иметь значения. Программист, использующий ваш класс, действительно не должен заботиться о том, что у вас есть. Это точка зрения Мейера.

Но это не всегда так, например, предположим, что у вас есть метод size для контейнера. Это может быть реализовано:

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 :: sort сортирует мои вещи, если это происходит правильно и эффективно. Ваш класс также не должен документировать, как он рассчитывает вещи, но если что-то имеет неожиданный профиль производительности, запишите это.


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

2
= (Что- lenто столь же тривиальное, как в Python, не в состоянии это сделать ... (По крайней мере, в некоторых ситуациях, O(n)как мы узнали в проекте в колледже, когда я предлагал сохранять длину, а не пересчитывать ее при каждой итерации цикла)
Izkata

@Izkata, любопытно. Вы помните, какая структура была O(n)?
Уинстон Эверт

@WinstonEwert К сожалению, нет. Это было 4+ года назад в проекте Data Mining, и я предложил его своему другу только на догадку, потому что я работал с C в другом классе ...
Изката,

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

16

С академической точки зрения или с точки зрения пуристов CS, конечно, невозможно описать в документации что-либо о внутренностях реализации функции. Это потому, что в идеале пользователь класса не должен делать никаких предположений о внутренней реализации класса. Если реализация изменится, в идеале ни один пользователь не заметит этого - функция создает абстракцию, а внутреннее устройство должно быть полностью скрыто.

Тем не менее, большинство реальных программ страдают от «Закона утечек абстракций» Джоэла Спольски , который гласит:

«Все нетривиальные абстракции, в некоторой степени, являются утечками».

Это означает, что практически невозможно создать полную абстракцию черного ящика со сложными функциями. И типичным симптомом этого являются проблемы с производительностью. Таким образом, для реальных программ может стать очень важным, какие вызовы дороги, а какие нет, и хорошая документация должна включать эту информацию (или она должна указывать, где пользователь класса может делать предположения о производительности, а где нет). ).

Итак, мой совет: включайте информацию о потенциально дорогих звонках, если вы пишете документы для реальной программы, и исключайте ее для программы, которую вы пишете только для образовательных целей вашего курса CS, учитывая, что любые соображения производительности должны быть сохранены намеренно выходит за рамки.


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

12

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

Каждая деталь, которую вы даете в документации, похожа на контракт, который должен соблюдать класс. Следует сообщить о важном поведении. Часто вы увидите указание сложности с большой буквой O. Но вы обычно хотите быть коротким и по существу.


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

Я поддерживаю это. Далее, в общем, стараемся свести к минимуму потребность в геттерах, предоставляя методы поведения.
sevenforce

9

Если бы я проектировал базу данных, заполненную объектами Person, не было бы важно знать, является ли Person.age дорогим вызовом?

Да.

Вот почему я иногда использую Find()функции, чтобы указать, что вызов может занять некоторое время. Это скорее соглашение, чем все остальное. Время, необходимое для функции или атрибута возврата не имеет никакого значения для программы (хотя это может пользователь), хотя среди программистов есть это ожидание того, что, если он объявлен как атрибут, стоимость назвать это должно быть низкий.

В любом случае в самом коде должно быть достаточно информации, чтобы определить, является ли что-то функцией или атрибутом, поэтому я не вижу необходимости говорить об этом в документации.


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

@Telastyn: я никогда не думал о документации в такой манере; то есть, что это должно быть сделано на уровне интерфейса. Кажется очевидным сейчас. +1 за этот ценный комментарий.
Stakx

Мне очень нравится этот ответ. Прекрасный пример того, что вы описываете, что производительность не имеет значения для самой программы, был бы, если бы Person был сущностью, полученной из службы RESTful. GET присущ, но не очевидно, будет ли это дешево или дорого. Это, конечно, не обязательно ООП, но смысл тот же.
maple_shaft

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

Откуда эта конвенция? Думая о Java, я ожидал бы его наоборот: getметод эквивалентен доступу к атрибутам и поэтому не дорогой.
sevenforce

3

Важно отметить, что первое издание этой книги было написано в 1988 году, в первые годы ООП. Эти люди работали с более чисто объектно-ориентированными языками, которые широко используются сегодня. Наши самые популярные на сегодняшний день языки OO - C ++, C # и Java - имеют некоторые довольно существенные отличия от того, как работали ранние, более чисто OO-языки.

В таких языках, как C ++ и Java, вы должны различать доступ к атрибуту и ​​вызов метода. Существует разница между instance.getter_methodи instance.getter_method(). Один действительно получает вашу ценность, а другой нет.

При работе с более чисто ОО-языком, с точки зрения Smalltalk или Ruby (который, как представляется, используется язык Eiffel, используемый в этой книге), он становится совершенно верным советом. Эти языки будут неявно вызывать методы для вас. Там не становится никакой разницы между instance.attributeи instance.getter_method.

Я бы не стал потеть эту точку зрения или воспринимать ее слишком догматично. Намерение хорошее - вы не хотите, чтобы пользователи вашего класса беспокоились о нерелевантных деталях реализации - но это не переводит чисто в синтаксис многих современных языков.


1
Очень важный момент при рассмотрении года, в котором было сделано предложение. Nit: Smalltalk и Simula датируются 60-ми и 70-ми годами, так что 88 - это вряд ли "первые дни".
Люзер Дрог

2

Как пользователь, вам не нужно знать, как что-то реализовано.

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


3
Всегда ли так случается, что вычислительно-дорогой метод является ошибкой? В качестве тривиального примера предположим, что меня интересует суммирование длин массива строк. Внутренне я не знаю, являются ли строки в моем языке в стиле Паскаля или в стиле Си. В первом случае, поскольку строки «знают» свою длину, я могу ожидать, что мой цикл суммирования длины будет принимать линейное время, зависящее от количества строк. Я также должен знать, что операции, которые изменяют длину строк, будут иметь накладные расходы, связанные с ними, так как 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, математика с плавающей точкой часто неточна. Например, предположим, что a 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. У объектов есть «измененный» бит, говорящий, если они, в некотором смысле, устарели. Достаточно просто, но тогда у них есть подчиненные объекты, поэтому легко позволить «модифицированному» быть функцией, которая суммирует все подчиненные объекты. Тогда, если есть несколько уровней подчиненных объектов (иногда совместно использующих один и тот же объект более одного раза), простые «Get» из «модифицированного» свойства могут в конечном итоге занять здоровую долю времени выполнения.

  2. Когда объект каким-либо образом модифицируется, предполагается, что другие объекты, разбросанные по всему программному обеспечению, должны быть «уведомлены». Это может происходить в нескольких слоях структуры данных, окнах и т. Д., Написанных разными программистами, и иногда повторяться в бесконечных рекурсиях, от которых необходимо защищаться. Даже если все авторы этих обработчиков уведомлений достаточно осторожны, чтобы не тратить время, все сложное взаимодействие может закончиться использованием непредсказуемой и мучительно большой доли времени выполнения, и предположение о том, что оно просто «необходимо», сделано беспечно.

Итак, мне нравится видеть классы, которые представляют хороший чистый абстрактный интерфейс с внешним миром, но мне нравится иметь некоторое представление о том, как они работают, хотя бы для того, чтобы понять, какую работу они спасают меня. Но помимо этого я склонен чувствовать, что «меньше значит больше». Люди настолько очарованы структурой данных, что думают, что чем больше, тем лучше, и когда я занимаюсь настройкой производительности, универсальная причина проблем с производительностью - рабская приверженность раздутым структурам данных, созданным так, как их учат.

Так что пойди разберись.


0

Добавление деталей реализации, таких как «вычислить или нет» или «информация о производительности», затрудняет синхронизацию кода и документа .

Пример:

Если у вас есть метод «дорогой производительности», вы хотите документировать «дорогой» также для всех классов, которые используют метод? Что делать, если вы измените реализацию, чтобы быть не дорогой больше. Вы хотите обновить эту информацию и для всех потребителей?

Конечно, для сопровождающего кода неплохо бы получить всю важную информацию из документации кода, но я не похож на документацию, в которой утверждается, что что-то недопустимо (не синхронизировано с кодом)


0

По принятому ответу приходит к выводу:

Итак: проблемы с документацией.

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.