13 июня 2013 г.

Интерфейс, как это?

Всем привет.

Вот от скуки и от хандры решил написать статейку про темную но полезную лошадку в ООП - интерфейсы.

Как мы помним из моих предыдущих очерков, основой ООП являются объекты. А сами обьекты, являются реализацией (экземплярами) классов.

Так вот, на ряду с классами, в ООП присутствует еще один фигурант - интерфейс. Это на самом деле очень полезная и при этом нереально удобная для программистов вещь, хотя не каждый нею пользуется (в частности я почему-то редко). Значит интерфейс. Интерфейс можно назвать шаблоном для класса. То есть, если шаблон - то значит одним классом все не кончиться. Так и есть. Начну с примера.



У нас есть куча разных коробок, лампочек, баночек, звуковых колонок, мышек, старых бабушкиных бус итп, короче куча разного барахла. И нам нужно произвести над ними некое одинаковое действие. Например сравнить размеры. Как мы поступим? Будем брать и прикладывать всё это барахло друг к другу и делать некие умозаключение, мол этот шире чем тот, тот выше чем этот итп. В реальной жизни это примерно так и выглядело бы, но не в жизни ООП, так как мы не можем написать какой-нить сторонний класс, метод которого бы сравнивал котлеты с мухами (а у нас примерно это и получается).

Поэтому нам на помощь приходит интерфейс. Что мы делаем? Мы пишем сам интерфейс(шаблон), и в каждом классе (мухе, котлете, телевизоре, шарике для пинг-понга) реализовуем этот интерфейс. Таким образом, мы получаем кучу разных вещей (сущностей) но с набором одинаковых возможностей (методов), что позволяет нам мерить их под одну линейку. При этом, в отличии от класса, область видимости данного метода (private, public, protected или internal) не указывается.

Просьба кстати не путать интерфейсы с базовыми классами - это разные вещи, хотя они друг другу не мешают. Так, погнали дальше. Что из себя представляет интерфейс в коде. Интерфейс по своей структуре напоминает некий класс, он тоже имеет методы, только не описывает их реализации. Другими словами, мы говорим, что у нас есть такой то метод и он называется вот так и он возвращает значения такого типа. Вот примерно так:
package{
public interface IИмя_интерфейса
{
 function имя_метода(имя_параметра:Number):void;
 function имя_метода():int;
 function get имя_метода():String;
}
}

Выше приводится пример того как выглядит интерфейс. Как я говорил, он очень похож на класс, но отличается тем, что не несет никакой бизнес логики (реализации). Так же хочу обратить внимание на имя интерфейса. Обычно интерфейсы принято называть по тем же правилам что и классы, но перед самим именем ставить большой символ 'I'. Таким образом, среди файлов проекта мы можем понять где находятся классы а где интерфейсы. Вот давайте теперь попробуем написать интерфейс для нашего примера. Как мы говорили выше - мы будем что-то измерять. Поэтому нам для всего вышеперечисленного барахла, нужно выделить 4 свойства (свойства мы будем определять через get/set функции, так как в ActionScript 3.0 применение переменных в интерфейсах не допускается, только функции) ну и пускай 1 метод.

Ну я думаю что логично выделить 3 свойства: высота, ширина и длинна. Четвертым свойством будет имя. Ну а в качестве метода мы напишем метод toString(), который каким-то образом будет выводить имя нашего объекта. Поехали:
package{
public interface IBarahlo
{
 // getter-ы/setter-ы наших свойств
 function set bHeight(a:Number):void;
 function get bHeight():Number;
 function set bWidth(b:Number):void;
 function get bWidth():Number;
 function set bLength(c:Number):void;
 function get bLength():Number;
 function set bName(d:Number):void;
 function get bName():Number;
 // ну и наш метод
 function bToString():void;
}
}

Ну вот наш интерфейс для барахла готов(имена методов я начал с буквы b для того, что бы в дальнейшем не было конфликтов имен). Ну а теперь давайте создадим пару классов реализующих (описывающих) данный интерфейс. Для этого объявим класс и после имени напишем слово implements, которое означает реализацию. После этого слова вписываем имя нашего интерфейса. Далее нам придется написать все методы которые указаны в этом интерфейсе (в FlashDevelop это сделать очень легко, достаточно установить курсор на имя интерфейса, после слова implements, и нажать чудо комбинацию Ctrl+Shift+1 и в появившемся меню, выбрать "Implement interface metods" и FlashDevelop сам добавит в ваш класс все методы с пустыми телами). Вот примерно так.

