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

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

Стратегия

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

Поэтому Принцип Разделения Команд и Запросов является методологическим предписанием, а не языковым ограничением. Это не снижает его важности.

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

Возражения

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

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

target.some_operation (...)
how_did_it_go := target.status

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

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

Недоразумение связано с тем, что в параллельном контексте имеется некоторая операция get доступа к буферу - параллельному аналогу очереди. Такая функция выполняется без прерываний и в нашей терминологии реализует как вызов item, так и remove. Элемент возвращается в качестве результата функции, а удаление из буфера является побочным эффектом. Но использование подобных примеров в качестве аргументов в защиту функций get -стиля смешивает два понятия. Что нам действительно необходимо в параллельном контексте - это способ, дающий клиенту исключительный доступ к заготовленному элементу для выполнения некоторых операций. Имея такой механизм, можно защитить клиента, когда он выполняет последовательно операции:

x := buffer.item; buffer.remove

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

buffer.remove; buffer.remove

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

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

Законные побочные эффекты: пример

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

Рассмотрим реализацию комплексных чисел. Как и в случае с точками, обсуждаемом в предыдущих лекциях, возможны два представления - декартово (с координатами x и y ) и полярное (с расстоянием r и углом q ). Какое из них выбрать? Простого ответа нет. Если, как обычно, обратиться к АТД, то разные применимые операции - сложение, вычитание, умножение и деление - и запросы для получения значений x, y, r и q эффективно выполняются для разных представлений (декартово представление лучше для сложения и умножения, полярное - для умножений и делений).

Можно было бы позволить клиенту решать, какое выбрать представление, но это делает классы трудными в использовании и нарушает принцип скрытия информации от клиента, которому нет дела до представления.

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

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

Пусть наш класс включает следующие операции:

class COMPLEX feature
    ... Объявления компонентов:
            infix "+", infix "-", infix "*", infix "/",
            add, subtract, multiply, divide,
            x, y, rho, theta, ...
end

Запросы x, y, rho и theta представляют экспортируемые функции, возвращающие вещественные значения. Они всегда определены (исключая theta для комплексного числа 0 ). Помимо инфиксных функций " + " и других предполагаем процедуру add и другие. Вызов: z1 + z2 дает новое комплексное число, вызов z1.add (z2) изменяет z1. На практике могут понадобиться только функции или только процедуры.

Наш класс включает следующие секретные (закрытые) атрибуты:

cartesian_ready: BOOLEAN
polar_ready: BOOLEAN
private_x, private_y, private_rho, private_theta: REAL

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

invariant
    cartesian_ready or polar_ready
    polar_ready implies (0 <= private_theta and private_theta <= Two_pi)
        -- cartesian_ready implies (private_x and private_y являются текущими)
        -- polar_ready implies (private_rho and private_theta являются текущими)

Последние два предложения выражены неформально в форме комментария.

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

Две закрытые процедуры доступны для проведения изменений представления:

prepare_cartesian is
            -- Сделать доступным декартово представление
    do
        if not cartesian_ready then
                check polar_ready end
                -- Поскольку инвариант требует, чтобы одно
                -- из двух представлений было текущим
            private_x := private_rho * cos (private_theta)
            private_y := private_rho * sin (private_theta)
            cartesian_ready := True
                -- Здесь cartesian_ready и polar_ready равны true:
                -- Оба представления являются текущими
        end
    ensure
        cartesian_ready
    end
prepare_polar is
            -- Сделать доступным полярное представление
    do
        if not polar_ready then
                    check cartesian_ready end
            private_rho := sqrt (private_x ^ 2 + private_y ^ 2)
            private_theta := atan2 (private_y, private_x)
            polar_ready := True
                -- Здесь cartesian_ready и polar_ready равны true:
                -- Оба представления являются текущими
        end
    ensure
        polar_ready
    end

