Этюды в тональности C#(продолжение).

Введение.

В статьях этого цикла, я постараюсь рассказать вам о тех конструкция языка C#, что ранее были оставлены мной за рамками повествования. На тот случай если вы не читали предыдущие статьи цикла, публиковавшиеся в "Компьютерной газете", рекомендую посетить мою авторскую веб-страничку на сайте издательства (короткий путь к ней http://german2004. da. ru). На этой страничке вы найдете все публиковавшиеся мной в газете статьи по тематике Net Framework, да и на другие темы. Сегодня мы с вами поговорим о понятии интерфейса.

Интерфейсы.

Интерфейсы это вершина, "пик" объектно ориентированного программирования. ООП всегда тяготела к абстрагированию используемых в программном проекте "сущностей". Интерфейс же - это полная абстракция, дальше идти уже некуда (хотя кто знает?). Описывая его, вы не вводите ни одной строчки реального кода, подлежащего выполнению, а лишь описываете то, как этот код должен выглядеть. Так сказать, строите предварительный макет, по модели которого, впоследствии, будете формировать реальный код. С точки зрения синтаксиса языка C# интерфейс описывается следующим образом:


public interface IMyInterface {

}


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

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


public interface IMyNewInterface:IMyInterface{

bool HasPivo{get;}

void Drink();

}


В этом примере мы описываем интерфейс IMyNewInterface, являющегося потомком интерфейса ImyInterface из моего предыдущего примера. Этот новый интерфейс добавляет своему предку описание доступного только для чтения булевского свойства HasPivo и метода Drink, не имеющего параметров и не возвращающего каких либо параметров.

Главным достоинством интерфейса является то, что реальный тип объекта, реализующего тот или иной интерфейс, никакой роли не играет. Так в нашем примере нет никаких ссылок на какие-либо объекты Net Framework. Если класс способен реализовать нужную нам функциональность, нам совершенно безразлично произошел этот класс, скажем, от System. Windows. Forms. Panel или, к примеру, от System. Data. OleDb. OleDbCommand. Для того чтобы класс мог реализовывать интерфейс IMyInterface, требуется лишь, чтобы в нем были реализованы указанные свойство и метод. Все остальное не суть важно, в том числе и место объекта в иерархии классов Net. Framework.

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

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

Подобно тому, как делегат является не более чем "договором" о том, как должен выглядеть некий метод, так интерфейс это примерно такой же “договор” о том, как именно должен выглядеть некий объект. Описывая интерфейс, вы задаете набор свойств или методов, которые гарантировано должны быть в объекте, реализующем этот интерфейс. Предварительное знание об их существовании и позволяет компилятору, в дальнейшем, к ним спокойно обращаться.

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

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

Один и тот же объект может реализовывать сразу несколько интерфейсов. Почему бы и нет? Ведь мы можем спросить у человека одолжившего нам зажигалку, который час? Разумеется, если у него есть часы. А наличие часов, с точки зрения бытовой логики это уже другой интерфейс.

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

Для начала опишем сам объект зажигалки, обладающий методом Go, ответственным за процесс прикуривания.


// Описываем саму зажигалку.

public class Lighter{

public void Go(){Console.WriteLine("Прикуриваем...");}

}

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

// интерфейс владельца зажигалки

public interface ILighterMan{

Lighter lighter{get;set;}

}


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

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


// интерфейс владельца часов

public interface IHasTime{

string GetTime();

}


Следуюущим шагом приступим к описанию классов, описывающих прохожих на нашей гипотетической улице.

Первый вид прохожих нам не интересен. У нет ни часов, ни зажигалки. С точки зрения программирования это ничем не примечательный потомок класса Object, не обладающий какими-либо методами или свойствами.


// обычный прохожий

class Walker {}


Следующий класс намного любопытнее. Это прямой потомок нашего предыдущего класса, но уже обладающий реализацией интерфейса ILighterMan. Мы указываем этот факт, ссылаясь на название интерфейса, через запятую, в списке предков нашего объекта. Как только мы взяли на себя ответственность за реализацию интерфейса, нам придется добавить к нашему будующему объекту реализацию свойства lighter, объявленного в интерфейсе.

Для этого я описал в теле класса private переменную класса Lighter (lighter1), и добавил публичное свойство для доступа к нему, по маске описанной в интерфейсе. Таким образом, мы выполнили "контракт" по реализации интерфейса ILighterMan. Объявленное в нем обязательное свойство теперь реализовано и мы можем смело утверждать о том, что данный класс реализует интерфейс ILighterMan.


// потомок Walker с зажигалкой.