Для примера, напишем сразу класс, который будет описывать самый абстрактный в мире объект - АХИНЕЮ (или другими словами, черт знает что).

Пример:
package  
{
 import flash.display.Sprite;
 
 public class Ahinea implements IBarahlo
 {
  // создаем несколько переменных, которые будут хранить (пытаться хранить) наши данные.
  private var _name:String;
  private var _h:Number;
  private var _w:Number;
  private var _l:Number;
  
  public function Ahinea() 
  {
   _name = "Ahinea";
   _h = 20;
   _w = Number.POSITIVE_INFINITY;
   _l = -4;
   //А почему бы и нет, ведь это же ахинея!!!
   
  }
  
  public function set bHeight(height:Number):void 
  {
   _h = height * 15;
  }
  
  public function get bHeight():Number 
  {
   return _h/Math.PI;
  }
  
  public function set bWidth(width:Number):void 
  {
   _w = width + 4;
  }
  
  public function get bWidth():Number 
  {
   return _w;
  }
  
  public function set bLength(length:Number):void 
  {
   _l = Math.random() * Math.abs(Math.round(length));
  }
  
  public function get bLength():Number 
  {
   return _l;
  }
  
  public function set bName(n:String):void 
  {
   _name = n;
  }
  
  public function get bName():String 
  {
   return _name;
  }
  // наш метод, и тоже с подкаверкой =)
  public function bToString():void
  {
   var buffStr:String="*";
   for (var i:int = 0; i < _name.length; i++)
   {
    buffStr += _name.slice(i, i + 1) + "*";
   }
   trace(buffStr);
  }
 }
}

Можете даже не пытаться вчитываться в тела наших методов и свойств, так как там реализована ахинея =). Это сейчас не главное, главное что они все есть. Кстати это основное условие реализации интерфейса.

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

Для примера, давайте сделаем класс наследующий базовый класс Sprite и реализующий наш интерфейс IBarahlo и назовем его Box. Если кто не знает, класс Sprite является стандартным классом ActionScript 3.0 и представляет из себя отображаемый на сцене объект (аля MovieClip).

Приступимс:
package  
{
 import flash.display.Sprite;
 
 public class Box extends Sprite implements IBarahlo 
 {
  private var length:Number = 0;
  private var w:Number = 100; // для того что бы нарисовать прямоугольник
  private var h:Number = 100; //
  public function Box() 
  {
   draw();
  }
  
  /* INTERFACE IBarahlo */
  
  public function set bHeight(a:Number):void 
  {
   h = a;
   draw();
  }
  
  public function get bHeight():Number 
  {
   return height;
  }
  
  public function set bWidth(b:Number):void 
  {
   w = b;
   draw();
  }
  
  public function get bWidth():Number 
  {
   return width;
  }
  
  public function set bLength(c:Number):void 
  {
   // можем оставить пустым
  }
  
  public function get bLength():Number 
  {
   return Number.NaN; // тут оставить пустым не получиться, по этому тупо вернем NaN
  }
  
  public function set bName(n:String):void 
  {
   name = n;
  }
  
  public function get bName():String 
  {
   return name;
  }
  
  public function bToString():void 
  {
   trace(name);
  }
  
  public function draw():void // допишем свой метод, который будет все рисовать
  {
   graphics.clear();
   graphics.lineStyle(1, 0x00ff00);
   graphics.beginFill(0x0000ff, 1);
   graphics.drawRect(0, 0, w, h);
   graphics.endFill();
  }
 }
}

В двух словах, этот класс, рисует в себе синий прямоугольник. И так как это наследник от Sprite, его нужно будет при использовании добавить на сцену при помощи addChild(); Но это потом.

Я надеюсь вышесказанное понятно... Если нет - не здавайтесь, читайте далее, далее будет еще непонятнее =))))

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

Давайте лучше для полноты картины, напишем еще один класс реализующий наше барахло. Что бы извратиться по полной, предлагаю наш класс унаследовать от чего-нибудь не визуального, например flash.net.URLLoader. Кто не знает, это класс, позволяющий загружать из вне всякие разные штуки (текст, XML? переменные, итд).

Пример:
package  
{
 import flash.net.URLLoader;
 
 public class OurLoader extends URLLoader implements IBarahlo 
 {
  public function OurLoader() 
  {}   
  
  /* INTERFACE IBarahlo */
  
  public function set bHeight(a:Number):void 
  {}
  
  public function get bHeight():Number 
  {
   return Number.NaN;
  }
  
  public function set bWidth(b:Number):void 
  {}
  
  public function get bWidth():Number 
  {
   return Number.NaN;
  }
  
  public function set bLength(c:Number):void 
  {}
  
  public function get bLength():Number 
  {
   return Number.NaN;
  }
  
  public function set bName(n:String):void 
  {}
  
  public function get bName():String 
  {
   return "I don't have name =(. But I have DataFormat: "+dataFormat;
  }
  
  public function bToString():void 
  {
   trace(bName);
  }
 }
}

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

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

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

Пример:
package  
{
 import flash.display.Sprite;
 import flash.net.URLLoaderDataFormat;
 
 public class Main extends Sprite 
 {
  
  public function Main() 
  {
   trace("First class Ahinea");
   var a:Ahinea = new Ahinea();
   a.bToString();
   
   trace("Second class Box");
   var b:Box = new Box();
   b.x = 100;
   b.y = 200;
   addChild(b);
   b.name = "Box";
   b.bToString();
   
   trace("Third class OurLoader");
   var c:OurLoader = new OurLoader();
   c.dataFormat = URLLoaderDataFormat.TEXT;
   c.bToString();
  }
 }
}

Запускаем, смотрим - все работает! Но вы спросите - А причем тут интерфейсы то? А интерфейсы здесь для того, чтобы мы могли все эти три разнотипные сущности использовать как однотипные.

Смотрим пример:
package  
{
 import flash.display.Sprite;
 import flash.net.URLLoaderDataFormat;
 public class Main extends Sprite 
 {
  public function Main() 
  {
   trace("First class Ahinea");
   var a:Ahinea = new Ahinea();
   a.bToString();
   
   trace("Second class Box");
   var b:Box = new Box();
   b.x = 100;
   b.y = 200;
   addChild(b);
   b.name = "Box";
   b.bToString();
   
   trace("Third class OurLoader");
   var c:OurLoader = new OurLoader();
   c.dataFormat = URLLoaderDataFormat.TEXT;
   c.bToString();
   
   trace("--------------- Trick #1 --------------");
   var ar:Array = new Array();
   ar.push(a);
   ar.push(b);
   ar.push(c);
   for each(var barahlo:IBarahlo in ar)
   {
    barahlo.bToString();
   }
  }
 }
}
Опишу словами, у нас было три разнотипных объекта, мы поместили их в массив, и в цикле выдернули их из массива как объект IBarahlo и запустили метод bToString(). То есть другими словами, мы привели все наши объекты к некому типу ,способности которого ограничены интерфейсом(IBarahlo), хотя это не повлияло на работоспособность этих объектов.

Кстати я надеюсь вы заметили, что реализация методов интерфейса в каждом классе разная (и в этом вся прелесть). О изначальной работоспособности объектов можно удостовериться из примера (попробуйте потырцать синий квадрат):
package  
{
 import flash.display.Sprite;
 import flash.events.MouseEvent;
 import flash.net.URLLoaderDataFormat;
 
 public class Main extends Sprite 
 {
  public function Main() 
  {
   trace("First class Ahinea");
   var a:Ahinea = new Ahinea();
   a.bToString();
   
   trace("Second class Box");
   var b:Box = new Box();
   b.x = 100;
   b.y = 200;
   addChild(b);
   b.name = "Box";
   b.bToString();
   
   trace("Third class OurLoader");
   var c:OurLoader = new OurLoader();
   c.dataFormat = URLLoaderDataFormat.TEXT;
   c.bToString();
   
   trace("--------------- Trick #1 --------------");
   var ar:Array = new Array();
   ar.push(a);
   ar.push(b);
   ar.push(c);
   for each(var barahlo:IBarahlo in ar)
   {
    barahlo.bToString();
   }
   
   b.addEventListener(MouseEvent.CLICK, onBClick);
  }
  
  private function onBClick(e:MouseEvent):void 
  {
   var b:IBarahlo = e.target as Box;  // Вот он некий абстрактный тип подозрительной наружности =)
   b.bHeight = Math.random() * 300;
   b.bWidth = Math.random() * 300;
  }
 }
}
Ну а напоследок, как и обещал, давайте все таки посчитаем кто из наших объектов все таки длиннее (хех, пацанский такой спор получается =)).

