Прописные истины объектно-ориентированного подхода

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

Контекст класса (текущего объекта). Понятие контекста мы уже вводили применительно к функции (1.6). Контекст (окружение) – это набор имен (объектов языка), которые можно использовать непосредственно, без указания «пути доступа». Контекстом функции являются ее формальные параметры и локальные переменные. Каждый класс определяет свой контекст – это имена элементов данных и методов класса. Этот контекст возникает, поскольку программные компоненты класса имеют дело с текущим объектом, умолчание касается именно его. Любой элемент контекста может адресоваться как просто по имени, так и через указатель this, например n и this->n.

Классы и порождение объектов. Класс – это описание объектов, объект – это инсталляция (отображение) класса в памяти. Можно, конечно, это понимать буквально, как создание экземпляра класса со всеми его «потрохами» - данными и методами. В реальности же проекция класса на традиционную компьютерную архитектуру выглядит таким образом:

для каждого объекта создается экземпляр данных;

методы класса, с которыми работает объект, представляют собой единственный экземпляр программного кода в сегменте команд, который одинаково выполняется для всех объектов (разделяется ими);

при вызове метода объект, для которого он выполняется, идентифицируется указателем текущего объекта this, задающим контекст текущего объекта.

Таким образом, связка «объект-метод» преобразуется в традиционную последовательность действий: «вызов функции – метода класса с фактическим параметром – указателем на текущий объект».

class A { // Эквивалентно:

int a; // struct A { int a; };

public: void F(){ a++; } // void A::F(A *this) { this->a++; }

};

// A DD;

A DD; DD.F(); // A::F(&DD); this=ⅅ

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

· если элементы данных класса имеют взаимосвязанные значения, то класс должен поддерживать установленные для них соглашения;

· если объект данных класса ссылается на внешние структуры данных, то при синтаксическом копировании объекта необходимо обеспечить независимость связанной структуры данных в объекте-копии (создать ее копию или обеспечить разделение – см. «конструктор копирования»;

· если объект содержит идентификаторы каких-либо внешних ресурсов (например, номер коммуникационного порта), то действия класса должны быть аналогичными.


рис. 101-1. Объект: граница ответственности транслятора и программы

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

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

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

class poly{

int n;

double *pd; // Внутренняя СД – дин. массив коэффициентов

public: void add(double D2[], int n2){} // Нарушение закрытости – параметр – внутренняя СД

void add(poly &T){} // Правильно: параметр – объект того же класса

};

Независимость. Внутренние данные объекта должны быть «личной собственностью» объекта. При выполнении операций, связанных с копированием его содержимого (присваивание, передача по значению) класс должен обеспечивать создание независимых копии связанных с объектом данных. Возможен и другой вариант: несколько объектов могут разделять общую структуру данных. Но тогда в классе должен быть прописан механизм такого разделения и синхронизации доступа.

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

По отношению к методам это означает, что интерфейс класса (набор методов) должен быть максимально разнообразен, методы должны сочетаться в любых комбинациях, давая широкое разнообразие возможностей работы с объектом.

Целостность. С одержимое объекта должно быть всегда корректно с точки зрения соглашений, установленных классом для возможных вариантов его внутреннего представления, за этим прежде всего должны следить конструкторы и деструктор, другие методы тоже не должны «оставлять после себя» неправильных объектов.

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

class poly{

int n;

double *pd; // Внутренняя СД – дин. массив коэффициентов

public: poly(){ n=0; pd=NULL; } // Нежелательно: NULL – отсутствие массива

poly(){ n=0; // Правильно: полином с a(0)=0

pd=new double[1];

pd[0]=0;

};

«Ложась спать, программист ставит рядом два стакана: один полный – если захочет пить, и один пустой – если не захочет». Анекдот в тему.

Объект – замкнутые, логически непротиворечивые, всегда корректные данные с четко определенным универсальным интерфейсом доступа к ним

Настало время посмотреть, как перечисленные требования учесть на практике. В качестве примера рассмотрим класс степенного полинома. Он имеет нетривиальное предметное наполнение, а с другой стороны, достаточно прост в реализации. Полином n -ой степени представляет собой выражение вида a0+a1x+a2x2+…+anxn или Σaixi. Для его представления и манипуляций с ним в объекте достаточно хранить массив его коэффициентов. Требование универсальности сразу же предполагает произвольную размерность и динамический массив, требование независимости – каждый объект обладает собственным динамическим массивом (разделение не допускается), требование закрытости – этот массив не доступен извне. Если в операции участвуют более одного полинома, то все они передаются как объекты, а не как динамические массивы. И наконец, в классе есть текущий объект, над которым полагается выполнение методов, он не может быть «свадебным генералом».

//-----------------------------------------------101-01.cpp

// Класс степенного полинома – заголовок класса (объявление)

struct poly{

int n; // степень полинома

double *pd; // динамический массив коэффициентов

double &get(int k); // получение ссылки на коэффициент

void add(poly &T); // сложение объектов (1=1+2)

void mul(poly &T); // умножение объектов объектов (1=1+2)

…};


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: