Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf550 Часть III • Программирование с агрегированием и наследованием
class SavingsAccount: public Account { double rate;
public:
SavingsAccount(double initBalance)
{ balance = initBalance; rate = 6.0; }
void paylnterestO
{ balance += balance * rate / 365 / 100; }
int mainO
{
Account |
а(1000); |
|
// объект базового |
класса |
||
CheckingAccount |
a1(1000); |
|
// объект производного |
класса |
||
SavingsAccount |
a2(1000); |
|
// объект производного |
класса |
||
al.withrawdOO); |
|
// метод производного |
класса |
|||
a2.deposit(100); |
|
// метод базового |
класса |
|||
a1.deposit(200); |
|
// метод базового |
класса |
|||
a2.withdraw(200); |
|
// метод базового |
класса |
|||
a2. paylnterestO; |
|
// метод производного |
класса |
|||
a.deposit(300); |
|
|
// метод базового |
класса |
||
a.withdraw(IOO); |
|
// метод базового |
класса |
|||
// a.paylnterestO; |
|
// синтаксическая |
ошибка |
|||
// a1.paylnterestO; |
|
// синтаксическая |
ошибка |
|||
cout « |
"Итоговые 6алансы\п объект Account: |
|
|
|
||
« |
a.getBalO « endl; |
« al.getBalO « endl; |
|
|
||
cout « |
" объект расчетного счета |
|
|
|||
cout « |
" объект сберегательного сч ета: " « |
a2.getBal() « endl; |
|
|||
return 0;
}
Различные режимы создания производного класса из базового класса
Для обозначения наследования можно использовать те же три ключевых слова, что и для предоставления прав на элементы данных класса: public, protected и private. Именно они (с предшествующим двоеточием) указывают, что между классами существует связь наследования.
Поскольку ключевые слова те же, многие программисты считают, что и смысл их при наследовании такой же, как при управлении доступом к компонентам класса. Например, в следующем фрагменте ключевое слово public используется два>вды.
class CheckingAccount |
: |
public Account { |
/ / Account - базовый класс |
|
double |
fee; |
/ / |
в производный класс добавлен элемент данных |
|
public: |
|
/ / |
начало общедоступного сегмента данных |
|
. . . } |
; |
/ / |
остальная часть |
производного класса CheckingAccount |
Не следует считать, что в обоих случаях употребление public имеет один и тот же смысл. Общее только само ключевое слово public и двоеточие. В случае управ ления доступом двоеточие находится справа от ключевого слова. Это значит, что к последующим элементам данных можно обращаться из любого места програм мы. В режиме наследования двоеточие находится слева от ключевого слова. Смысл ключевого слова заключается в том, что права доступа к наследуемым компонентам будут те же, что и в базовом классе, т. е. закрытый компонент в базовом классе остается закрытым в производном и т. д
Глава 13 • Подобные классы и их интерпретация |
551 |
Права доступа к элементам данных класса и режим создания производного класса задаются одним ключевым словом. Однако имейте виду, что все остальное разное.
Двоеточие и ключевое слово для режима наследования синтаксически связы вают базовый и производный классы. Независимо от того, где в исходном коде размещаются определения класса, у программиста, проверяющего его, есть неоднозначный визуальный признак. Он означает:
•Наличие другого класса, используемого как базовый для данного класса
•Имя базового класса
Если использовать диаграмму UML (Unified Modeling Language), то связи между классами обозначаются связями между значками классов с пустыми треугольными стрелками, указывающими вершинами на базовый класс. Если у базового класса более одного производного класса, то каждый производный класс может иметь индивидуальную связь с базовым классом или общую связь (с одной стрелкой). Альтернативные способы описания связей между классом Account и двумя производными классами показаны на рис. 13.4.
А) |
Account |
В) |
Account |
Ж |
7V |
|
^ |
CheckingAccount |
SavingsAccount |
CheckingAccount |
SavingsAccount |
Рис. 1 3 . 4 . Связи |
между классами в иерархии |
Account |
|
Это пример использования наследования как способа представления связанных понятий приложения. Расчетный счет "является видом" счета (Account). Каждый расчетный счет — это счет, но не каждый счет — расчетный. Таково обобщенное замечание относительно связей наследования. Каждый автомобиль — транспорт ное средство, но не ка>вдое транспортное средство — автомобиль. Прямоугольник есть вид многоугольника, но не каждый многоугольник — прямоугольник.
Отношение "является видом" ("is а") концептуально связывает классы и допу скает применение наследования. Это отличается от агрегирования, когда объекты просто связываются отношением принадлежности. Например, прямоугольник имеет точки, а объект History содержит объекты Sample. Было бы некорректно говорить, что объект History является объектом Sample. У этих двух объектов со вершенно разные данные и поведение. При наследовании данные и поведение клас сов также различны, но имеют общее подмножество, которое определено в базовом классе. Класс Account содержит элемент данных balance и метод depositO. Благодаря наследованию класс CheckingAccount также имеет элемент данных balance и метод deposit (), хотя в определении класса эти компоненты не пере числяются.
Наследование представляет собой связь между классами. Класс Account опре деляет элемент данных balance, а класс CheckingAccount этого не делает. Так как класс CheckingAccount наследует свойства от класса Account, объекты CheckingAccount являются объектами Account и имеют все свойства Account, а также все свойства, указанные в определении класса CheckingAccount.
Таким образом, наследование не экономит память. Все данные Account при сутствуют в каждом объекте CheckingAccount. Наследование помогает создавать компактные классы, если они становятся чрезмерно большими, и показывает логическую взаимосвязь между ними. Например, в листинге 13.4 показано, что классы CheckingAccount и SavingsAccount связаны. Оба они являются наследника ми класса Account. В листинге 13.3 такую логическую взаимосвязь показать было
552 |
Часть III * Программирование с агрегированием и насдедовониег^! |
|||||
|
невозможно. В нем определения классов размеш,ались вместе, но было показано |
|||||
|
наличие обш,их элементов данных и функций-членов. Программисту, читающему |
|||||
|
исходный код, приходилось додумываться до этого самому. |
|||||
|
Каждый класс C + + можно использовать как базовый для создания производ |
|||||
|
ных классов. Иерархия наследования транзитивна. Например, из класса Checking- |
|||||
|
Account можно получить класс TradingAccount. Объект TradingAccount будет иметь |
|||||
|
все свойства объекта CheckingAccount. Так как все объекты CheckingAccount |
|||||
|
имеют обш,ие свойства объекта Account, объект TradingAccount включает также |
|||||
|
все свойства объекта Account. |
|
|
|
|
|
|
С этой точки зрения термины."суперкласс" и "подкласс", часто применяемые |
|||||
|
для обозначения базового и производного класса, не очень точны. Они показы |
|||||
|
вают, что базовый класс (суперкласс) в чем-то превосходит производный класс |
|||||
|
(подкласс), а это не так. |
|
|
|
|
|
|
Возможности базовых классов не теряются ниже по иерархии, в производных |
|||||
|
классах. Объекты CheckingAccount могут делать все то же, что и объекты Account. |
|||||
|
Ниже по иерархии увеличиваются лишь ограничения членства. Класс Checking- |
|||||
|
Account более ограничен, чем Account. В мире меньше расчетных счетов, чем |
|||||
|
счетов вообш.е. Аналогично, в мире меньше объектов TradingAccount, поскольку |
|||||
|
каждый объект TradingAccount является объектом CheckingAccount. |
|||||
|
Рассматривая иерархию классов, можно видеть, что в каждом подклассе число |
|||||
|
экземпляров объектов уменьшается, но объектам данного подкласса становится |
|||||
|
доступно больше средств. С математической точки зрения число экземпляров во |
|||||
|
множестве может иметь важное значение, а с точки зрения программирования |
|||||
|
в расчет принимается предлагаемый объектом сервис. Суперклассы предлагают |
|||||
|
меньше сервисов, чем подклассы. Вот почему эти термины неточно отражают суть |
|||||
|
проблемы. Предпочтение отдается терминам "базовый класс" и "производный |
|||||
|
класс". |
|
|
|
|
|
|
Наследование повышает модульность программного кода и способствует по |
|||||
|
вторному использованию компонентов. Группу хорошо сконструированных клас |
|||||
|
сов обш.его назначения можно организовать в библиотеку. Интерфейс таких |
|||||
|
библиотечных классов следует опубликовать, а реализацию — инкапсулировать. |
|||||
|
Библиотечные классы могут специализироваться путем создания новых производ |
|||||
|
ных классов. В этих классах к элементам данных и функциям базового класса |
|||||
|
добавляются новые данные. Подобный метод широко используется для создания |
|||||
|
графических пользовательских интерфейсов. Классы приложения наследуют |
|||||
|
свойства |
из библиотечных классов — окон, диалоговых блоков, графических |
||||
|
командных кнопок. Программисты приложения применяют эти свойства, реали |
|||||
|
зованные в библиотечных классах, добавляют специфические свойства, которые |
|||||
|
определяют, как именно должна вести себя в приложении конкретная кнопка, |
|||||
|
диалоговый блок или окно. |
|
|
|
|
|
|
В процессе такой специализации вносить изменения в базовые библиотечные |
|||||
|
классы не требуется. Следовательно, |
нет |
необходимости в их редактировании |
|||
|
и перекомпиляции. |
|
|
|
|
|
|
Как показывает листинг 13.4, каждый производный класс должен явно указы |
|||||
|
вать свой базовый класс. Кроме того, в нем задаются дополнительные данные |
|||||
|
и функции. |
|
|
|
|
|
|
class SavingsAccount : public |
Account |
{ |
/ / синтаксис производного класса |
||
|
double |
rate; |
/ / |
дополнительное |
средство |
|
|
public: |
|
|
|
|
|
|
. . . } |
; |
/ / |
остальная часть |
класса SavingsAccount |
|
Между тем клиент ничего не должен знать о наследовании. Если клиент реали зуется в отдельном файле, то в нем должен быть известен только производный класс, но не базовый. Базовый класс должен быть известен в тех файлах, где содержится его спецификация и где он реализуется. Это также подтверждает, что
I 554 I |
Чость I!! '^ Программтроваише с агрегированием и наследованием |
производного класса. Метод paylnterestO в определении базового класса Account отсутствует, и вызов функции дает синтаксическую ошибку.
Другая ситуация возникает, когда получателем сообщения является объект производного класса. Нужно различать три случая:
1.Метод унаследован из базового класса и не переопределяется в производном классе.
2 . Метод отсутствует в базовом классе и добавлен в производном классе.
3 . Метод имеется в базовом классе и переопределен в производном классе.
Когда клиент вызывает унаследованный метод, у компилятора возникает проб лема. Подобно обработке других сообщений, он находит тип получателя сообще ния (вспоминает, что оно отправлено объекту производного класса) и ищет в спецификации производного класса имя функции-члена.
a1.deposit(200); |
/ / базовый метод класса |
Очевидно, функции-члена там нет, поскольку унаследованные методы (в данном случае depositO) описываются только в базовом, а не в производном классе. Описать унаследованный метод синтаксически приемлемо и в производном клас се, но в таком случае это был бы уже не унаследованный, а переопределенный метод.
Когда метод не удается найти в классе целевого объекта, компилятору следует предупредить программиста об отсутствии вызванного метода. Однако перед этим компилятор проверяет, следует ли за спецификацией имени класса двоеточие. Если метод найден в базовом классе, он корректно приходит к заключению, что это производный класс, находит имя базового класса и ищет в нем определение.
Впротивном случае компилятор проверяет, имеет ли этот класс базовый класс
иповторяет процедуру, пока не произойдет одно из двух событий: в цепочке насле дования будет найден класс без базового класса или в спецификации очередного класса обнаружится искомая функция. В последнем случае компилятор проверяет число и типы аргументов, сравнивает их с сигнатурой функции и генерирует для вызова функции объектный код.
Обратите внимание, что применение наследования нарушает первый принцип объектно-ориентированного программирования: связывание данных и операций
вопределении класса в границах его области действия. Применение наследования как техники программирования имеет в объектно-ориентированной разработке ПО очень важное значение. Следовательно, программисты не должны ограничивать его практическое использование из-за каких-то абстрактных принципов. Чтобы соответствовать и принципам (одному — концептуальному, другому — техниче скому), и потребностям программиста, в С+Н- делаются две оговорки.
На концептуальном уровне в C + + утверждается, что объект производного класса является объектом базового класса, следовательно, он имеет все данные и методы, определенные в базовом классе. Согласно правилам области действия (в знакомом нам виде — для файла, функции, блока и класса) методы базового класса доступны производному классу.
Однако не следует беспокоиться об этих концептуальных и технических проб лемах. Имейте в виду, что когда компилятор не находит метод в спецификации производного класса, он видит его в спецификации базового касса. Позднее вы по знакомитесь с правилами области действия и разрешения имен при наследовании.
Во втором случае, когда метод отсутствует в базовом классе, но имеется в производном классе, применяются стандартные правила интерпретации вызова функции. Компилятор находит метод в спецификации производного класса и на
Глава 13 • Подобные классы и их интерпретация |
555 |
том успокаивается. Если аргументы не соответствуют сигнатуре функции, то по является синтаксическая ошибка. Если же аргументы совпадают, генерируется соответствующий вызов.
a2.paylnterest(); |
/ / метод производного класса |
Аналогичные правила применяются в третьем случае, когда метод переопре деляется в производном классе. Компилятор игнорирует связь наследования. Как уже было показано выше, когда целью сообидения является объект базового клас са, компилятор игнорирует соответствуюш,ий метод базового класса и методы про изводных классов. Если цель сообш^ения — получить объект производного класса, компилятор иш.ет спецификацию производного класса и останавливается, когда находит метод. Метод будет найден, так как он переопределяется в производном классе.
al.withdrawO; |
/ / метод производного класса |
Когда число фактических аргументов и их типы соответствуют сигнатуре функ ции, компилятор генерирует вызов функции. Если совпадения нет, выводится син таксическая ошибка. Компилятор не обраш.ается к базовому классу в поиске лучшего совпадения. Как будет показано ниже, это может быть источником проблем.
Доступ к сервисам базового и производного классов
Обычно производный класс "является" по сути и базовым классом, т. е. каж дый объект производного класса имеет все элементы данных и функции базового класса, а также добавленные и переопределенные данные и методы.
Производный класс — клиент базового класса. Это напоминает любой кли ентский код C + + с серверными классами. Клиент использует сервис сервера — его элементы данных и функции. Серверный класс не знает о своих клиентских классах, не знает имен клиентов. Это естественно, поскольку функция серверного класса может быть библиотечной, написанной за годы до создания клиента. Кли ентский класс должен знать имена своих серверных классов и открытых сервисов, которые он может использовать.
Например, клиент из листинга 13.4 определяет объект класса Account, обозна чая имя класса. Клиентский код получает доступ к сервисам Account по их именам.
Account |
а(1000); |
/ / |
объект базового |
класса |
a.cleposit(300); |
/ / |
метод базового |
класса |
|
cout « |
" Итоговые балансы\п объекта |
Account: |
|
|
« |
a.getBalO « endl; |
|
|
|
В данном примере класс Account не имеет представления о том, что его исполь зует клиент. Как уже говорилось, класс Account разрабатывался за несколько лет до создания клиентов совсем другими программистами.
Аналогично, производный класс использует сервисы базового класса (данные и функции). Базовый класс не знает о производных классах, так как при програм мировании клиенты в сервере никогда не идентифицируются. Производный класс должен знать имя своего базового класса и имена не являющихся закрытыми сервисов, доступных для использования.
Например, производный класс в листинге 13.4 устанавливает связь наследо вания с базовым классом Account, указывая имя базового класса после двоеточия.
class SavingsAccount : public Account { |
/ / |
синтаксис |
производного класса |
|
double |
rate; |
|
|
|
public: |
|
|
|
|
. . . } |
; |
/ / |
остальная |
часть SavingsAccount |
556I Чость III • Программирование с агрвтроваитвт и наследованием
Ме ж ду связями клиент/сервер при композиции классов (агрегации) и связями наследования (производный/базовый) есть разница. Прикомпозиции для получе ния доступа к сервисам клиент должен создавать экземпляр серверного объекта. При наследовании производному классу не нужно задавать экземпляр отдельного
базового объекта. В определении производного класса достаточно использовать
имя базового класса.
При композиции класса объект-контейнер не предоставляет своим клиентам сервис собственных компонентов. О н предоставляет только свой собственный сервис, явно определяемый в его интерфейсе. Например, класс Point,использо вавшийся как компонент класса Rectangle, имеет обн^едоступные методы set(), get() и move().
class Point { |
// закрытые координаты |
X, у; |
|
public- |
// обобщенный конструктор |
Point (int а, intb;) |
|
{ X = а; у = b; } |
// функция-модификатор |
void set (int a, intb) |
|
{ X = a; у = b; } |
// функция-модификатор |
void move (int a, intb) |
|
{ X += a; у += b; } |
// функция-селектор |
void get (int& a, int& b) const - |
|
{ a = x; b = у; } } ; |
|
Это не означает,что класс Rectangle,содержандий элемент данных Point, может
предоставить своим клиентам те же сервисы. Пример клиента:
Point р1(20,40), р2(70,90); |
// верхний левый, нижний правый углы |
Rectangle гес(р1,р2,4); |
// составной объект: клиент Point |
гее.set(30,40); |
// это не имеет смысла |
rec.move(10,20); |
// это нормально: в чемразница? |
Разница между методами set() и move() здесь в том, чтокласс Rectangle не беспокоится о реализации функции-члена set(), ноопределяет, что означает метод moveO в контексте класса Rectangle.
class Rectangle { |
// верхний левый, нижний правый углы |
Point pt1, pt2; |
|
int thickness; |
// толщина границы прямоугольника |
public: |
|
Rectangle (const Point& pi, const Point& p2, int width=1); |
|
void move(int a, int b); |
// перемещение обоих точек |
void setThickness(int width = 1 ) ; |
// изменить толщину линии |
bool pointIn(const Point& pt) const; |
// точка в прямоугольнике? |
. . . . } ; |
// остальная часть Rectangle |
Между тем производный класс предлагает своим клиентам сервис базового класса. Разработчику производного класса для этого ничего не нужно делать. Рассмотрим, например, класс SavingsAccount из листинга 13.4.
class SavingsAccount : public Account |
{ |
// еще один производный класс |
double rate; |
|
// дополнительные компоненты |
public: |
|
|
SavingsAccount(double initBalance) |
} |
// для сберегательных счетов |
{ balance = initBalance; rate = 6.0; |
||
void paylnterestO |
|
// для сберегательных счетов |
{ balance += balance * rate / 365 / 100; } } |
; |
|
Глава 13 « Подобные классы и их интерпретация |
•ш |
i |
1 5 5 7 1 |
||
Клиент данного класса может определять объекты типа SavingsAccount и пе редавать им сообщения paylnterest(). Если же обратиться к клиенту из листин га 13.4, можно увидеть гораздо больше, чем просто передачу этого сообщения.
SavingsAccount а2(1000); |
/ / |
объект производного класса |
|
a2.deposit(100); |
/ / |
метод базового |
класса |
a2.withdraw(200); |
/ / |
метод базового |
класса |
а2. paylnterestO; |
/ / |
метод производного класса |
|
cout « " объект SavingsAccount: " « |
a2.getBal() |
« endl; |
|
Сервисы depositO, withdrawO и getBalO, используемые в клиенте, не пере числяются в производном классе SavingsAccount. Они перечисляются только
вбазовом классе Account. Для компилятора это не проблема. Он легко следует по цепочке наследования в определении класса и находит данные функции-члены
вбазовом классе. Что же делать программисту, работающему над клиентом?
Откуда ему знать, что эти сервисы доступны для объектов, определяемых в клиенте? Ему нужно сделать то же, что и компилятору: пройтись по цепочке наследования в определениях классов.
Программисту, использующему сервисы SavingsAccount, следует найти сред ства Account и понять, что они доступны для объектов SavingsAccount. В листин ге 13.4 эти определения классов расположены вместе. В крупных системах со сложной иерархией наследования (когда производный класс используется как ба зовый для другого класса и т. д.) это не всегда возможно. Поиск списка средств, предоставляемых производным классом, становится для программиста трудной задачей. Описания производного класса уже недостаточно — приходится искать их в другом месте.
Тем самым усложняется программа, возникают ошибки, которые трудно обна ружить и нелегко исправить. Снова встает вопрос о соответствии наследования принципам объектно-ориентированного программирования. Наследование удобно для программиста, разрабатывающего классы в его иерархии. Это метод для по вторного использования разработанных фрагментов ПО и уменьшения объема исходного кода.
Что касается разработчика клиента, то два разных класса — SavingsAccount и CheckingAccount — являются неплохим техническим решением. Они связывают родственные данные и функции. Попытка передать сообщение неверному классу помечается компилятором как ошибка. Что добавляет к этому наследование? Данные и методы, общие для обоих классов, нужно реализовывать только один раз, а изменения в базовом классе распространяются на все производные классы автоматически. Такой подход очень удобен при реализации серверных классов.
С другой стороны, наследование затрудняет изучение свойств сервера. Некото рые библиотеки языка C++ снабжают свои классы большим числом сервисов (более 100). Эти сервисы распространяются на пять или более уровней наследо вания. Чтобы понять работу библиотечного класса, нужно исследовать все уровни наследования. А это непростая задача, поскольку сама иерархия и доступные сер висы меняются от одной версии библиотеки к другой. Таким образом, вам надо совершенствовать свои знания, чтобы быть в курсе изменений. Программирова ние на С+-\ нескучное занятие, особенно когда без всякой меры используется наследование.
Вотличие от унаследованных, переопределенные средства непосредственно доступны в списке сервисов производного класса. Их не нужно нигде искать. Обычно они делают то же, что и сервисы, определенные в базовом классе, но бо лее эффективно или с применением несколько других алгоритмов или данных.
Впримере наследования, приведенном в листинге 13.4, в производном классе CheckingAccount переопределялась функция-член withdrawO из базового класса Account.
558 Часть III» Программирование с агрегированием и наследованием
Переопределенная функция использует данные (элемент данных fee), доступ ные только в производном, но не в базовом классе. Обычно это происходит, потому что в других производных классах (в нашем примере SavingsAccount) такие данные не используются. Если же они там необходимы (в примере все производ ные классы применяют базовый элемент данных balance), то элемент данных следует включить в базовый класс (как в программе из листинга 13.4).
Применение дополнительных данных в функциях-членах, переопределенных в производном классе,— популярный и распространенный, но не обязательный прием.
Объекты производных классов можно рассматривать как сумму частей произ водного класса (его компонентов private, protected и public) и частей базового класса (компонентов private, protected и public этого класса). Память, рас пределяемая для объекта производного класса, также представляет собой сумму областей памяти для частей базового и производного классов.
Например, на нашей машине размер объектов класса Account равен 8 байт, а размеры объектов CheckingAccount и SavingsAccount составляют 16 байт каж дый. Если типы данных, используемых как элементы данных, нужно выравнивать в памяти, может потребоваться дополнительное пространство.
Клиент производного объекта вызывает обш.едоступные сервисы базового класса, используя производный объект, как будто данные сервисы находятся в его части public. Например, объект CheckingAccount отвечает на сообш,ения deposit О и getBalO, как если бы они были определены в классе CheckingAccount. Клиент не знает о различиях, и ему не нужно о них знать.
Компоненты базового класса не имеют доступа к средствам, добавленным или переопределенным в производных классах. Например, у класса Account нет доступа к закрытому элементу данных rate и обш,едоступной функции-члену paylnterestO, определяемым в классе SavingsAccount. Следуюш^ая запись бес смысленна.
Account а(1000); а. paylnterestO; / / синтаксическая ошибка
Такие синтаксические правила расширяют понятие, согласно которому объект производного класса является объектом базового класса, плюс что-то еш,е.
Что касается объектов базового класса, то они не могут ничего знать о сервисе другого класса, даже если это производный от них класс. Все равно класс другой. Объект базового класса не может отвечать на сообщения, не описанные в специ фикации.
Аналогично, определение функции или класса как "дружественных" классу Account не предоставляет этой функции прямой доступ к отличным от public компонентам производных классов CheckingAccount и SavingsAccount.
Доступ к базовым компонентам объекта производного класса
Компоненты и "друзья" производного класса получают доступ ко всем элемен там данных и его функциям-членам. Кроме того, они имеют некоторый доступ к элементам данных и функциям-членам базового класса. Они могут обранлаться только к компонентам public и protected, но не к закрытым данным и функциямчленам базового класса. Они также не получают доступ к компонентам других классов, производных от того же базового класса.
Базовый класс имеет три вида клиентов (три области доступа). Во внутренней области находятся самые большие права доступа к элементам данных и функциям. Их получают функции-члены класса и его "друзья". Они имеют доступ к эле ментам данных и функциям private, protected и public. Эти права даются им по определению, поскольку они объявлены в границах фигурных скобок класса