Функции cos, sin, sqrt и atan2 берутся из стандартной математической библиотеки, atan2(y, x) вычисляет arctangent(y/x).

Нам также нужны процедуры создания - make_cartesian и make_polar:

make_cartesian (a, b: REAL) is
        -- Инициализация: abscissa a, ordinate b
    do
        private_x := a; private_y := b
        cartesian_ready := True; polar_ready := False
    ensure
        cartesian_ready; not polar_ready
    end

и симметрично для make_polar.

Экспортируемые операции пишутся просто, начнем, например, с процедуры, имеющей варианты в зависимости от операции:

add (other: COMPLEX) is
        -- Добавляет значение other
    do
        prepare_cartesian; polar_ready := False
        private_x := x + other.x; private_y = y + other.y
    ensure
        x = old x + other.x; y = old y + other.y
        cartesian_ready; not polar_ready
    end

Заметьте, в постусловии важно использовать x и y, а не private_x и private_y, которые могут не быть текущими перед вызовом.

divide (z: COMPLEX) is
        -- Divide by z.
    require
        z.rho /= 0
        -- Численное выражение дает более реалистичное предусловие
    do
        prepare_polar; cartesian_ready := False
        private_rho := rho / other.rho
        private_theta = (theta - other.theta) \\ Two_pi
                        -- \\ - остаток от деления
    ensure
         rho = old rho / other.rho
         theta = (old theta - other.theta) \\ Two_pi
        polar_ready; not cartesian_ready
    end

Аналогично для вычитания и умножения - subtract и multiply. (Предусловие и постусловие могут быть слегка адаптированы для учета особенностей операций с плавающей точкой.) Варианты функций следуют тому же образцу:

infix "+" (other: COMPLEX): COMPLEX is
        -- Сумма текущего числа и other
    do
        create Result.make_cartesian (x + other.x, y + other.y)
    ensure
        Result.x = x + other.x; Result.y = y + other.y
        Result.cartesian_ready
    end
infix "/" (z: COMPLEX): COMPLEX is
        -- Частное от деления текущего комплексного числа на z
    require
        z.rho /= 0
    do
    create Result.make_polar (rho / other.rho, (theta - other.theta) \\ Two_pi)
    ensure
        Result.rho = rho / other.rho
        Result.theta = (old theta - other.theta) \\ Two_pi
        Result.polar_ready
    end

Аналогично для infix "-" и infix "**".

Обратите внимание на последние предложения в постусловиях этих функций - cartesian_ready и polar_ready должны экспортироваться самому классу, появляясь в предложениях в форме feature {COMPLEX} ; они не экспортируются никакому другому классу.

Но где здесь побочные эффекты? В последних двух функциях они непосредственно не видны. Все дело в x, y, rho и theta - они являются хитроумными создателями побочных эффектов. Вычисление x или y приведет к изменению представления (вызовется prepare_cartesian ), если не подготовлено декартово представление. Все симметрично для rho и theta. Вот примеры для x и theta:

x: REAL is
        -- Abscissa
    do
        prepare_cartesian; Result := private_x
    end
theta: REAL is
        -- Angle
    do
        prepare_polar; Result := private_theta
    end

Функции y и rho подобны. Все эти функции вызывают процедуру, которая может включить изменение состояния. В отличие от add и его собратьев, однако, они не делают предыдущее представление неверным, когда вычисляется новое представление. Например, если x вызывается в состоянии с ложным значением cartesian_ready, оба представления (все четыре вещественных атрибута) станут текущими. Все это потому, что функциям разрешается производить побочные эффекты только на конкретных объектах, но не на ассоциированных абстрактных объектах. Выразим это свойство более формально: вычисление z.x или другой функции может изменять конкретный объект, связанный с z, скажем от c1 до c2, но всегда с гарантией того, что

a(c1) = a(c2)

где a - абстрактная функция. Объекты c1 и c2 могут быть различными, но они представляют один и тот же математический объект - комплексное число.

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

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