class WalkerObject : Walker, ILighterMan {

private Lighter lighter1=new Lighter();

public Lighter lighter{

get{return lighter1;}

set{lighter1=value;}

}

}


Третий вид прохожих попадающихся на нашей улице выглядит еще вычурнее. Для того чтобы подчеркнуть вам достоинство от использования интерфейсов, я унаследовал его не от вновь создаваемой нами иерархии классов производных от Walker, а от класса System. Data. DataColumn. Я намеренно выбрал столь неественный для нашей программы класс. Надеюсь, вы еще не забыли о том, что понятие реализации интерфейса никак не связано с понятием наследования в иерархии классов Net. Framework? Благодаря этому обстоятельству мы можем оформлять реализацию нужного нам интерфейса в самых непредсказуемых, на первых взгляд, объектах.

Также этот вид объекта реализует сразу два интерфейса, ставший привычным для нас интерфейс ILighterMan, а также интерфейс IHasTime, отвечающий за наличие часов. Наследование дополнительного интерфейса накладывает на нас обязательство реализовать, помимо свойства lighter, описанного в интерфейсе ILighterMan, еще и метод GetTime, описанный в интерфейсе IHasTime.


// DataColumn "с зажигалкой" и часами

class WalkerDataColumn:

System.Data.DataColumn,ILighterMan,IHasTime {

private Lighter lighter1=new Lighter();

public Lighter lighter{

get { return lighter1; }

set { lighter1=value; }

}

public string GetTime(){

return System.DateTime.Now.ToString();

}

}


Описав всех действующих лиц, можем приступать к реализации основного потока программы, моделирующего поведение прохожих на улице. Основной поток, для простоты примера, мы реализуем прямо в методе Main нашей программы. Если бы я писал реальную программу, а не пример, я бы его оформил, скорее всего, в виде отдельного класса. Я намеренно подробно расписал в комментариях каждый шаг нашей программы. Обратите внимание на то, что, получая доступ к объекту, мы приводим его к типу нужного интерфейса, а не к типу какого-либо объекта.


 static void Main(string[] args){ 

// создаем динамический массив.

System.Collections.ArrayList Street =

new System.Collections.ArrayList();

int i=0;

// в цикле добавляем в массив 15 объектов,

// по 5 штук каждого вида

for(i=0;i<5;i++){

Street.Add(new Walker());

Street.Add(new WalkerObject());

Street.Add(new WalkerDataColumn());

}

// перебираем все объекты в массиве.

foreach(object walker in Street){

// выводим строчку с описанием типа прохожего.

Console.WriteLine("\nВстретили : "+walker.GetType());

if(walker is ILighterMan){

// вот тут хитро, смотрите к чему

// мы приводим объект. Уловили идею?

ILighterMan lighterMan = (walker as ILighterMan);

//берем зажигалку у прохожего

Lighter lighter = lighterMan.lighter;

// прикуриваем

lighter.Go();

// отдаем зажигалку обратно

lighterMan.lighter = lighter;

// выясняем есть ли у прохожего часы

if(walker is IHasTime){

string time = (walker as IHasTime).GetTime();

Console.WriteLine("Выяснили время : " +time);

}

}else {

// встреченный объект не поддерживает ILighterMan

Console.WriteLine("Тут нам ничего не светит...");

}

}

// Ждем ввода Enter

Console.WriteLine("Press Enter for continue...");

Console.ReadLine();

}


Обратите свое внимание на довольно любопытный нюанс этой программы. Мы с вами добавляем в массив совершенно разнородные объекты. В дальнейшем мы вовсе не интересуемся их настоящим типом! Этот самый «настоящий тип объекта» нам просто неинтересен. Мы просто выясняем, с помощью команды is, реализован ли в этом объекте нужный нам интерфейс. Если такой интерфейс присутствует, мы приводим этот объект к виду эту интерфейса. Внешне все это выглядит как создание переменной типа интерфейса. Дальше мы совершенно спокойно работаем с объектом таким образом, как будто это экземпляр некоего гипотетического объекта ILighterMan. Но заметьте, в реальности это объект совершенно другого типа, а объекта типа ILigherMan так и вовсе не существует в природе! Тем не менее, мы пользуемся реализуемыми им методами и свойствами, в рамках объявленного нами интерфейса. Этот программный прием широко используется в иерархии классов Net. Framework, сошлюсь в качестве примера на пространство имен System. Collection. В нем имеется множество интерфейсов предназначенных для создания списков, коллекций, а также сравниваемых и клонируемых классов.

(с) Герман Иванов

[Продолжение следует.]
 

Ссылки:


При перепечатке сохранение раздела "Ссылки" обязательно!!!