Опубликован: 23.10.2005 | Доступ: свободный | Студентов: 4086 / 201 | Оценка: 4.44 / 4.19 | Длительность: 33:04:00
Специальности: Программист
Лекция 5:

Принципы проектирования класса

Размер класса: Подход списка требований

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

Следует ли подобным образом сосредоточиться на рассмотрении размеров класса как целого? Уверенного ответа здесь нет.

Определение размера класса

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

Все же остаются два вопроса:

  • Скрытие информации: следует ли учитывать все компоненты ( внутренний размер) или только экспортируемые ( внешний размер)?
  • Наследование: следует ли учитывать только непосредственные компоненты, введенные в самом классе, - непосредственный (immediate) размер - или считать все компоненты, включая наследованные от всех предков, - плоский (flat) размер, связанный с понятием плоской формы класса. Возможно, следует считать только непосредственные компоненты, присоединяя к ним компоненты, модифицируемые в классе через переопределение и эффективизацию, не учитывая переименования, которое не влияет на возрастающий (incremental) размер?

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

Поддержка согласованности

Некоторые авторы, в том числе Поль Джонсон ([Johnson 1995]) настаивают на ограничении размеров класса:

Проектировщики класса часто пытаются включить в него много компонентов. 
В результате, в интерфейсе появляется несколько обще используемых компонентов 
и довольно много странных подпрограмм. Совсем плохо, когда список компонентов велик.

Из опыта ISE следует другая точка зрения. Мы полагаем, что сам по себе размер класса не создает проблем. Хотя большинство классов относительно невелико (от нескольких компонентов до дюжины), встречаются и большие классы (от 60 до 80 компонентов, а иногда и больше), и с ними не возникает никаких особых проблем, если они хорошо спроектированы.

Этот опыт привел нас к подходу, называемому список требований (shopping list). Реализация не ухудшается при добавлении в класс компонентов, связанных с ним концептуально. Если вы колеблетесь, включать ли в класс экспортируемый компонент, то вас не должен беспокоить размер класса. Единственный критерий, подлежащий учету, - это согласованность компонента с остальными членами класса. Этот критерий отражен в следующей рекомендации:

Совет: Список Требований

При рассмотрении добавления нового экспортируемого компонента следите за следующими правилами:

  • S1 Компонент должен соответствовать абстракции данных, задающей класс.
  • S2 Он должен быть совместимым с другими компонентами класса.
  • S3 Он не должен дублировать цель другого компонента класса.
  • S4 Он должен поддерживать инвариант класса.

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

Интересно отметить, что тот же пример - класс string - один из самых больших классов в библиотеках ISE, был подвергнут критике из-за своих размеров Полем Джонсоном. Но реакция пользователей библиотеки в течение многих лет была противоположной, - они просили добавления свойств. Класс, хотя и обладал богатой функциональностью, но был прост в использовании, поскольку все компоненты четко применяли одну и ту же абстракцию - строку символов, над которой по ее природе определено много операций, начиная от извлечения подстроки и замены до конкатенации и глобальной подстановки.

Класс STRING показывает, что большой не означает сложный. Некоторые абстракции по своей природе обладают многими компонентами. Процитирую Валдена и Нерсона ([Walden 1995]):

Класс, обрабатывающий документ, содержит 100 различных операций, позволяющих 
выполнять различные вариации со шрифтом, <...> фактически имеет дело 
с небольшим числом лежащих в его основе концепций, довольно простых и легко 
усваиваемых. Простота выбора надлежащей операции поддерживается прекрасно 
уорганизованным руководством.

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

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

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

infix "+", infix "-", infix "*", infix "/"

Вычисление выражения z1 + z2 создаст новый объект, представляющий сумму z1 и z2, аналогично для других функций. Другие клиенты, или тот же клиент в другой ситуации предпочтет процедурную версию операции, где вызов z1.add(z2) обновляет объект z1. Теоретически избыточно включать и функции и процедуры в класс, поскольку они взаимно заменимы. На практике удобно иметь обе версии по меньшей мере по трем причинам: удобства, эффективности, повторного использования.

Запреты и послабления

В последнем примере два множества компонентов, будучи теоретически избыточными, практически являются различными. Конечно же, не следует вводить новый компонент, если есть старый, выполняющий аналогичную работу, о чем говорит предложение S3 в рекомендациях Списка Требований. Это предложение более требовательное, чем может показаться с первого взгляда. В частности:

  • Предположим, вы хотите изменить порядок аргументов в подпрограмме для совместимости с другими подпрограммами. Но вас сдерживает совместимость с уже существующим ПО. Решение не может состоять в том, чтобы иметь оба компонента с тем же статусом, - это противоречило бы рекомендации S3. Вместо этого следует использовать библиотечный механизм эволюции obsolete, который чуть позже будет описан в этой лекции.
  • Аналогично следует поступать для обеспечения значений аргументов, требуемых некоторой программе. Не следует предоставлять две версии, одну со специальными аргументами, а другую - общую, основанную на умолчаниях, как обсуждалось ране в этой лекции. Сделайте один интерфейс официальным, а другой обеспечьте через механизм obsolete.
  • Если вы колеблетесь в выборе имени компонента, следует почти всегда сопротивляться попытке сделать имена синонимами. В библиотеке ISE единственное исключение сделано для фундаментальных компонентов, имеющих инфиксное имя и идентификатор, например доступ к массиву может осуществляться двояко: my_array.item (some_index) или my_array @ some_index. Каждая форма предпочтительнее в определенном контексте. Но это редкая ситуация. Как правило, проектировщик должен выбрать имя, не перекладывая ответственность на клиентов.

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

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

Следует связать Рекомендации Списка Требований с предшествующей дискуссией о размере компонентов. Трудности использования класса определяются не числом его компонентов, а их индивидуальной сложностью. Более точно, размер класса является некоторой проблемой лишь на первых порах. После завершения этапа освоения разработчик будет постоянно иметь дело с компонентами, скорее всего, подмножеством компонентов, Размер компонента становится приоритетным, а размер класса перестает быть таковым. Не следует пользоваться численными критериями типа: "никакой класс не должен иметь более n строк или m компонентов", - разделение класса на такой основе может лишь сделать его более трудным в использовании.

Урок для разработчиков класса, вытекающий из Рекомендаций Списка Требований: следует заботиться о качестве класса, в частности о его концептуальной целостности и размере его компонентов, но не о размере самого класса.