Пример:
package  
{
 import flash.display.Sprite;
 import flash.events.MouseEvent;
 import flash.net.URLLoaderDataFormat;
 
 public class Main extends Sprite 
 {
  
  public function Main() 
  {
   trace("First class Ahinea");
   var a:Ahinea = new Ahinea();
   a.bName = "My name is Ahinea!!!";
   a.bToString();
   
   trace("Second class Box");
   var b:Box = new Box();
   b.x = 100;
   b.y = 200;
   addChild(b);
   b.name = "Box";
   b.bToString();
   
   trace("Third class OurLoader");
   var c:OurLoader = new OurLoader();
   c.dataFormat = URLLoaderDataFormat.TEXT;
   c.bToString();
   
   trace("--------------- Trick #1 --------------");
   var ar:Array = new Array();
   ar.push(a);
   ar.push(b);
   ar.push(c);
   ar.push(new Ahinea()); // Добавим парочку ахиней для веселья =)
   ar.push(new Ahinea());
   ar.push(new Ahinea());
   ar.push(new Ahinea());
   ar.push(new Ahinea());
   
   for each(var barahlo:IBarahlo in ar)
   {
    barahlo.bToString(); 
   }
   
   b.addEventListener(MouseEvent.CLICK, onBClick);
   trace("--------------- Trick #2 --------------");
   
   for each(var br:IBarahlo in ar) // выводим всех участников соц соревнования
   {
    trace(br.bName + ": " + br.bLength);
   }
   var biggest:IBarahlo = ar[0];
   for (var i:int = 1; i < ar.length-1; i++)
   {
    biggest = whoIsMoreBiggest(biggest, ar[i] as IBarahlo);
    var s:Number = 0;
   }
   // Чтото типа такого. Могут быть ошибки - писалось на скорую руку. Но дело не в этом.
   trace("--------------- Result --------------")
   trace("Biggest: "+biggest.bName);
  }
  
  private function onBClick(e:MouseEvent):void 
  {
   var b:IBarahlo = e.target as Box;
   b.bHeight = Math.random() * 300;
   b.bWidth = Math.random() * 300;
  }
  
  private function whoIsMoreBiggest(b1:IBarahlo, b2:IBarahlo):IBarahlo
  {
   if (b1.bLength > b2.bLength)
   {
    return b1;
   }
   else
   {
    return b2;
   }
  }
 }
}

Вот примерно в этом и заключается роль интерфейсов в ООП. Так же хотелось добавить, интерфейсы часто применяются в командных проектах. Кстати это очень удобное решений. Вот смотрите, у нас есть 3 разработчика, которые разрабатывают юнитов для некой игры. Эти разработчики, находятся в разных местах, и работают в разное время. Так же есть четвертый разработчик, который пишет модули взаимодействия этих юнитов (столкновения, попадание пуль, итд). Как нам поступить, что бы все разработчики работали слаженно и не ждали друг друга? Есть ответ - Интерфейсы! То есть, все 4 разработчика, договариваются о структуре обьектов юнитов и их взаимодействиях, на основе этого создают интерфейс для всех юнитов. Потом каждый из "юнитостроителей" реализовывает в коде своего юнита этот интерфейс. А человек который работает над взаимодействием, работает непосредственно с интерфейсом.

Так же, в некоторых языках (Java), с помощью интерфейсов строятся ивенты (обработчики событий) или же как их называю callback функции.

Вот так вот!

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

Это наверное все, что можно сказать про интерфейсы!

ПС: Если есть вопросы - задавайте!

Комментариев нет:

Отправить комментарий