Игнорирование статического контроля типов
Рассмотрим вариант 3, относящийся к проекту, в котором игнорируется статический контроль типов. Распространенные доводы в пользу отказа на стадии проектирования от статического контроля типов сводятся к тому, что "типы - это продукт языков программирования", или что "более естественно рассуждать об объектах, не заботясь о типах", или "статический контроль типов вынуждает нас думать о реализации на слишком раннем этапе". Такой подход вполне допустим до тех пор, пока он работает и не приносит вреда. Вполне разумно на стадии проектирования не заботиться о деталях проверки типов, и часто вполне допустимо на стадии анализа и начальных стадиях проектирования полностью забыть о вопросах, связанных с типами. В то же время, классы и иерархии классов очень полезны на стадии проектирования, в частности, они дают нам большую определенность понятий, позволяют точно задать взаимоотношения между понятиями и помогают рассуждать о понятиях. По мере развития проекта эта определенность и точность преобразуется во все более конкретные утверждения о классах и их интерфейсах.
Важно понимать, что точно определенные и строго типизированные интерфейсы являются фундаментальным средством проектирования. Язык С++ был создан как раз с учетом этого. Строго типизированный интерфейс гарантирует, что только совместимые части программы могут быть скомпилированы и скомпонованы воедино, и тем самым позволяет делать относительно строгие допущения об этих частях. Эти допущения обеспечиваются системой типов языка. В результате сводятся к минимуму проверки на этапе выполнения, что повышает эффективность и приводит к значительному сокращению фазы интеграции частей проекта, реализованных разными программистами. Реальный положительный опыт интеграции системы со строго типизированными интерфейсами привел к тому, что вопросы интеграции вообще не фигурируют среди основных тем этой главы.
Рассмотрим следующую аналогию: в физическом мире мы постоянно соединяем различные устройства, и существует кажущееся бесконечным число стандартов на соединения. Главная особенность этих соединений: они специально спроектированы таким образом, чтобы сделать невозможным соединение двух устройств, нерассчитанных на него, то есть соединение должно быть сделано единственным правильным способом. Вы не можете подсоединить электробритву к розетке с высоким напряжением. Если бы вы смогли сделать это, то сожгли бы бритву или сгорели сами. Масса изобретательности была проявлена, чтобы добиться невозможности соединения двух несовместимых устройств. Альтернативой одновременного использования нескольких несовместимых устройств может послужить такое устройство, которое само себя защищает от несовместимых с ним устройств, подключающихся к его входу. Хорошим примером может служить стабилизатор напряжения. Поскольку идеальную совместимость устройств нельзя гарантировать только на "уровне соединения", иногда требуется более дорогая защита в электрической цепи, которая позволяет в динамике приспособиться или (и) защититься от скачков напряжения.
Но не следует впадать в другую крайность. Нельзя обнаружить все ошибки с помощью статического контроля. Например, даже программы с самым обширным статическим контролем уязвимы к сбоям аппаратуры. Но все же, в идеале нужно иметь большое разнообразие интерфейсов со статической типизацией с помощью типов из области приложения, см. $$12.4.
Может получиться, что проект, совершенно разумный на абстрактном уровне, столкнется с серьезными проблемами, если не учитывает ограничения базовых средств, в данном случае С++. Например, использование имен, а не типов для структурирования системы приведет к ненужным проблемам для системы типов С++ и, тем самым, может стать причиной ошибок и накладных расходов при выполнении. Рассмотрим три класса:
class X { // pseudo code, not C++
f()
g()
}
class Y {
g()
h()
}
class Z {
h()
f()
}
используемые некоторыми функциями бестипового проекта:
k(a, b, c) // pseudo code, not C++
{
a.f()
b.g()
c.h()
}
Здесь обращения
X x
Y y
Z z
k(x,y,z) // ok
k(z,x,y) // ok
будут успешными, поскольку k() просто требует, чтобы ее первый параметр имел операцию f(), второй параметр - операцию g(), а третий параметр - операцию h(). С другой стороны обращения
k(y,x,z); // fail
k(x,z,y); // fail
завершатся неудачно. Этот пример допускает совершенно разумные реализации на языках с полным динамическим контролем (например, Smalltalk или CLOS), но в С++ он не имеет прямого представления, поскольку язык требует, чтобы общность типов была реализована как отношение к базовому классу. Обычно примеры, подобные этому, можно представить на С++, если записывать утверждения об общности с помощью явных определений классов, но это потребует большого хитроумия и вспомогательных средств. Можно сделать, например, так:
class F {
virtual void f();
};
class G {
virtual void g();
};
class H {
virtual void h();
};
class X : public virtual F, public virtual G {
void f();
void g();
};
class Y : public virtual G, public virtual H {
void g();
void h();
};
class Z : public virtual H, public virtual F {
void h();
void f();
};
k(const F& a, const G& b, const H& c)
{
a.f();
b.g();
c.h();
}
main()
{
X x;
Y y;
Z z;
k(x,y,z); // ok
k(z,x,y); // ok
k(y,x,z); // error F required for first argument
k(x,z,y); // error G required for second argument
}
Обратите внимание, что сделав предположения k() о своих аргументах явными, мы переместили контроль ошибок с этапа выполнения на этап трансляции. Сложные примеры, подобные приведенному, возникают, когда пытаются реализовать на С++ проекты, сделанные на основе опыта работы с другими системами типов. Обычно это возможно, но в результате получается неестественная и неэффективная программа. Такое несовпадение между приемами проектирования и языком программирования можно сравнить с несовпадением при пословном переводе с одного естественного языка на другой. Ведь английский с немецкой грамматикой выглядит столь же неуклюже, как и немецкий с английской грамматикой, но оба языка могут быть доступны пониманию того, кто бегло говорит на одном из них.
Этот пример подтверждает тот вывод, что классы в программе являются конкретным воплощением понятий, используемых при проектировании, поэтому нечеткие отношения между классами приводят к нечеткости основных понятий проектирования.