|
|
Конкретный язык программирования не может прямо поддерживать любое понятие любого метода проектирования. Если язык программирования не способен прямо представить понятие проектирования, следует установить удобное отображение конструкций, используемых в проекте, на языковые конструкции. Например, метод проектирования может использовать понятие делегирования, означающее, что всякая операция, которая не определена для класса A, должна выполняться в нем с помощью указателя p на соответствующий член класса B, в котором она определена. На С++ нельзя выразить это прямо. Однако, реализация этого понятия настолько в духе С++, что легко представить программу реализации:
class A { B* p; //... void f(); void ff(); }; class B { //... void f(); void g(); void h(); };
Тот факт, что В делегирует A с помощью указателя A::p, выражается в следующей записи:
class A { B* p; // делегирование с помощью p //... void f(); void ff(); void g() { p->g(); } // делегирование q() void h() { p->h(); } // делегирование h() };
Для программиста совершенно очевидно, что здесь происходит, однако здесь
явно нарушается принцип взаимнооднозначного соответствия. Такие
"программируемые" отношения трудно выразить на языках программирования,
и поэтому к ним трудно применять различные вспомогательные средства.
Например, такое средство может не отличить "делегирование" от B
к A с помощью A::p от любого другого использования B*.
Все-таки следует всюду, где это возможно, добиваться
взаимнооднозначного соответствия между понятиями проекта и понятиями
языка программирования. Оно дает определенную простоту и гарантирует,
что проект адекватно отображается в программе, что упрощает
работу программиста и вспомогательных средств.
Операции преобразований типа являются механизмом, с помощью которого
можно представить в языке класс программируемых отношений, а именно:
операция преобразования X::operator Y() гарантирует, что всюду,
где допустимо использование Y, можно применять и X. Такое же
отношение задает конструктор Y::Y(X). Отметим, что операция
преобразования типа (как и конструктор) скорее создает новый объект,
чем изменяет тип существующего объекта. Задать операцию преобразования
к функции Y - означает просто потребовать неявного применения
функции, возвращающей Y. Поскольку неявные применения операций
преобразования типа и операций, определяемых конструкторами, могут
привести к неприятностям, полезно проанализировать их в отдельности
еще в проекте.
Важно убедиться, что граф применений операций преобразования типа
не содержит циклов. Если они есть, возникает двусмысленная ситуация,
при которой типы, участвующие в циклах, становятся несовместимыми в
комбинации. Например:
class Big_int { //... friend Big_int operator+(Big_int,Big_int); //... operator Rational(); //... }; class Rational { //... friend Rational operator+(Rational,Rational); //... operator Big_int(); };
Типы Rational и Big_int не так гладко взаимодействуют, как можно было бы подумать:
void f(Rational r, Big_int i) { //... g(r+i); // ошибка, неоднозначность: // operator+(r,Rational(i)) или // operator+(Big_int(r),i) g(r,Rational(i)); // явное разрешение неопределенности g(Big_int(r),i); // еще одно }
Можно было бы избежать таких "взаимных" преобразований, сделав некоторые из них явными. Например, преобразование Big_int к типу Rational можно было бы задать явно с помощью функции make_Rational() вместо операции преобразования, тогда сложение в приведенном примере разрешалось бы как g(BIg_int(r),i). Если нельзя избежать "взаимных" операций преобразования типов, то нужно преодолевать возникающие столкновения или с помощью явных преобразований (как было показано), или с помощью определения нескольких различных версий бинарной операции (в нашем случае +).
| |