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

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

Много ли аргументов должно быть у компонента?

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

Важность числа аргументов

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

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

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

nonlinear_ode
    (equation_count: in INTEGER;
     epsilon: in out DOUBLE;
    func: procedure    (eq_count: INTEGER; a: DOUBLE; eps: DOUBLE;
     b: ARRAY [DOUBLE]; cm: pointer Libtype)
    left_count, coupled_count: in INTEGER;
    ...)
    [И так далее. Всего 19 аргументов, включающих:
            - 4 in out значения;
            - 3 массива, используемы как входные и выходные;
        - 6 функций, каждая имеющая 6 - 7 аргументов, из которых
            2 или 3 являются массивами!]

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

Некоторые свойства делают эту процедуру особенно сложной в использовании:

  • Большинство аргументов имеют статус in out, означающий необходимость их инициализации перед вызовом и обновление их значений в процессе работы программы. Например, аргумент epsilon указывает на входе, требуется ли продолжение функций (да, если меньше 0, если между 0 и 1, то продолжение требуется, если epsilon < vprecision и т. д.). На выходе аргумент представляет оценку приращения.
  • Многие из аргументов как самой процедуры, так и функций, являющихся ее аргументами, заданы массивами, служащими для передачи информации в процедуру и обратно.
  • Некоторые аргументы служат для спецификации большого числа возможностей по обработке ошибок (прервать обработку, записывать сообщения в файл, продолжать в любых ситуациях...)

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

Часть этой сложности, несомненно, связана со сложностью самой проблемы. Но все можно сделать лучше. ОО-библиотека для численных вычислений - Math ([Dubois 1997]) - предлагает совсем другой подход, согласованный с концепциями объектной технологии и принципами этой книги. Как ранее упоминалось, эта библиотека служит примером использования объектной технологии для упаковки старого программного обеспечения - ее ядром является не ОО-библиотека. Было бы абсурдно не использовать хорошо зарекомендовавшие себя алгоритмы, и прекрасно, когда им придается современный интерфейс, привлекательный для клиентов. Базисная подпрограмма nonlinear_ode имеет в ней форму:

solve
    -- Решить проблему, записав ответ в x и y

У нее теперь вообще нет аргументов! Просто создается экземпляр класса GENERAL_BOUNDARY_VALUE_PROBLEM, представляющий требуемую задачу, устанавливаются его свойства, отличные от значений, принятых по умолчанию. При этом могут вызываться подходящие процедуры, присоединенные к объекту, решающему проблему. Затем вызывается метод solve для этого объекта. Атрибуты класса x и y дают возможность анализа ответа.

Таким образом, применение ОО-техники дает существенный эффект по сокращению числа аргументов. Измерения, сделанные для библиотек ISE, показывают, что среднее число аргументов находится в пределах от 0,4 для базовых библиотек Base до 0,7 для графической библиотеки Vision. Для корректного сравнения с не ОО-библиотеками следует добавлять единицу, поскольку в объектном случае мы учитываем два аргумента в вызове x.f (a, b) против трех в необъектной программе - f (x, a, b). Но все равно сравнение явно в пользу объектной технологии, так как число аргументов, как мы видели, в необъектном случае достигает 19 аргументов и часто имеет значения 5, 10 или 15.

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

Операнды и необязательные параметры (опции)

Аргументы подпрограммы могут быть одного из двух возможных видов: операнды и опции.

Для понимания разницы рассмотрим пример класса DOCUMENT и его процедуру печати print. Предположим - просто для конкретизации изложения, - что печать основана на Postscript. Типичный вызов иллюстрирует возможный интерфейс, не совместимый с ниже излагаемыми принципами. Вот пример:

my_document.print (printer_name, paper_size, color_or_not,
                 postscript_level, print_resolution)

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

Этот пример иллюстрирует разницу между операндами и опциями:

Определение: операнд и опция

Аргумент является операндом, если он представляет объект, с которым оперирует программа.

Аргумент является опцией, если он задает режим выполнения операции.

Это определение носит общий характер и оставляет место для неопределенности. Существуют два прямых критерия:

Как отличать опции от операндов

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

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

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

Принцип

Определение операндов и опций дает правило для аргументов:

Принцип операндов

Аргументы подпрограмм должны быть только операндами, но не опциями.

Два случая ослабления правила, не рассматриваемые как исключения, упоминаются ниже.

В стиле, продвигаемом этим принципом, опции к операциям устанавливаются не при вызове операции, а при вызове специальных процедур, задачей которых является установка опций:

my_document.set_printing_size ("A4")
my_document.set_color
my_document.print                     -- Совсем нет аргументов

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

Для любого типа, отличного от Boolean, процедуры, устанавливающие опцию, имеют ровно один аргумент соответствующего типа, как это проиллюстрировано при вызове set_printing_size. Стандартное имя для таких процедур имеет форму set_property_name. Заметьте, аргументы таких процедур сами удовлетворяют Принципу Операнда. Так, например, аргумент, задающий размер страницы, является опцией для процедуры print, но операндом для установочной процедуры set_printing_size.

Для булевских процедур та же техника приводила бы к аргументу, принимающему всего два значения - True or False. Оказывается, что пользователи часто забывают, какая из двух возможностей соответствует True, поэтому лучше использовать пару процедур с удобными именами в форме set_property_name и set_no_property_name, например, set_color и set_no_color, во втором случае можно предложить и другой вариант set_black_and_white.

Применение Принципа Операндов дает несколько преимуществ:

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

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

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

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

Преимущества, обеспечиваемые Принципом Операндов

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

Этот стиль вскоре становится естественным для разработчиков. (Мы сделали его частью стандарта в ISE.) Вы создаете требуемые объекты, устанавливаете любые значения, отличающиеся от принятых по умолчанию, затем применяете нужные операции. Эта схема была показана на примере solve в библиотеке Math. Она, конечно, предпочтительнее передачи 19 аргументов.

Исключения из Принципа Операндов?

Принцип Операндов универсально применим. Но два специальных случая, не являясь настоящими исключениями, требуют некоторой адаптации.

Во-первых, можно получить преимущества от существования множества процедур создания. Класс поддерживает разные способы инициализации объектов, вызывая create x.make_specific (argument, ...), где make_specific - соответствующая процедура создания. Можно ослабить Принцип Операндов для таких процедур, облегчая задачу клиенту, предлагая различные способы установки значений, отличные от значений по умолчанию. Однако имеют место два ограничения:

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

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

my_document.set_printing_size ("...")
my_document.set_printer_name ("...")
my_document.print

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

print_with_size_and_printer (printer_name: STRING; size: SIZE_SPECIFICATION)

Это предполагает, конечно, что основная минимальная программа ( print в нашем примере) остается доступной и что новая программа является дополнением, упрощая задачу клиента в тех случаях, когда она действительно часто встречается.

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

Контрольный перечень

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

Таблица 5.1. Описание опций класса
Option Initialized Queried Set
Paper size

default:A4 (international)

make_LTR: LTR (US)

size

set_size

set_LTR

set_A4

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

  • Initialized помогает обнаружить ошибочную инициализацию, особенно в случае умолчаний. (Если, например, по умолчанию хотим установить цветную печать, то значение опции Black_and_white_only должно быть установлено как false.)
  • Queried помогает обнаружить ошибки доступа. Заметьте, программа, получающая объект, может изменять опции в своих собственных целях, но затем восстанавливать их начальное состояние. Это возможно, если разрешен запрос начального состояния.
  • Set помогает обнаружить пропущенные опции установочных процедур.

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

Александр Качанов
Александр Качанов
Япония, Токио
Янош Орос
Янош Орос
Украина, Киев