Если операции имеют одинаковую семантику, но разное число формальных параметров, можно добавить отсутствующие параметры, но игнорировать их при выполнении операции; например, операция обрисовки изображения на монохромный монитор не требует параметра цвет, но его можно добавить и не принимать во внимание при выполнении операции.
Некоторые операции имеют меньше параметров потому, что они являются частными случаями более общих операций; такие операции можно не реализовывать, сведя их к более общим операциям с соответствующими значениями параметров; например, добавление элемента в конец списка есть частный случай вставки элемента в список.
Одинаковые по смыслу атрибуты или операции разных классов могут иметь разные имена; такие атрибуты (операции) можно переименовать и перенести в класс, являющийся общим предком рассматриваемых классов.
Операция может быть определена не во всех классах некоторой группы классов; можно, тем не менее, вынести ее в их общий суперкласс, переопределив ее в подклассах, как пустую там, где она не нужна.
Использование делегирования операций можно пояснить на следующем примере (рисунок 2.9). Класс стек близок классу список, причем операциям стека push и pop соответствуют очевидные частные случаи операций списка add и remove. Если реализовать класс стек как подкласс класса список, то придется применять вместо операций push и pop более общие операции add и remove, следя за их параметрами, чтобы избежать записи или чтения из середины стека; это неудобно и чревато ошибками. Гораздо лучше объявить класс список телом класса стек (делегирование), обращаясь к операциям списка через операции стека. При этом, не меняя класса список, мы заменяем его интерфейс интерфейсом класса стек.
Рисунок 2.9 - Реализация стека с использованием наследования (а) и делегирования (б)
2.2.6. Разработка зависимостей
Зависимости - это "клей" объектной модели: именно они позволяют рассматривать модель как нечто целое, а не просто как множество классов.
Односторонние зависимости можно реализовать с помощью ссылок (указателей) (см. рисунок 2.10). При этом, если кратность зависимости равна единице, ей соответствует один указатель, если кратность больше единицы, то множество указателей.
Рисунок 2.10 - Реализация односторонней зависимости
На рисунке 2.11 показан способ реализации двусторонней зависимости с помощью указателей.
Рисунок 2.11 - Реализация двусторонней зависимости
На рисунке 2.12 показан способ реализации зависимости с помощью таблицы (как в реляционных базах данных).
Рис. 2.12. Реализация зависимости с помощью таблицы
При реализации зависимостей с помощью указателей атрибуты зависимостей (связей) переносятся в один из классов, участвующих в зависимости.
3. Реализация объектно-ориентированного проекта
Каждый язык программирования имеет средства для выражения трех сторон спецификации разрабатываемой прикладной системы: структур данных, потоков управления и функциональных преобразований.
Все три модели методологии OMT, разработанные на этапе анализа требований к системе и уточненные на этапе ее проектирования, используются на этапе реализации программного обеспечения системы. Объектная модель определяет классы, атрибуты, иерархию наследования, зависимости. Динамическая модель определяет стратегию управления, которая будет принята в системе (процедурно-управляемая, событийно-управляемая, или многозадачная). Функциональная модель содержит функциональность объектов, которая должна быть воплощена в их методах.
В объектно-ориентированной программе действия выполняются путем создания объектов и вызова методов объектов. Однако какое-то действие должно начать этот процесс. В языке Си++ программа начинает выполняться со специальной функции main. Эта функция может вызвать другие функции и создать какие-либо объекты, которым она может передать сообщения.
При определении поведения программы важную роль играет тот факт, что язык Си++ поддерживает не только объектно-ориентированный, но и процедурный стиль программирования. Действия, в том числе методы классов, - это функции или процедуры.
Данный фрагмент программы реализует два вида счетов - расчетный счет (класс CheckingAccount) и депозитный счет (класс DepositAccount) - с помощью наследования из базового класса Account.
Класс Account - это абстрактный класс с чисто виртуальным методом GetAccountTypc. Кроме того, он задает общие для всех порожденных из него классов методы- Deposit, Withdraw и Balance. Вызывая эти методы, программы, работающие с базовым классом, будут использовать ограниченный полиморфизм.
Атрибут amount - остаток на счету - помещен в защищенную часть класса для того, чтобы порожденные классы могли с ним работать. Атрибут accountNumber - номер счета - помещен во внутреннюю часть класса, поскольку базовый класс полностью контролирует создание уникальных номеров счетов.
//
// Класс Account - базовый класс для всех типов счетов
//
class Account
{
// внешняя часть класса, его интерфейс
public:
// перечисление всех возможных типов счетов
enum AccountType ( CHECKING, DEPOSIT );
Account() ( amount = 0; ); // Создание пустого счета
Account(long _accountNumber); // Создание объекта для
// имеющегося счета
virtual ~Account() ;
// Хотя класс Account абстрактный, он проводит
// стандартные реализации методов "положить на счет"
// (Deposit), "снять со счета" (Withdraw) и
// "остаток на счете" (Balance), хотя они могут быть
// переопределены в порожденных классах
virtual void Deposit(float _amount); //Положить на счет
virtual bool Withdraw(float _ainount); //Снять со счета
virtual float Balance(void) const; //Остаток на счете
// Метод GetAccountType возвращает тип конкретного счета
// и определен только в порожденных классах
virtual AccountType GetAccountType(void) consfc = 0;
// Узнать номер счета
long GetAccountNumber(void) const
{ return accountNumber; } ;
// Защищенная часть класса доступна порожденным классам
protected:
float amount; // Атрибут - сумма на счете
// Внутренняя часть класса
private:
long accountNumber; // Атрибут - номер счета
};
Класс DatabaseObject - абстрактный класс, который задает интерфейс для сохранения и восстановления объектов в базе данных. Порожденные из него классы обязаны определить два метода: Save - для сохранения самого себя в базе данных, и Restore - для восстановления состояния из базы данных. В данном классе показано, что в абстрактном классе могут задаваться конкретные методы (MarkModifled, IsModifled, GetDatabaseConnection). Если первые два метода относятся к интерфейсу класса, то третий - это защищенный метод, необходимый для реализации методов Save и Restore в порожденных классах. (Мы исходим из предположения, что операции с базой данных требуют установления соединения с базой данных через объект типа DBConnection, который и возвращает метод GetDatabaseConnection.)
//
// Базовый класс для всех'объектов,
// которые могут храниться в базе данных
//
class DatabaseObject
{
public:
DatabaseObject () :niodifiedFlag( false) {}
virtual ~DatabaseObject() ;
virtual boot Save(void) = 0;
virtual boot Restore(void) = 0;
// Два дополнительных метода устанавливают или проверяют
// признак того, что объект был модифицирован и должен
// быть сохранен в базе данных
void MarkModified(void) ;
boot IsModified(void) const;
protected:
DBConnection* GetDatabaseConnection(void);
private:
boot modifiedFlag;
};
Два следующих класса: CheckingAccount и DepositAccount - это конкретные классы, реализующие два типа счетов - расчетный счет и депозит. Они используют множественное наследование. В качестве базовых классов выступают классы Account и DatabaseObject. Класс Account задает интерфейс и стандартную реализацию свойств счета, а класс DatabaseObject - необходимый интерфейс для хранения объектов в базе данных.
//
// Класс Расчетный счет - конкретный счет, который
// может храниться в базе данных
//
class CheckingAccount : public Account, DatabaseObject
{
public:
CheckingAccount() {} ;
CheckingAccount(long _accountNumber) •;
Account (_accountNuinber) {} ;
// При конструировании нового объекта данного
// класса мы вначале конструируем его
// базовую часть
virtual ~CheckingAccount ();
// Класс переопределяет метод снятия со счета, потому
// что имеется требование минимального остатка на счете,
// оставляя неизменными методы "положить на счет"
//и "проверить остаток"
virtual boot Withdraw(float _amount);
// Метод GetAccountType определен и возвращает тип
// счета - расчетный счет
virtual AccountType GetAccountType(void) const
( return CHECKING;) ;
// Поскольку объекты данного класса можно хранить
//в базе данных - класс выведен из DatabaseObject,
//в нем определяются методы Save и Restore для
// сохранения и восстановления из базы данных
virtual boot Save(void) ;
virtual boot Restore(void) ;
private:
// Класс определяет один дополнительный атрибут
//минимальный остаток на счете
float minBalance;
};
//
// Класс счет-депозит, аналогичный предыдущему
// расчетному счету
//
class DepositAccount : public Account, DatabaseObject
{
DepositAccount() {}; ,
DepositAccount (long _accountNurnber) :
Account (_accountNuinber) {};
virtual ~DespositAccount() ;
// Класс переопределяет метод снятия со счета и метод
// "положить на счет", потому что имеется ограничение,
//в какие сроки можно их производить,
// метод "проверить остаток" также переопределен,
// поскольку на сумму начисляется процент
virtual void Deposit(float _amount);
virtual boot Withdraw(float amount);
virtual float Balance(void) const;
// Метод GetAccountType определен и возвращает тип
// счета - депозит
virtual AccountType GetAccountType(void) const
( return DEPOSIT;);
// Поскольку объекты данного класса можно хранить
//в базе данных - класс выведен из DatabaseObject,
// в нем определяются методы Save и Restore для
// сохранения и восстановления из базы данных