double arrow

Отношение вложенности

До сих пор мы рассматривали возможность выстраивания отношений между классами, основанные на использовании только классического наследования. Теперь остановимся на том как могут быть реализованы отношения вложенности между классами, в чем их отличительные особенности. Начнем с простого примера. Предположим, что нам нужен класс объектов, отображающих геометрическую линию. Для этого можно воспользоваться ранее созданным нами классом точки. Очевидно, что отношение прямого наследования ("is-a") между типами для описания класса точки (Point) и описания класса линии (Line) было бы возможным, но выглядело бы достаточно странно. Является ли точка линией? Вряд ли. Но ясно и другое, что какая-то связь между ними все-таки существует. Попробуем отобразить ее в следующем определении класса Line. Построим класс Line, выполняющий роль контейнера, с вложенными в него двумя экземплярами класса Point:

class Line

{

Point begin, end; // объекты начальной и конечной точек линии

public Line()

{

begin=new Point(100, 100);

end=new Point(200, 100);

}

public Line(int x1, int y1, int x2, int y2)

{

begin=new Point(x1, y1);

end=new Point(x2, y2);

}

public int GetLenght()

{

Math.Sqrt( Math.Pow(end.GetX()-begin.GetX(), 2) +

Math.Pow(end.GetY()-begin.GetX(), 2) );

}

public Move(int dx, int dy)// Делегирует функциональную возможность класса точки

{

begin.Move(dx, dy);

end.Move(dx, dy);

}

}

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

Теперь, после того как конструктор создал поле вложенного объекта, методы владеющего класса могут его использовать, вызывая доступные ему методы и поля вложенного экземпляра класса. Метод (Move) класса Line выполняет свою работу с помощью вызова методов begin.Move(dx, dy) и end.Move(dx, dy), используя сервис, поставляемый методом Move(dx, dy) класса Point..

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

Расширение понятия о владеющем классе

До сих пор мы рассматривали случай, когда класс-контейнер содержит поле, представляющее собой вложенный в него объект (экземпляр) другого класса. Несмотря на то, что это довольно частая, но далеко не единственная ситуация, когда класс использует возможности другого класса. Возможна ситуация, когда метод владеющего класса локально создает вложенный объект другого класса и вызывает его методы в собственных целях, но по завершении работы такого метода заканчивает свою жизнь и локальный объект. Еще одна возможная ситуация - когда объекты вообще не создаются ни конструктором, ни методами класса клиента, а объект владеющего класса вызывает статические методы вложенного объекта. Оба эти варианта демонстрируют следующие два метода класса B:

public class A

{

public A(string s, int v) { str = s; val = v; }

public string str;
public int val;
public void MethodA()
{

Console.WriteLine( "Это класс A");
Console.WriteLine ("поле1 = {0}, поле2 = {1}", str, val);

}

public static void StatMethodA()
{

string s1 = "Статический метод класса А";
string s2 = "Помните: 2*2 = 4";
Console.WriteLine(s1 + " ***** " + s2);

}

}

public class B

{

public void MethodB1()

{

A inner = new A("локальный объект А",77);

inner.MethodA();

}

public void MethodB2()

{

A.StatMethodA();

}

}

Следует заметить что можно предложить еще один вариант класса В, который обладал бы точно такими же функциональными возможностями, как и приведенный вариант. Например:

public class B

{

class A //Определение вложенного класса А

{

public A(string s, int v) { str = s; val = v; }

public string str;
public int val;
public void MethodA()
{

Console.WriteLine( "Это класс A");
Console.WriteLine ("поле1 = {0}, поле2 = {1}", str, val);

}

public static void StatMethodA()
{

string s1 = "Статический метод класса А";
string s2 = "Помните: 2*2 = 4";
Console.WriteLine(s1 + " ***** " + s2);

}

}

public void MethodB1()

{

//Создание локального объекта (экземпляра) вложенного класса А

A inner = new A("локальный объект А",77);

inner.MethodA(); //Вызов метода вложенного объекта класса А

}

public void MethodB2()

{

A.StatMethodA();//Вызов статического метода вложенного класса А

}

}

На основе приведенных примеров можно дать более расширенное определение владеющего класса (контейнера), которое можно сформулировать следующим образом: класс B называется владельцем класса A, если внутри класса B:

- определяется вложенный класс А;

- или создаются экземпляры класса A в виде полей или локальных переменных;

- или используются статические поля или методы класса А;

Рассмотри отношения, которые могут возникать между владеющими и вложенными классами.

Отношения между владеющими и вложенными классами

Что могут делать владеющие классы (классы-контейнеры) и что могут делать вложенные классы или вложенные объекты других классов? Вложенный класс имеет в своем распоряжении поля и методы и предоставляет их для использования своему владеющему классу. Владеющие классы или классы-контейнеры создают подчиненные им экземпляры других классов. Вызывая доступные им методы из полей подчиненных им объектов, они могут управлять их работой. При этом владеющие классы не могут ни повлиять на поведение подчиненных им объектов, ни изменить состава их функциональных возможностей.

