Программа CS моей школы избегает каких-либо упоминаний об объектно-ориентированном программировании, поэтому я немного читал сам, чтобы дополнить его - в частности, конструкцию объектно-ориентированного программного обеспечения Бертрана Мейера.
Мейер неоднократно подчеркивал, что классы должны скрывать как можно больше информации об их реализации, что имеет смысл. В частности, он неоднократно утверждает, что атрибуты (т. Е. Статические неисчисляемые свойства классов) и процедуры (свойства классов, соответствующие вызовам функций / процедур) должны быть неотличимы друг от друга.
Например, если у класса Person
есть атрибут age
, он утверждает, что из обозначений должно быть невозможно определить, Person.age
соответствует ли он внутренне чему-либо подобному return current_year - self.birth_date
или просто return self.age
, где self.age
он был определен как постоянный атрибут. Это имеет смысл для меня. Тем не менее, он продолжает утверждать следующее:
Будет разработана стандартная клиентская документация для класса, известная как краткая форма класса, чтобы не раскрывать, является ли данная функция атрибутом или функцией (в тех случаях, когда она может быть любой).
то есть он утверждает, что даже в документации для класса следует избегать указания, выполняет ли «получатель» какие-либо вычисления.
Этого я не понимаю. Разве документация не является единственным местом, где было бы важно информировать пользователей об этом различии? Если бы я проектировал базу данных, заполненную Person
объектами, не было бы важно знать, Person.age
является ли это дорогостоящим вызовом, или нет , поэтому я мог бы решить, следует ли реализовывать какой-либо кеш для него? Я неправильно понял, что он говорит, или он просто особенно яркий пример философии дизайна ООП?
источник
Ответы:
Я не думаю, что смысл Майера в том, что вы не должны сообщать пользователю, когда у вас дорогая операция. Если ваша функция попадет в базу данных или сделает запрос к веб-серверу и потратит несколько часов на вычисления, другой код должен будет это знать.
Но кодер, использующий ваш класс, не должен знать, реализовали ли вы:
или:
Характеристики производительности между этими двумя подходами настолько минимальны, что это не должно иметь значения. Программист, использующий ваш класс, действительно не должен заботиться о том, что у вас есть. Это точка зрения Мейера.
Но это не всегда так, например, предположим, что у вас есть метод size для контейнера. Это может быть реализовано:
или
или это может быть:
Разница между первыми двумя не должна иметь значения. Но последний может иметь серьезные последствия для производительности. Вот почему STL, например, говорит, что
.size()
это такO(1)
. Он не документирует точно, как рассчитывается размер, но дает характеристики производительности.Итак : проблемы с документацией. Не документируйте детали реализации. Мне все равно, как std :: sort сортирует мои вещи, если это происходит правильно и эффективно. Ваш класс также не должен документировать, как он рассчитывает вещи, но если что-то имеет неожиданный профиль производительности, запишите это.
источник
// O(n) Traverses the entire user list.
len
то столь же тривиальное, как в Python, не в состоянии это сделать ... (По крайней мере, в некоторых ситуациях,O(n)
как мы узнали в проекте в колледже, когда я предлагал сохранять длину, а не пересчитывать ее при каждой итерации цикла)O(n)
?С академической точки зрения или с точки зрения пуристов CS, конечно, невозможно описать в документации что-либо о внутренностях реализации функции. Это потому, что в идеале пользователь класса не должен делать никаких предположений о внутренней реализации класса. Если реализация изменится, в идеале ни один пользователь не заметит этого - функция создает абстракцию, а внутреннее устройство должно быть полностью скрыто.
Тем не менее, большинство реальных программ страдают от «Закона утечек абстракций» Джоэла Спольски , который гласит:
Это означает, что практически невозможно создать полную абстракцию черного ящика со сложными функциями. И типичным симптомом этого являются проблемы с производительностью. Таким образом, для реальных программ может стать очень важным, какие вызовы дороги, а какие нет, и хорошая документация должна включать эту информацию (или она должна указывать, где пользователь класса может делать предположения о производительности, а где нет). ).
Итак, мой совет: включайте информацию о потенциально дорогих звонках, если вы пишете документы для реальной программы, и исключайте ее для программы, которую вы пишете только для образовательных целей вашего курса CS, учитывая, что любые соображения производительности должны быть сохранены намеренно выходит за рамки.
источник
Вы можете написать, если данный вызов дорогой или нет. Лучше использовать соглашение об именах, например,
getAge
для быстрого доступаloadAge
илиfetchAge
для дорогого поиска. Вы определенно хотите сообщить пользователю, выполняет ли метод какой-либо ввод-вывод.Каждая деталь, которую вы даете в документации, похожа на контракт, который должен соблюдать класс. Следует сообщить о важном поведении. Часто вы увидите указание сложности с большой буквой O. Но вы обычно хотите быть коротким и по существу.
источник
Да.
Вот почему я иногда использую
Find()
функции, чтобы указать, что вызов может занять некоторое время. Это скорее соглашение, чем все остальное. Время, необходимое для функции или атрибута возврата не имеет никакого значения для программы (хотя это может пользователь), хотя среди программистов есть это ожидание того, что, если он объявлен как атрибут, стоимость назвать это должно быть низкий.В любом случае в самом коде должно быть достаточно информации, чтобы определить, является ли что-то функцией или атрибутом, поэтому я не вижу необходимости говорить об этом в документации.
источник
Get
методов над атрибутами для обозначения более тяжелой операции. Я видел достаточно кода, в котором разработчики предполагают, что свойство является просто средством доступа, и используют его несколько раз вместо сохранения значения в локальной переменной и, таким образом, выполняют очень сложный алгоритм более одного раза. Если не существует соглашения о том, чтобы не реализовывать такие свойства, и документация не намекает на сложность, тогда я желаю всем, кто должен поддерживать такое приложение, удачи.get
метод эквивалентен доступу к атрибутам и поэтому не дорогой.Важно отметить, что первое издание этой книги было написано в 1988 году, в первые годы ООП. Эти люди работали с более чисто объектно-ориентированными языками, которые широко используются сегодня. Наши самые популярные на сегодняшний день языки OO - C ++, C # и Java - имеют некоторые довольно существенные отличия от того, как работали ранние, более чисто OO-языки.
В таких языках, как C ++ и Java, вы должны различать доступ к атрибуту и вызов метода. Существует разница между
instance.getter_method
иinstance.getter_method()
. Один действительно получает вашу ценность, а другой нет.При работе с более чисто ОО-языком, с точки зрения Smalltalk или Ruby (который, как представляется, используется язык Eiffel, используемый в этой книге), он становится совершенно верным советом. Эти языки будут неявно вызывать методы для вас. Там не становится никакой разницы между
instance.attribute
иinstance.getter_method
.Я бы не стал потеть эту точку зрения или воспринимать ее слишком догматично. Намерение хорошее - вы не хотите, чтобы пользователи вашего класса беспокоились о нерелевантных деталях реализации - но это не переводит чисто в синтаксис многих современных языков.
источник
Как пользователь, вам не нужно знать, как что-то реализовано.
Если производительность является проблемой, что-то должно быть сделано внутри реализации класса, а не вокруг него. Следовательно, правильное действие - исправить реализацию класса или сообщить об ошибке сопровождающему.
источник
string.length
будут пересчитываться при каждом изменении.Любая часть документации, ориентированная на программиста, которая не информирует программистов о сложности процедур / методов, является ошибочной.
Мы ищем, чтобы производить методы без побочных эффектов.
Если выполнение метода имеет сложность во время выполнения и / или сложность памяти
O(1)
, отличную от того , в условиях ограниченного объема памяти или времени, можно считать, что он имеет побочные эффекты .Принцип наименьшего удивления нарушается , если метод делает что - то совершенно неожиданное - в данном случае, коробления памяти или тратить процессорное время.
источник
Я думаю, что вы правильно поняли его, но я также думаю, что у вас есть хорошая точка зрения. если
Person.age
это реализовано с дорогостоящим расчетом, то я думаю, что я хотел бы видеть это и в документации. Это может иметь значение между повторным вызовом (если это недорогая операция) или однократным вызовом и кэшированием значения (если это дорого). Я не знаю наверняка, но я думаю, что в этом случае Мейер может согласиться с тем, что в документацию должно быть включено предупреждение.Другой способ справиться с этим может состоять в том, чтобы ввести новый атрибут, имя которого подразумевает, что может иметь место длительное вычисление (например,
Person.ageCalculatedFromDB
), а затемPerson.age
возвращать значение, которое кэшируется в классе, но это не всегда может быть уместно и, как представляется, слишком усложняет вещи, на мой взгляд.источник
age
aPerson
, вы должны вызвать метод, чтобы получить его независимо. Если вызывающие стороны начинают делать слишком хитрые вещи, чтобы избежать необходимости делать вычисления, они рискуют, что их реализации не будут работать правильно, потому что они пересекли границу дня рождения. Дорогие реализации в классе будут проявляться в виде проблем с производительностью, которые могут быть устранены путем профилирования, а улучшения, такие как кэширование, могут быть выполнены в классе, где все вызывающие пользователи увидят преимущества (и правильные результаты).Person
классе, но я думаю, что вопрос был задуман как более общий, и этоPerson.age
был только пример. Возможно, в некоторых случаях было бы более разумно выбирать вызывающего абонента - возможно, вызываемый имеет два разных алгоритма для вычисления одного и того же значения: один быстрый, но неточный, другой намного медленнее, но более точный (3D-рендеринг приходит на ум как одно место где это может произойти), и документация должна упомянуть об этом.Документация для объектно-ориентированных классов часто предполагает компромисс между предоставлением сопровождающим класса гибкости в изменении его дизайна и предоставлением потребителям класса полного использования его потенциала. Если непреложный класс будет иметь ряд свойств , которые будут иметь определенные точно отношения друг с другом (например
Left
,Right
иWidth
свойства целочисленного координатного прямоугольника, выровненного по сетке), можно спроектировать класс для хранения любой комбинации двух свойств и вычислить третье, или можно спроектировать его для хранения всех трех. Если в интерфейсе ничего не ясно, какие свойства хранятся, программист класса может изменить дизайн в том случае, если это по какой-то причине окажется полезным. Напротив, если, например, два свойства представлены в видеfinal
полей, а третье - нет, тогда в будущих версиях класса всегда будут использоваться те же два свойства, что и в качестве «основы».Если свойства не имеют точного отношения (например , потому что они
float
илиdouble
вместоint
), то это может оказаться необходимым документом , какие свойства «определить» значение класса. Например, хотяLeft
плюсWidth
должен быть равенRight
, математика с плавающей точкой часто неточна. Например, предположим, что aRectangle
использует тип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». Однако, не зная, следует ли считать глобальное или местное время «более правдоподобным», невозможно определить, какое исправление следует применять. Однако, если запись отслеживала, какое время было указано, изменения часового пояса могут оставить один в покое, а другой - изменить.
источник
Все эти правила о том, как скрыть информацию в классах, имеют смысл, если предположить, что нужно защищать от этого кого-то из пользователей класса, который допустит ошибку, создав зависимость от внутренней реализации.
Хорошо встроить такую защиту, если у класса есть такая аудитория. Но когда пользователь пишет вызов функции в вашем классе, он доверяет вам свой банковский счет времени выполнения.
Вот что я часто вижу:
У объектов есть «измененный» бит, говорящий, если они, в некотором смысле, устарели. Достаточно просто, но тогда у них есть подчиненные объекты, поэтому легко позволить «модифицированному» быть функцией, которая суммирует все подчиненные объекты. Тогда, если есть несколько уровней подчиненных объектов (иногда совместно использующих один и тот же объект более одного раза), простые «Get» из «модифицированного» свойства могут в конечном итоге занять здоровую долю времени выполнения.
Когда объект каким-либо образом модифицируется, предполагается, что другие объекты, разбросанные по всему программному обеспечению, должны быть «уведомлены». Это может происходить в нескольких слоях структуры данных, окнах и т. Д., Написанных разными программистами, и иногда повторяться в бесконечных рекурсиях, от которых необходимо защищаться. Даже если все авторы этих обработчиков уведомлений достаточно осторожны, чтобы не тратить время, все сложное взаимодействие может закончиться использованием непредсказуемой и мучительно большой доли времени выполнения, и предположение о том, что оно просто «необходимо», сделано беспечно.
Итак, мне нравится видеть классы, которые представляют хороший чистый абстрактный интерфейс с внешним миром, но мне нравится иметь некоторое представление о том, как они работают, хотя бы для того, чтобы понять, какую работу они спасают меня. Но помимо этого я склонен чувствовать, что «меньше значит больше». Люди настолько очарованы структурой данных, что думают, что чем больше, тем лучше, и когда я занимаюсь настройкой производительности, универсальная причина проблем с производительностью - рабская приверженность раздутым структурам данных, созданным так, как их учат.
Так что пойди разберись.
источник
Добавление деталей реализации, таких как «вычислить или нет» или «информация о производительности», затрудняет синхронизацию кода и документа .
Пример:
Если у вас есть метод «дорогой производительности», вы хотите документировать «дорогой» также для всех классов, которые используют метод? Что делать, если вы измените реализацию, чтобы быть не дорогой больше. Вы хотите обновить эту информацию и для всех потребителей?
Конечно, для сопровождающего кода неплохо бы получить всю важную информацию из документации кода, но я не похож на документацию, в которой утверждается, что что-то недопустимо (не синхронизировано с кодом)
источник
По принятому ответу приходит к выводу:
и самодокументированный код считается лучше, чем документация, из чего следует, что имя метода должно содержать любые необычные результаты производительности.
Так что до сих пор
Person.age
дляreturn current_year - self.birth_date
но если метод использует цикл для вычисления возраста (да):Person.calculateAge()
источник