Страница 311 из 371
Определения 1 и 2 задают прямых или непосредственных клиентов и поставщиков, прямых родителей и наследников. Вследствие транзитивности необходимо ввести понятие уровня. Прямые клиенты и поставщики, прямые родители и наследники относятся к соответствующему уровню 1 (клиенты уровня 1, поставщики уровня 1 и так далее). Затем следует рекурсивное определение: прямой клиент клиента уровня к относится к уровню k+1.
Для отношения наследования используется терминология, заимствованная из естественного языка. Прямые классы-наследники часто называются сыновними или дочерними классами. Непрямые родители называются предками, а их непрямые наследники — потомками.
Замечу, что цепочки вложенности и наследования могут быть достаточно длинными. На практике вполне могут встречаться цепочки длины 10. Например, библиотечные классы, составляющие систему Microsoft Office, полностью построены на отношении вложенности. При программной работе с объектами Word можно начать с объекта, задающего приложение Word, и добраться до объекта, задающего отдельный символ в некотором слове некоторого предложения одного из открытых документов Word. Для выбора нужного объекта можно задать такую цепочку: приложение Word — коллекция документов — документ — область документа — коллекция абзацев — абзац — коллекция предложений — предложение — коллекция слов — слово — коллекция символов — символ. В этой цепочке каждому понятию соответствует класс библиотеки Microsoft Office, где каждая пара соседствующих классов связана отношением "поставщик-клиент".
Классы библиотеки FCL связаны как отношением вложенности, так и отношением наследования.
Длинные цепочки наследования достаточно характерны для классов этой библиотеки.
Отношения "является" и "имеет"
При проектировании классов часто возникает вопрос, какое же отношение между классами нужно построить. Рассмотрим совсем простой пример двух классов — Square и Rectangle, описывающих квадраты и прямоугольники. Наверное, понятно, что эти классы следует связать скорее отношением наследования, чем вложенности; менее понятным остается вопрос, а какой из этих двух классов следует сделать родительским. Еще один пример двух классов — Саr и Person, описывающих автомобиль и персону. Какими отношениями с этими классами должен быть связан класс Person_of_Саr, описывающий владельца машины? Может ли он быть наследником обоих классов? Найти правильные ответы на эти вопросы проектирования классов помогает понимание того, что отношение "клиент-поставщик" задает отношение "имеет" ("has"), а отношение наследования задает отношение "является" ("is а"). В случае классов Square и Rectangle понятно, что каждый объект квадрат "является" прямоугольником, поэтому между этими классами имеет место отношение наследования, и родительским классом является класс Rectangle, а класс Square является его потомком.
В случае автомобилей, персон и владельцев авто также понятно, что владелец "имеет" автомобиль и "является" персоной. Поэтому класс Person of Car является клиентом класса Саr и наследником класса Person.
Отношение вложенности
Рассмотрим два класса A и B, связанных отношением вложенности. Оба класса применяются для демонстрации идей и потому устроены просто, не неся особой смысловой нагрузки. Пусть класс-поставщик а уже построен. У класса два поля, конструктор, один статический и один динамический метод. Вот его текст:
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);
}
public static void StatMethodA()
{
string s1 = "Статический метод класса A";
string s2 = "Помните: 2*2 = 4";
Console.WriteLine(s1 + " ***** " + s2);
}
}
Построим теперь класс B — клиента класса A. Класс будет устроен похожим образом, но в дополнение будет иметь одним из своих полей объект i
public class ClassB
{
public ClassB(string f1A, int f2A, string f1B, int f2B)
{
i
fieldB1 = f1B; fieldB2 = f2B;
}
ClassA i
public string fieldB1;
public int fieldB2;
public void MethodBl()
{
i
Console.WriteLine("Это класс В");
Console.WriteLine ("поле1 = {0}, поле2 = {1}", fieldB1, fieldB2);
}
}
Обратите внимание: конструктор клиента (класса B) отвечает за инициализацию полей класса, поэтому он должен создать объект поставщика (класса A), вызывая, как правило, конструктор поставщика. Если для создания объектов поставщика требуются аргументы, то они должны передаваться конструктору клиента, как это сделано в нашем примере.
После того как конструктор создал поле — объект поставщика — методы класса могут использовать этот объект, вызывая доступные клиенту методы и поля класса поставщика. Метод класса B — MethodB1 начинает свою работу с вызова: i
Расширение определения клиента класса
До сих пор мы говорили, что клиент содержит поле, представляющее объект класса поставщика. Это частая, но не единственная ситуация, когда класс является клиентом другого класса. Возможна ситуация, когда метод клиентского класса локально создает объект поставщика, вызывает его методы в собственных целях, но по завершении метода локальный объект заканчивает свою жизнь. Еще одна возможная ситуация — когда объекты поставщика вообще не создаются ни конструктором, ни методами класса клиента, но клиент вызывает статические методы класса поставщика. Оба эти варианта демонстрируют следующие два метода класса B;
public void MethodB2()
{
ClassA loc = new ClassA("локальный объект A",11);
loc.MethodA ();
}
public void MethodB3()
{
ClassA.StatMethodA();
}
Дадим теперь расширенное определение клиента.
Определение 3. Класс B называется клиентом класса A, если в классе B создаются объекты класса A — поля или локальные переменные — или вызываются статические поля или методы класса A.
Отношения между клиентами и поставщиками
Что могут делать клиенты и что могут делать поставщики? Класс-поставщик создает свойства (поля) и сервисы (методы), предоставляемые своим клиентам. Клиенты создают объекты поставщика. Вызывая доступные им методы и поля объектов, они управляют работой созданных объектов поставщика. Клиенты не могут ни повлиять на поведение методов поставщика, ни изменить состав предоставляемых им полей и методов, они не могут вызывать закрытые поставщиком поля и методы класса.
Класс-поставщик интересен клиентам своей открытой частью, составляющей интерфейс класса. Но большая часть класса может быть закрыта для клиентов — им незачем вникать в детали представления и в детали реализации. Сокрытие информации вовсе не означает, что разработчики класса не должны быть знакомы с тем, как все реализовано, хотя иногда и такая цель преследуется. В общем случае сокрытие означает, что классы-клиенты строят свою реализацию, основываясь только на интерфейсной части класса-поставщика. Поставщик закрывает поля и часть методов класса от клиентов, задавая для них атрибут доступа private или protected. Он может некоторые классы считать привилегированными, предоставляя им методы и поля, недоступные другим классам. В этом случае поля и методы, предназначенные для таких vip-персон, снабжаются атрибутом доступа internal, а классы с привилегиями должны принадлежать одной сборке.