ооп теория
.pdf
Тема 18. ОТНОШЕНИЯ МЕЖДУ КЛАССАМИ. КЛИЕНТЫ И НАСЛЕДНИКИ.
СОДЕРЖАНИЕ ЛЕКЦИИ:
ОТНОШЕНИЯ МЕЖДУ КЛАССАМИ
o ОТНОШЕНИЯ "ЯВЛЯЕТСЯ" И "ИМЕЕТ" o ОТНОШЕНИЕ ВЛОЖЕННОСТИ
o РАСШИРЕНИЕ ОПРЕДЕЛЕНИЯ КЛИЕНТА КЛАССА
o ОТНОШЕНИЯ МЕЖДУ КЛИЕНТАМИ И ПОСТАВЩИКАМИ o САМ СЕБЕ КЛИЕНТ
o НАСЛЕДОВАНИЕ
o ДОБАВЛЕНИЕ ПОЛЕЙ ПОТОМКОМ
o КОНСТРУКТОРЫ РОДИТЕЛЕЙ И ПОТОМКОВ
oДОБАВЛЕНИЕ МЕТОДОВ И ИЗМЕНЕНИЕ МЕТОДОВ РОДИТЕЛЯ
o СТАТИЧЕСКИЙ КОНТРОЛЬ ТИПОВ И ДИНАМИЧЕСКОЕ
СВЯЗЫВАНИЕ
o ТРИ МЕХАНИЗМА, ОБЕСПЕЧИВАЮЩИЕ ПОЛИМОРФИЗМ
oПРИМЕР РАБОТЫ С ПОЛИМОРФНЫМ СЕМЕЙСТВОМ КЛАССОВ
o АБСТРАКТНЫЕ КЛАССЫ
o КЛАССЫ БЕЗ ПОТОМКОВ
321
ОТНОШЕНИЯ МЕЖДУ КЛАССАМИ
Каждый класс, как не раз отмечалось, играет две роли: он является модулем -
архитектурной единицей, и он имеет содержательный смысл, определяя некоторый тип данных. Но классы программной системы - это ансамбль, в
котором классы, играя свои роли, не являются независимыми - все они находятся в определенных отношениях друг с другом. Два основных типа отношений между классами определены в ОО-системах. Первое отношение
"клиенты и поставщики", называется часто клиентским отношением или отношением вложенности (встраивания). Второе отношение "родители и наследники" называется отношением наследования.
Определение 1. Классы А и В находятся в отношении "клиент-поставщик",
если одним из полей класса В является объект класса А. Класс А называется поставщиком класса В, класс В называется клиентом класса А.
Определение 2. Классы А и В находятся в отношении "родитель -
наследник", если при объявлении класса В класс А указан в качестве родительского класса. Класс А называется родителем класса В, класс В называется наследником класса А.
Оба отношения - наследования и вложенности - являются транзитивными.
Если В - клиент А и С - клиент В, то отсюда следует, что С - клиент А. Если В - наследник А и С - наследник В, то отсюда следует, что С - наследник А.
Определения 1 и 2 задают прямых или непосредственных клиентов и поставщиков, прямых родителей и наследников. Вследствие транзитивности необходимо ввести понятие уровня. Прямые клиенты и поставщики, прямые родители и наследники относятся к соответствующему уровню 1 (клиенты уровня 1, поставщики уровня 1 и так далее). Затем следует рекурсивное определение: прямой клиент клиента уровня k относится к уровню k+1.
322
Для отношения наследования используется терминология, заимствованная из естественного языка. Прямые классы-наследники часто называются сыновними или дочерними классами. Непрямые родители называются предками, а их непрямые наследники - потомками.
Замечу, что цепочки вложенности и наследования могут быть достаточно длинными. На практике вполне могут встречаться цепочки длины 10.
Например, библиотечные классы, составляющие систему Microsoft Office,
полностью построены на отношении вложенности. При программной работе с объектами Word можно начать с объекта, задающего приложение Word, и
добраться до объекта, задающего отдельный символ в некотором слове некоторого предложения одного из открытых документов Word. Для выбора нужного объекта можно задать такую цепочку: приложение Word -
коллекция документов - документ - область документа - коллекция абзацев -
абзац - коллекция предложений - предложение - коллекция слов - слово -
коллекция символов - символ. В этой цепочке каждому понятию соответствует класс библиотеки Microsoft Office, где каждая пара соседствующих классов связана отношением "поставщик-клиент".
Классы библиотеки FCL связаны как отношением вложенности, так и отношением наследования. Длинные цепочки наследования достаточно характерны для классов этой библиотеки.
Отношения "является" и "имеет"
При проектировании классов часто возникает вопрос, какое же отношение между классами нужно построить. Рассмотрим совсем простой пример двух классов - Square и Rectangle, описывающих квадраты и прямоугольники.
Наверное, понятно, что эти классы следует связать скорее отношением наследования, чем вложенности; менее понятным остается вопрос, а какой из
323
этих двух классов следует сделать родительским. Еще один пример двух классов - Car и Person, описывающих автомобиль и персону. Какими отношениями с этими классами должен быть связан класс Person_of_Car,
описывающий владельца машины? Может ли он быть наследником обоих классов? Найти правильные ответы на эти вопросы проектирования классов помогает понимание того, что отношение "клиент-поставщик" задает отношение "имеет" ("has"), а отношение наследования задает отношение
"является" ("is a"). В случае классов Square и Rectangle понятно, что каждый объект квадрат "является" прямоугольником, поэтому между этими классами имеет место отношение наследования, и родительским классом является класс Rectangle, а класс Square является его потомком.
В случае автомобилей, персон и владельцев авто также понятно, что владелец "имеет" автомобиль и "является" персоной. Поэтому класс
Person_of_Car является клиентом класса Car и наследником класса Person.
Отношение вложенности
Рассмотрим два класса A и B, связанных отношением вложенности. Оба класса применяются для демонстрации идей и потому устроены просто, не неся особой смысловой нагрузки. Пусть класс-поставщик A уже построен. У
класса два поля, конструктор, один статический и один динамический метод.
Вот его текст:
public class ClassA
{
public ClassA(string f1, int f2)
{
fieldA1 = f1; fieldA2 = f2;
}
public string fieldA1; public int fieldA2; public void MethodA()
{
Console.WriteLine( "Это класс A"); Console.WriteLine ("поле1 = {0}, поле2 = {1}",
fieldA1, fieldA2);
}
324
public static void StatMethodA()
{
string s1 = "Статический метод класса А"; string s2 = "Помните: 2*2 = 4"; Console.WriteLine(s1 + " ***** " + s2);
}
}
Построим теперь класс B - клиента класса A. Класс будет устроен похожим образом, но в дополнение будет иметь одним из своих полей объект inner
класса A:
public class ClassB
{
public ClassB(string f1A, int f2A, string f1B, int f2B)
{
inner = new ClassA(f1A, f2A); fieldB1 = f1B; fieldB2 = f2B;
}
ClassA inner;
public string fieldB1; public int fieldB2; public void MethodB1()
{
inner.MethodA(); Console.WriteLine( "Это класс B");
Console.WriteLine ("поле1 = {0}, поле2 = {1}", fieldB1, fieldB2);
}
}
Обратите внимание: конструктор клиента (класса B) отвечает за инициализацию полей класса, поэтому он должен создать объект поставщика
(класса A), вызывая, как правило, конструктор поставщика. Если для создания объектов поставщика требуются аргументы, то они должны передаваться конструктору клиента, как это сделано в нашем примере.
После того как конструктор создал поле - объект поставщика - методы класса могут использовать этот объект, вызывая доступные клиенту методы и поля класса поставщика. Метод класса B - MethodB1 начинает свою работу с вызова: inner.MethodA, используя сервис, поставляемый методом класса A.
325
Расширение определения клиента класса
До сих пор мы говорили, что клиент содержит поле, представляющее объект класса поставщика. Это частая, но не единственная ситуация, когда класс является клиентом другого класса. Возможна ситуация, когда метод клиентского класса локально создает объект поставщика, вызывает его методы в собственных целях, но по завершении метода локальный объект заканчивает свою жизнь. Еще одна возможная ситуация - когда объекты поставщика вообще не создаются ни конструктором, ни методами класса клиента, но клиент вызывает статические методы класса поставщика. Оба эти варианта демонстрируют следующие два метода класса B:
public void MethodB2()
{
ClassA loc = new ClassA("локальный объект А",77); loc.MethodA();
}
public void MethodB3()
{
ClassA.StatMethodA();
}
Дадим теперь расширенное определение клиента.
Определение 3. Класс B называется клиентом класса A, если в классе B
создаются объекты класса A - поля или локальные переменные - или вызываются статические поля или методы класса A.
Отношения между клиентами и поставщиками
Что могут делать клиенты и что могут делать поставщики? Класс-поставщик создает свойства (поля) и сервисы (методы), предоставляемые своим клиентам. Клиенты создают объекты поставщика. Вызывая доступные им методы и поля объектов, они управляют работой созданных объектов поставщика. Клиенты не могут ни повлиять на поведение методов
326
поставщика, ни изменить состав предоставляемых им полей и методов, они не могут вызывать закрытые поставщиком поля и методы класса.
Класс-поставщик интересен клиентам своей открытой частью, составляющей интерфейс класса. Но большая часть класса может быть закрыта для клиентов - им незачем вникать в детали представления и в детали реализации. Сокрытие информации вовсе не означает, что разработчики класса не должны быть знакомы с тем, как все реализовано, хотя иногда и такая цель преследуется. В общем случае сокрытие означает, что классы-
клиенты строят свою реализацию, основываясь только на интерфейсной части класса-поставщика. Поставщик закрывает поля и часть методов класса от клиентов, задавая для них атрибут доступа private или protected. Он может некоторые классы считать привилегированными, предоставляя им методы и поля, недоступные другим классам. В этом случае поля и методы,
предназначенные для таких vip-персон, снабжаются атрибутом доступа internal, а классы с привилегиями должны принадлежать одной сборке.
В заключение построим тест, проверяющий работу с объектами классов A и B:
public void TestClientSupplier()
{
ClassB objB = new ClassB("AA",22, "BB",33); objB.MethodB1();
objB.MethodB2();
objB.MethodB3();
}
Результаты работы этого теста показаны на рис. 18.1.
327
Рис. 18.1. Клиенты и поставщики
Сам себе клиент
Зададимся вопросом, может ли класс быть сам себе клиентом, другими словами, может ли поле класса быть объектом описываемого класса? Другой,
не менее интересный вопрос - могут ли два класса быть одновременно клиентами и поставщиками друг для друга? Ответы на оба вопросы положительны, и подобные ситуации типичны и не являются какой-либо экзотикой.
Первая ситуация характерна для динамических структур данных. Элемент односвязного списка имеет поле, представляющее элемент односвязного списка; элемент двусвязного списка имеет два таких поля; узел двоичного дерева имеет два поля, представляющих узлы двоичного дерева. Эта ситуация характерна не только для рекурсивно определяемых структур данных. Вот еще один типичный пример. В классе Person могут быть заданы два поля - Father и Mother, задающие родителей персоны, и массив Children.
Понятно, что все эти объекты могут быть того же класса Person.
Не менее часто встречается ситуация, когда классы имеют поля, взаимно ссылающиеся друг на друга. Типичным примером могут служить классы Man
328
и Woman, первый из которых имеет поле wife класса Woman, а второй - поле husband класса Man.
Заметьте, классы устроены довольно просто - их тексты понятны, отношения между классами очевидны. А вот динамический мир объектов этих классов может быть довольно сложным, отношения между объектами могут быть запутанными; для объектов характерны не только любовные треугольники,
но и куда более сложные фигуры.
НАСЛЕДОВАНИЕ
Мощь ООП основана на наследовании. Когда построен полезный класс, то он может многократно использоваться. Повторное использование - это одна из главных целей ООП. Но и для хороших классов неизбежно наступает момент, когда необходимо расширить возможности класса, придать ему новую функциональность, изменить интерфейс. Всякая попытка изменять сам работающий класс чревата большими неприятностями - могут перестать работать прекрасно работавшие программы, многим клиентам класса вовсе не нужен новый интерфейс и новые возможности. Здесь-то и приходит на выручку наследование. Существующий класс не меняется, но создается его потомок, продолжающий дело отца, только уже на новом уровне.
Класс-потомок наследует все возможности родительского класса - все поля и все методы, открытую и закрытую часть класса. Правда, не ко всем полям и методам класса возможен прямой доступ потомка. Поля и методы родительского класса, снабженные атрибутом private, хотя и наследуются, но по-прежнему остаются закрытыми, и методы, создаваемые потомком, не могут к ним обращаться напрямую, а только через методы, наследованные от родителя. Единственное, что не наследует потомок - это конструкторы
329
родительского класса. Конструкторы потомок должен создавать сам. В этом есть некоторая разумная идея, и я позже поясню ее суть.
Рассмотрим класс, названный Found, играющий роль родительского класса.
У него есть обычные поля, конструкторы и методы, один из которых снабжен новым модификатором virtual, ранее не появлявшимся в классах, другой -
модификатором override:
public class Found
{
//поля
protected string name; protected int credit; public Found()
{
}
public Found(string name, int sum)
{
this.name = name; credit = sum;
}
public virtual void VirtMethod()
{
Console.WriteLine ("Отец: " + this.ToString() );
}
public override string ToString()
{
return(String.Format("поля: name = {0}, credit = {1}", name, credit));
}
public void NonVirtMethod()
{
Console.WriteLine ("Мать: " + this.ToString() );
}
public void Analysis()
{
Console.WriteLine ("Простой анализ");
}
public void Work()
{
VirtMethod();
NonVirtMethod();
Analysis();
}
}
Заметьте, класс Found, как и все классы, по умолчанию является наследником класса Object, его потомки наследуют методы этого класса уже не напрямую,
а через методы родителя, который мог переопределить методы класса Object.
В частности, класс Found переопределил метод ToString, задав собственную
330
