Главная » C# » Принципы работы наследования и компонентов Visual C# (Sharp)

0

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

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

Иллюстрация наследования с помощью фигуры, прямоугольника и квадрата

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

abstract class Shape {

public abstract double CalculateArea();

}

Метод caicuiateArea ()  используется для вычисления площади фигуры. Он объяен абстрактным и должен быть реализован в производном классе.

Для начала, определим класс  Square, который представляет квадратную фигуру:

class Square : Shape { double _width;

public double Width {

get {

return _width;

}

set {

_width = value;

}

}

public override double CaicuiateArea() { return Width * Width;

}

Квадрат имеет только один размер, width, который представляет размер стороны квадрата. В определении класса был определен метод CaicuiateAreaо, который вычисляет площадь квадрата, умножая свойство width на само себя.

Так как прямоугольник является разновидностью квадрата, то класс Rectangle нледует от класса square:

class Rectangle :  Square { double _length; .

public double Length { get {

return _length;

}

set {

_length = value;

}

}

public new double CaicuiateArea() { return Width * _length;

}

}

Так как для описания прямоугольника размера только одной стороны недостатоо, то мы добавляем свойство Length. В реализации метода для вычисления плади Rectangle . CaicuiateArea () длина умножается на ширину.

Посмотрите внимательно, как объявлен метод caicuiateArea (). В случае с метом Rectangle.caiculateAreaо было использовано ключевое СЛОВО new, а не override. Это было сделано с целью обеспечения однородности вычислений. Под однородностью вычислений имеется в виду, что при выполнении определенных вычислений с типом мы получаем ответ, ожидаемый от данного типа, а не от како-либо другого типа.

Предположим, что вы создали экземпляр типа Rectagle, с которым впоследствии выполнили понижающее приведение к типу square. При вызове метода CaiculateArea () вы хотите, чтобы он выполнял вычисления, как для квадрата, а не как для прямоугольника. Таким образом, использование ключевого слова new в моде Rectangle .CaiculateArea () обеспечивает, что площадь квадрата вычисляется способом для квадрата, а прямоугольника — способом для прямоугольника.

Но не все так просто, как кажется. Допустим, что Rectangle приведен с пониженм к shape. Так как при вызове метода CaiculateArea () объявляется наследование, то площадь вычисляется методом для квадрата, что является неправильным. Птому кажется, что вместо ключевого слова new нужно использовать ключевое сло override. Но если ЭТО решает проблему С методом Shape.CaiculateArea о, ТО при преобразовании прямоугольника в квадрат площадь представляет прямоуголик, а не квадрат. Таким образом, мы имеем ситуацию, для которой нет решения.

Для иллюстрации этих различий рассмотрим следующий исходный код для вычиения площади фигуры  Rectangle:

Rectangle els = new Rectangle(); els.Width =20 ;

els.Length = 30;

double area = els.CaiculateArea();

В данном примере создается экземпляр класса Rectangle и свойствам width и Length присваиваются значения 20 и 30 соответственно. При вызове метода CaiculateArea ()  вычисленное значение площади сохраняется в переменной  area.

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

Rectangle rectangle = new Rectangle(); rectangle.Width = 20;

rectangle.Length = 30;

Square square = rectangle;

double area = square.CaiculateArea(); Console.WriteLine("Square Area is " + square.CaiculateArea() +

" Rectangle Area is " + rectangle.CaiculateArea());

Здесь переменная rectangle имеет тип Rectangle. Сторонам прямоугольника прваиваются  размеры,  после  чего  прямоугольник  преобразуется  в  квадрат  и  при-

сваивается переменной square. Когда используется ключевое слово new, то пладь будет 400, что верно, т. к. квадрат имеет размер только для одной стороны, который равен 20.

Теперь допустим,  что  мы  использовали  ключевое  слово  override и  привели тип к Square. Значение width так и останется 20, но площадь уже будет 600. И если мы протестируем вычисление площади квадрата, то результаты этого тестирования будут отрицательными.

Пример с классом shape демонстрирует, что даже если вы думаете, что квадрат побен прямоугольнику, один из этих классов не может наследовать от другого, не вызывая какого-либо рода проблем. Поэтому shape должен быть реализован как интерфейс, а не как базовый класс. А класс Square является  базовым  классом класса  Rectangle, но каждый из этих классов реализует интерфейс shape. Тогда у нас будет единообразное поведение. Решение выглядит таким образом:

interface IShape {  double CalculateArea();

}

class Square : IShape { double _width;

public double Width { set {

width = value;

} get {

return _width;

}

}

public double CalculateArea() { return _width * _width;

}

}

class Rectangle : Square, IShape { double Height;

public double Height { set {

height = value;

}

get {

return Height;

}

} public new double CalculateArea() {

return Width * Height;

}

}

В результате данной модификации наследования с использованием как интерфеов, так и классов поведение метода calculateArea () будет единообразным, незисимо от того, к какому типу может привестись экземпляр Rectangle. Верифацию  на  отсутствие  проблем  с  единообразным  поведением  можно  выполнить с помощью следующего кода:

Rectangle rectangle = new Rectangle(); rectangle.Height = 30;

rectangle.Width = 20;

Square square = rectangle;

IShape shapeCastFromRectangle = rectangle; IShape shapeCastFromSquare = square;

Console.WriteLine("Area Rectangle (" + rectangle.CalculateArea() + ") Area Square (" + square.CalculateArea()+

") Area Cast From Rectangle (" + shapeCastFromRectangle.CalculateArea() + ") Area Cast From Square (" +

shapeCastFromSquare. CalculateArea() + ")");

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

ПРИМЕЧАНИЕ

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

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

Фигуру также можно реализовать с помощью компонентов. Использование компентов означает определение идеи, которое следует определением реализации этой идеи. Разработка и реализация компонентов отличаются от разработки и реализии иерархий наследования. При наследовании  необходимо  принимать во  внимие приведение типов, функциональность базовых классов, а также подмену и пегрузку методов и свойств. {Приведением типа называется преобразование одного типа в другой с применением или без применения явного оператора  приведения типа.) При работе с компонентами необходимо мыслить концепциями и их реалацией в виде интерфейсов.

Рассмотрев реализации shape, Rectangle и square, мы можем определить интеейс ishape следующим образом:

interface IShape {  double CaicuiateArea();

double Width { get; set; }

}

К этому объявлению можно даже добавить свойство Length, но, тем не менее, оая идея интерфейса ishape неверна. Думаем ли мы о фигуре в терминах ее длины и ширины? Пожалуй, нет. Скорее, мы представляем себе фигуру как комбинацию площади, периметра и других свойств, общих для всех фигур. А длина и ширина не являются определяющими для всех фигур. Например, круг определяется радиусом или диаметром, а треугольник— размером основания, высотой и смещением веины треугольника. Суть состоит в том, что концепция фигуры не является коепцией прямоугольника или квадрата.

Поэтому правильное определение концепций в виде интерфейсов  будет  выглеть так:

interface IShape {  double CaicuiateArea();

}

interface ISquare : IShape { double Width { get; set; }

}

interface IRectangle : IShape { double Width { get; set; } double Length { get; set; }

}

Данное определение содержит три интерфейса: ishape, определяющий фигуру; IRectangle, определяющий прямоугольник; isquare, определяющий квадрат. Иерфейс ы IRectangle И ISquare ЯВЛЯЮТСЯ прОИЗВОДНЫМИ Интерфейс а IShape. Эт о означает, что они также ishape. Интерфейсы isquare и IRectangle независимы друг от друга, т. к. они являются разными фигурами, хотя с виду и похожи (вообщо квадрат — это разновидность прямоугольника, но прямоугольник не обязатело квадрат).

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

class Squarelmpl : ISquare, IRectangle { } class Rectanglelmpl -. IRectangle { >

Класс Squarelmpl реализует поведение интерфейсов ISquare И IRectangle, а также моделирует реальность, где квадрат также является и  прямоугольником.  А  класс Rec tangle imp 1 реализует только поведение IRectangle, демонстрируя, что прямгольник может быть только прямоугольником, но не квадратом.

Теперь  написание  кода,  в  котором  реализация  выдает  неоднородные  результаты, является невозможным. Например, следующий код является невозможным:

IRectangle rectangle = new Rectanglelmpl(); ISquare square = (ISquare)rectangle;

Но этот код также разрешается:

IRectangle rectangle = new Squarelmpl(); ISquare square = (ISquare)rectangle;

ПРИМЕЧАНИЕ

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

В качестве оптимизации было бы возможным следующее наследование интерфейса:

interface ISquare : IShape { double Width { get; set; }

}

interface IRectangle : ISquare { double Length { get; set; }

}

или:

interface ISquare : IRectangle {

}

interface IRectangle : IShape { double Width { get; set; } double Length { get; set; }

}

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

хотя  она  может  и  не  иметь  их.  Таким  образом,  при  использовании  наследования с интерфейсами ishape в качестве базового интерфейса iRectangie является дустимым, но взаимосвязь между iRectangie и isquare — нет. Имейте в виду, что данное взаимоотношение может возникнуть при реализации и может быть  извлено с помощью приведения типов.

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

Источник: Гросс  К. С# 2008:  Пер. с англ. — СПб.:  БХВ-Петербург, 2009. — 576 е.:  ил. — (Самоучитель)

По теме:

  • Комментарии