Вложенные классы или экземпляры других классов интересны их владельцам только своей открытой частью, составляющей интерфейс класса. Но большая часть членов подчиненных классов обычно закрывается от вмешательства владеющих классов, поскольку им незачем вникать в детали их внутреннего представления и в особенности их реализации. Такое сокрытие информации вовсе не означает, что разработчики класса-контейнера не должны быть знакомы с тем, как все реализовано во вложенных в него объектов других классов, хотя иногда преследуется и такая цель. В общем случае сокрытие означает, что классы-контейнеры строят свою реализацию, основываясь только на интерфейсной части вложенных в них класса или их экземпляров. Встроенный в класс-контейнер класс или встроенный экземпляр класса закрывает свои поля и часть методов от своего владельца, задавая для них атрибут доступа private или protected. Но некоторые элементы своего класса-контейнера вложенный элемент может считать привилегированными, предоставляя им методы и поля, недоступные другим классам. В этом случае поля и методы, снабжаются атрибутом доступа internal, а классы вложенных элементов класса-контейнера с такими привилегиями должны принадлежать одной сборке.

Особенности использования владеющих и вложенных классов

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

- Может ли вложенный элемент быть того же класса, что и его владелец или нет?

- Могут ли два класса быть одновременно и контейнерами и элементами, вложенными друг в друга?

Ответы на оба эти вопросы положительны, а подобные ситуации достаточно типичны и не являются какой-либо экзотикой.

Первая ситуация наиболее характерна для динамических структур данных, например, списков. Элемент односвязного списка имеет поле, представляющее ссылку на следующий элемент (такого же типа, как и текущий элемент) односвязного списка. Элемент двусвязного списка имеет два таких поля (ссылки предыдущий и последующий элементы списка). Но подобная ситуация характерна не только для рекурсивно определяемых структур данных. Рассмотрим еще один пример. В классе Person могут быть заданы два поля - Father и Mother, задающие родителей персоны, и массив Children. Понятно, что все эти объекты могут быть того же класса что и класс Person.

Достаточно часто встречается и такая ситуация, когда классы имеют поля, которые взаимно ссылаются друг на друга. Например, классы Мужчина и Женщина, первый из которых имеет поле "жена" класса Женщина, а второй - поле "муж" класса Мужчина.

Таким образом, несмотря на то, что классы устроены достаточно просто, но они позволяют выстраивать между ними довольно сложные отношения.

Абстрактные классы.

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

Абстрактный метод создается с помощью модификатора abstract. Абстрактный метод не имеет тела и, следовательно, не реализуется базовым классом, а производные классы должны его обязательно переопределить. Абстрактный метод автоматически является виртуальным, однако использовать спецификатор virtual не нужно. Более того, если вы попытаетесь использовать два спецификатора одновременно, abstract и virtual, то компилятор выдаст сообщение об ошибке.

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

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

Рассмотрим конкретный пример, в котором приводится описание абстрактного класса фигуры и производных от него классов окружности и прямоугольника.

// Абстрактный класс фигуры

abstract class Figura

{

Color borderColor, // Цвет контура

fillColor; // Цвет закраски

public Figura(){ borderColor=Color.Black; fillColor=Color.Red; } // Конструктор

// Функции доступа к закрытым данным:

public void SetBColor(Color c);

public void SetFColor(Color c);

// Абстрактные функции, реализация которых будет зависеть от порождаемых классов:

public abstract void Draw(Graphics g);

public abstract void Move(int dx, int dy);

}

class Circle : Figura

{

// Собственные переменные:

Point pnt; // Положение центра окружности

int radius; // Радиус окружности

// Конструктор по умолчанию:

public Circle(){ pnt = new Point(200,200); radius=100; }

// Абстрактные функции базового класса, которые необходимо реализовать:

public override void Draw(Graphics g) //Реализация функции рисования

{

g.DrawCircle(…);

}

public override void Move(int dx, int dy) // Реализация функции перемещения

{ pnt.offset(dx, dy); }

}

class Rect : Figura

{

// Собственные данные:

Point p1, p2; // Положение левой-верхней и правой-нижней вершин прямоугольника

// Конструктор по умолчанию:

public Rect() { p1 = new Point(100,100); p2 = new Point(200,200); }

// Абстрактные функции базового класса, которые необходимо реализовать:

public override void Draw(Graphics g) // Собственная реализация функции рисования

{

g.DrawRectangle(…);

}

public override void Move(int dx, int dy) // Собственная реализация функции перемещения

{ p1.offset(dx, dy); p2.offset(dx, dy); }

}

Как видно из листинга, абстрактный класс фигуры содержит две переменные, конструктор и два абстрактных метода Draw и Move. Этот класс создан для того, чтобы порождать от него другие классы, характеризующие разные геометрические фигуры. В данном случае это классы описания объектов типа окружности и прямоугольника. Поскольку заранее невозможно определить какой из методов будет использоваться порождаемыми классами для своего вырисовывания или перемещения, то в базовом классе фигуры эти методы объявляются абстрактными. Но поскольку класс содержит абстрактные методы, то и сам класс объявляется как абстрактный.

Порождая производные классы окружности и прямоугольника от абстрактного класса фигуры мы с помощью модификатора override переопределяем в каждом из них абстрактные методы Draw и Move, описывая в их телах конкретную реализацию, учитывающую специфику порождаемого класса. Теперь в объявлениях классов окружности и прямоугольника можно убрать спецификатор abstract, сообщая тем самым о том, что теперь они являются полностью определенными классами, которые можно использовать для создания объектов.

Итак, подведем итоги. Класс называется абстрактным, если он имеет, хотя бы один, абстрактный метод. Метод называется абстрактным, если при определении метода задана только его сигнатура и не задана его реализация. Объявление абстрактных методов и абстрактных классов должно сопровождаться модификатором abstract. Поскольку абстрактные классы не являются полностью определенными классами, то нельзя создавать объекты абстрактных классов. При переопределении абстрактного метод базового класса в производном классе необходимо переопределить его с указанием модификатора override.


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