Объектно-ориентированный PHP: работа с наследованием

Добро пожаловать в третий урок из серии, посвященной ООП. Если вы не проходили первые два урока, то возможно, захотите с ними ознакомиться, так как данный урок базируется на них:

В этом уроке мы поговорим о наследовании в ООП и о том, как оно работает в PHP. С помощью наследования можно сделать классы намного сильнее и гибче, а также сэкономить уйму времени на написание скриптов.

Мы рассмотрим следующее:

  • Концепцию наследования, и почему его полезно использовать;
  • Как один PHP класс может наследоваться от другого;
  • Как один из “детей” класса может перегружать функциональность методов своего “родителя”;
  • Работа с методами и классами final;
  • Использование абстрактных классов;
  • Работа с интерфейсами.

Готовы? Тогда вперед!

Как осуществляется наследование?

Наследование базируется на понятиях класс-родитель и класс-наследник. Используя определенный синтаксис, вы можете создать класс, который будет наследовать другой класс (становится его наследником).

На заметку: классы-родители также называют базовыми классами или супер-классами. Классы-наследники, в свою очередь, можно назвать дочерними классами или подклассами.

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

К примеру, в веб-приложении форума есть класс Member, у которого есть методы createPost(), editProfile(), showProfile() и др. Так как администраторы форума также являются его членами, то вы можете создать класс Administrator - дочерний класса Member. Класс Administrator наследует все поля и методы класса Member, а значит, объект класса Administrator будет вести себя точно так же, как объект Member.

Вы можете расширить функциональность класса Administrator, добавив в него такие методы, как createForum(), deleteForm() и banMember(). А если хотите назначать роли еще и администраторам, то добавьте в данный дочерний класс поле, например, $adminLevel.

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

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

Создание дочерних классов

Итак, как же создать класс, который станет наследником другого класса, в PHP? Для этого существует ключевое слово extends:

class ParentClass {
  // описание полей и методов
}

class ChildClass extends ParentClass {
  // описание дополнительных полей и методов
}

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

А теперь - пример. Создадим класс Member для воображаемого веб-форума, а затем класс Administrator - дочерний от Member:

class Member {
 
  public $username = "";
  private $loggedIn = false;
 
  public function login() {
    $this->loggedIn = true;
  }
 
  public function logout() {
    $this->loggedIn = false;
  }
 
  public function isLoggedIn() {
    return $this->loggedIn;
  }
}
 
class Administrator extends Member {
 
  public function createForum( $forumName ) {
    echo "$this->username created a new forum: $forumName<br>";
  }
 
  public function banMember( $member ) {
    echo "$this->username banned the member: $member->username<br>";
  }
 
}

Как видите, класс Member содержит поле public $username и поле private $loggedIn, а также методы для входа-выхода из форума и метод для определения того, зашел ли пользователь или нет.

Затем мы добавили класс-наследника Administrator. Он наследует все поля и методы класса Member. Мы также добавили в него дополнительные методы:

  • createForum( $forumName ) для создания нового форума с названием $forumName;
  • banMember( $member ) для бана пользователя $member.

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

Давайте посмотрим на наши классы в действии. Создадим по одному объекту обоих классов, а затем вызовем некоторые из их методов:

// создаем участника форума и логиним его
$member = new Member();
$member->username = "Fred";
$member->login();
echo $member->username . " is " . ( $member->isLoggedIn() ? "logged in" : "logged out" ) . "<br>";
 
// создаем администратора и логиним его
$admin = new Administrator();
$admin->username = "Mary";
$admin->login();
echo $admin->username . " is " . ( $member->isLoggedIn() ? "logged in" : "logged out" ) . "<br>";
 
// отобразит "Mary created a new forum: Teddy Bears"
$admin->createForum( "Teddy Bears" );
 
// отобразит "Mary banned the member: Fred"
$admin->banMember( $member );

На странице отобразится:

Fred is logged in
Mary is logged in
Mary created a new forum: Teddy Bears
Mary banned the member: Fred

Вот, как это работает:

  1. Сначала мы создали объект класса Member, задали имя пользователя “Fred”, залогинили его и отобразили на странице сообщение о том, что он вошел на форум.
  2. Затем создаем объект класса Administrator. Так как данный класс наследуется от Member, мы можем пользоваться всеми методами и полями этого класса для объектов класса Administrator. Мы даем имя администратору - Mary - и логиним ее, после чего отображаем сообщение о том, что она вошла.
  3. Теперь вызываем метод класса Administrator createForum(), передав в него название форума - “Teddy Bears”.
  4. Наконец, вызываем метод banMember() от объекта - админа, передав имя пользователя Fred.

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

Перегрузка родительских методов

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

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

Перегрузкой метода login() в дочернем классе, вы можете изменить данный метод по своему усмотрению.

Чтобы перегрузить метод супер-класса в дочернем классе, просто создайте в нем метод с таким же названием. Тогда при вызове метода для объектов дочернего класса, будет вызываться именно перегруженный метод, а не метод супер-класса:

class ParentClass {
  public function myMethod() {
    // (действия)
  }
}

class ChildClass extends ParentClass {
  public function myMethod() {
    // вызывется для объекта класса ChildClass
    // вместо метода супер-класса MyMethod()
  }
}

Давайте перегрузим метод login() для класса Administrator так, чтобы в файл записывались логи:

class Member {
 
  public $username = "";
  private $loggedIn = false;
 
  public function login() {
    $this->loggedIn = true;
  }
   
  public function logout() {
    $this->loggedIn = false;
  }
}
 
class Administrator extends Member {
 
  public function login() {
    $this->loggedIn = true;
    echo "Log entry: $this->username logged in<br>";
  }
 
}
 
// создаем нового пользователя и логиним его
$member = new Member();
$member->username = "Fred";
$member->login();
$member->logout();
 
// создаем нового администратора и логиним его
$admin = new Administrator();
$admin->username = "Mary";
$admin->login();         // отобразит "Log entry: Mary logged in"
$admin->logout();

Как видите, мы перегрузили метод login() класса Administrator, чтобы он отображал сообщения, как в файлах - логах.

Затем мы создали обычного пользователя (Fred) и администратора (Mary). При вызове метода login() от Фреда вызывается метод Member::login(). А когда мы вызываем метод от администратора Mary, вызовется метод Administrator::login(), так как PHP видит, что мы перегрузили этот метод для данного класса. На странице отобразится строка "Log entry: Mary logged in".

С другой стороны, мы не перегрузили метод logout() в дочернем классе, поэтому Member:logout() вызывается и для админов и для обычных пользователей.

Вызов метода супер-класса из дочернего класса

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

В примере, приведенном в прошлом параграфе, мы перегрузили метод login(). Но мы продублировали часть метода Member::login() в Administrator::login():

class Administrator extends Member {
 
  public function login() {
    $this->loggedIn = true;
    echo "Log entry: $this->username logged in<br>";
  }
}

Вместо того, чтобы дублировать код, лучше вызвать метод Member::login() из Administrator::login().

Чтобы получить доступ к методу супер-класса из класса дочернего, воспользуйтесь ключевым словом parent:

parent::myMethod();

Давайте теперь перепишем метод login() в дочернем классе так, чтобы из него вызывался тот же метод из класса-родителя, а затем добавим в него что-то новое:

class Administrator extends Member {
 
  public function login() {
    parent::login();
    echo "Log entry: $this->username logged in<br>";
  }
}

Это не только сокращает код, но также обеспечивает легкость его будущих корректировок. Если вам позже понадобится изменить способ, по которому логинится любой пользователь, вам потребуется подкорректировать метод login() только в классе Member, и в классе Administrator будет вызываться уже измененный метод.

Предотвращение наследования с помощью методов и классов final

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

Однако бывает такое, что перегрузка методов супер-классов может привести к неполадкам, снизить безопасность, усложнить код. В этих случаях, было бы полезно запретить наследование метода или даже всего класса.

Чтобы запретить дочерним классам перегружать методы супер-класса, добавьте перед его описанием ключевое слово final. Например, вы можете запретить перегрузку метода login() класса Member по причинам усиления безопасности:

class Member {
 
  public $username = "";
  private $loggedIn = false;
 
  public final function login() {
    $this->loggedIn = true;
  }
 
  public function logout() {
    $this->loggedIn = false;
  }
 
  public function isLoggedIn() {
    return $this->loggedIn;
  }
}

Если кто-то попытается наследовать класс и перегрузить данный метод:

class NaughtyMember extends Member {
 
  public function login() {
    $this->loggedIn = true;
    // сделать что-то плохое
  }
}

… PHP выведет сообщение об ошибке:

Fatal error: Cannot override final method Member::login()

Вы можете также запретить наследование от всего класса:

final class Member {
  // от этого класса нельзя вообще наследовать
}

Любая попытка создать дочерний класс данного класса, не увенчается успехом:

Fatal error: Class NaughtyMember may not inherit from final class (Member)

На заметку: несмотря на то что это больше касается Java, нежели PHP, данная статья приводит преимущества использования методов и классов final.

Работа с абстрактными классами

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

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

На заметку: как только вы создали хотя бы один абстрактный метод в классе, вы должны объявить этот класс как абстрактный.

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

На заметку: вы можете добавлять в абстрактный класс и не абстрактные методы. Они будут обыкновенным образом наследоваться дочерними классами.

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

abstract class Person {
 
  private $firstName = "";
  private $lastName = "";
 
  public function setName( $firstName, $lastName ) {
    $this->firstName = $firstName;
    $this->lastName = $lastName;
  }
 
  public function getName() {
   return "$this->firstName $this->lastName";
  }
   
  abstract public function showWelcomeMessage();
}

Как видите, мы создали абстрактный класс, добавив в его описание ключевое слово abstract. В этом классе есть несколько свойств, общих для всех людей, - $frstName и $lastName - а также методы для инициализации и чтения значений этих полей.

В классе также есть абстрактный метод showWelcomeMessage(). Этот метод выводит приветствие, когда пользователь входит на сайт. Опять же, мы добавляем ключевое слово abstract в описание данного метода, чтобы сделать его абстрактным. Так как он абстрактный, в нем нет ни строчки кода, это просто его объявление. Тем не менее, любой дочерний класс обязан добавить и описать метод showWelcomeMessage().

Теперь давайте создадим пару классов от абстрактного класса Person:

  1. класс Member для участников форума;
  2. класс Shopper для покупателей онлайн-магазина.
class Member extends Person {
 
  public function showWelcomeMessage() {
    echo "Hi " . $this->getName() . ", welcome to the forums!<br>";
  }
 
  public function newTopic( $subject ) {
    echo "Creating new topic: $subject<br>";
  }
}
 
class Shopper extends Person {
 
  public function showWelcomeMessage() {
    echo "Hi " . $this->getName() . ", welcome to our online store!<br>";
  }
 
  public function addToCart( $item ) {
    echo "Adding $item to cart<br>";
  }
}

Как видите, каждый из них описывает метод showWelcomeMessage() из абстрактного супер-класса. Они имплементированы по-разному: в классе Member отображается сообщение "welcome to the forums", а в классе Shopper - "welcome to our online store", но это нормально. Главное то, что они оба описали данный метод.

Если бы один из них, например, Shopper, не описал метод, PHP выдал бы ошибку:

Class Shopper contains 1 abstract method and must therefore be declared abstract
or implement the remaining methods (Person::showWelcomeMessage)

Наряду с имплементацией абстрактного метода, в каждом классе есть свои обычные методы. В Member есть метод newTopic() для создания новой темы в форуме, а в Shopper - метод addToCart() для добавления товаров в корзину.

Теперь мы можем создавать участников форума и покупателей на нашем сайте. Мы можем вызывать методы newTopic() и addToCart() от этих объектов, а также getName() и setName(), так как они наследуются от супер-класса Person.

Более того, зная, что классы Member и Shopper наследуются от Person, мы можем спокойно вызывать метод showWelcomeMessage() для обоих классов, так как он точно реализован и в том и в другом. Мы в этом уверены, так как знаем, что он был объявлен как абстрактный метод в классе Person.

Вот пример:

$aMember = new Member();
$aMember->setName( "John", "Smith" );
$aMember->showWelcomeMessage();
$aMember->newTopic( "Teddy bears are great" );
 
$aShopper = new Shopper();
$aShopper->setName( "Mary", "Jones" );
$aShopper->showWelcomeMessage();
$aShopper->addToCart( "Ornate Table Lamp" );

На странице отобразится:

Hi John Smith, welcome to the forums!
Creating new topic: Teddy bears are great
Hi Mary Jones, welcome to our online store!
Adding Ornate Table Lamp to cart

Создание и использование интерфейсов

Интерфейсы во многом схожи с абстрактными классами. Интерфейс - это шаблон, который задает поведение одного или более классов.

Вот основные отличия между интерфейсами и абстрактными классами:

  1. Ни один метод не может быть описан в интерфейсе. Они все абстрактны. В абстрактном классе могут быть и не абстрактные методы.
  2. Интерфейс не может содержать полей - только методы.
  3. Класс имплементирует интерфейс, и класс наследует или расширяет другой класс.
  4. Класс может имплементировать несколько интерфейсов одновременно. Этот же класс может наследовать другой класс. Но у дочернего класса может быть только один супер-класс (абстрактный или нет).

Как и абстрактный класс, интерфейс объявляет несколько методов, которые должны быть реализованы в любом классе, который имплементирует данный интерфейс. Синтаксис выглядит так:

interface MyInterface {
  public function aMethod();
  public function anotherMethod();
}

Чтобы создать класс, который имплементирует тот или иной интерфейс, напишите так:

class MyClass implements MyInterface {

  public function aMethod() {
    // (имплементация метода)
  }

  public function anotherMethod() {
    // (имплементация метода)
  }

}

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

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

Тем не менее, давайте предположим, что нам нужно будет доставать их и записывать в базу данных как объекты класса Member, так и объекты Topic. Для этого мы создадим интерфейс Persistable, в котором будут методы для сохранения объектов в БД и извлечения их оттуда:

interface Persistable {
 
  public function save();
  public function load();
  public function delete();
 
}

Теперь давайте создадим класс Member и имплементируем для него интерфейс Persistable. Это значит, что в интерфейсе должны быть методы save(), load() и delete():

class Member implements Persistable {
  
  private $username;
  private $location;
  private $homepage;
  
  public function __construct( $username, $location, $homepage ) {
    $this->username = $username;
    $this->location = $location;
    $this->homepage = $homepage;
  }
    
  public function getUsername() {
    return $this->username;
  }
 
  public function getLocation() {
    return $this->location;
  }
 
  public function getHomepage() {
    return $this->homepage;
  }
 
  public function save() {
    echo "Saving member to database<br>";
  }
 
  public function load() {
    echo "Loading member from database<br>";
  }
 
  public function delete () {
    echo "Deleting member from database<br>";
  }
 
}

Наш класс Topic также будет имплементировать данный интерфейс, поэтому в нем тоже должны быть эти три метода:

class Topic implements Persistable {
  
  private $subject;
  private $author;
  private $createdTime;
  
  public function __construct( $subject, $author ) {
    $this->subject = $subject;
    $this->author = $author;
    $this->createdTime = time();
  }
    
  public function showHeader() {
    $createdTimeString = date( 'l jS M Y h:i:s A', $this->createdTime );
    $authorName = $this->author->getUsername();
    echo "$this->subject (created on $createdTimeString by $authorName)<br>";
  }
 
  public function save() {
    echo "Saving topic to database<br>";
  }
 
  public function load() {
    echo "Loading topic from database<br>";
  }
 
  public function delete () {
    echo "Deleting topic from database<br>";
  }
 
}

На заметку: так как у нас форум - воображаемый, вместо взаимодействия с базой данных в методах save(), load() и delete() просто выводятся сообщения.

Мы также добавили несколько дополнительных методов, например, Member::getUsername() для получения имени пользователя, и Topic::showHeader() для отображения названия темы, имени автора и времени создания темы.

Теперь можем создать объекты классов Member и Topic, а затем вызвать их методы getUsername() и showHeader(). Более того, зная, что эти классы имплементируют интерфейс Persistable, мы можем вызывать такие методы, как save(), load() или delete():

$aMember = new Member( "fred", "Chicago", "http://example.com/" );
echo $aMember->getUsername() . " lives in " . $aMember->getLocation() ."<br>";
$aMember->save();
 
$aTopic = new Topic( "Teddy Bears are Great", $aMember );
$aTopic->showHeader();
$aTopic->save();

На странице отобразится:

fred lives in Chicago
Saving member to database
Teddy Bears are Great (created on Wednesday 25th May 2011 02:19:14 AM by fred)
Saving topic to database

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

class MyClass implements anInterface, anotherInterface {
  ...
}

На заметку: интерфейсы - это мощное свойство ООП, и о них можно еще много чего сказать. Узнать о них больше можно в PHP документации.

Заключение

В этом уроке вы ознакомились с одним из самых мощных свойств ООП - наследованием. Вы узнали:

  1. Как работает наследование, и как его использовать для расширения классов;
  2. Как создавать дочерние классы в PHP;
  3. Почему вам может понадобиться перегружать методы в дочерних классах;
  4. Как получить доступ к методам супер-класса;
  5. Всё о методах и классах final, и почему полезно их использовать;
  6. Концепцию абстрактных классов для создания шаблонов дочерних классов;
  7. Как использовать интерфейсы, чтобы задать общую функциональность несвзянным между собой классам.

Если вы прошли все уроки из данной серии, то вы уже сможете писать сложные приложения на ООП. Поздравляю!

В следующем и последнем уроке я покажу вам супер-полезные ООП свойства, которые есть в PHP.

А пока удачного кодирования!

Данный урок подготовлен для вас командой сайта ruseller.com
Источник урока: www.elated.com/articles/object-oriented-php-working-with-inheritance/
Перевел: Станислав Протасевич
Урок создан: 30 Июня 2011
Просмотров: 77798
Правила перепечатки


5 последних уроков рубрики "PHP"

  • Фильтрация данных с помощью zend-filter

    Когда речь идёт о безопасности веб-сайта, то фраза "фильтруйте всё, экранируйте всё" всегда будет актуальна. Сегодня поговорим о фильтрации данных.

  • Контекстное экранирование с помощью zend-escaper

    Обеспечение безопасности веб-сайта — это не только защита от SQL инъекций, но и протекция от межсайтового скриптинга (XSS), межсайтовой подделки запросов (CSRF) и от других видов атак. В частности, вам нужно очень осторожно подходить к формированию HTML, CSS и JavaScript кода.

  • Подключение Zend модулей к Expressive

    Expressive 2 поддерживает возможность подключения других ZF компонент по специальной схеме. Не всем нравится данное решение. В этой статье мы расскажем как улучшили процесс подключение нескольких модулей.

  • Совет: отправка информации в Google Analytics через API

    Предположим, что вам необходимо отправить какую-то информацию в Google Analytics из серверного скрипта. Как это сделать. Ответ в этой заметке.

  • Подборка PHP песочниц

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

или авторизуйтесь, чтобы добавлять комментарии, оценивать уроки и сохранять их в личном кабинете
  • 27 Сентября 2013 05:40
    valerol
    Мне осталось непонятно, в чём преимущество использования интерфейсов? Зачем их создавать, если все равно в имплементирующем классе каждый метод нужно описывать?
    • 2 Января 2016 20:19
      w3lifer
      Чтобы не забыть их описать при написании кода и знать что они есть при использовании кода.
  • 2 Января 2016 20:34
    w3lifer
    Сверху вниз ...
    Мы создали класс ParentClass, а затем класса ChildClass, который наследуется от ParentClass.
    Ошибка: «класс», а не «класса».
    На заметку: несмотря на то что это больше касается Java, нежели PHP, данная статья приводит преимущества использования методов и классов final.
    Битая ссылка: http://renaud.waldura.com/doc/java/final-keyword.shtml. Далее треш!..
    Вот основные отличия между интерфейсами и абстрактными классами:
    Ни один метод не может быть описан в интерфейсе. Они все абстрактны. В абстрактном классе могут быть и не абстрактные методы.
    Так правильней и логичней:
    Ни один метод не может быть описан в интерфейсе — они все абстрактны. В абстрактном классе могут быть и не абстрактные методы.
    Класс имплементирует интерфейс, и класс наследует или расширяет другой класс.
    Нужно же уточнять, что к чему относится, а то можно с ума сойти:
    Классы имплементируют интерфейсы; НО классы наследуют или расширяют абстрактные классы.
    Это вообще вынос мозга:
    Класс может имплементировать несколько интерфейсов одновременно. Этот же класс может наследовать другой класс. Но у дочернего класса может быть только один супер-класс (абстрактный или нет).
    Нужно так:
    Класс может имплементировать несколько интерфейсов одновременно; причём этот же класс может наследовать другой класс. Однако в общем наследовать класс может только один класс — абстрактный или нет.
    Так и не рассмотрели конкретно почему полезно использовать final. Ссылка на источник битая. И всё это только читая наискосок :). P. S. «Суперская» система комметирования ...
  • 30 Марта 2016 15:36
    FrodoRing
    Для контингента данного сайта - интефейсы это через чур. По Wordpress тут писать самое то, но не более.
  • Комментарий удален
    • 1 Июля 2011 08:54
      budzin
      Читал :)
    • 1 Июля 2011 08:58
      <:Golgi:>
      спасибо, посмотрим
    • 1 Июля 2011 10:54
      IKLO
      Спасибо за книгу, почитаю на досуге, может что новое подчеркну :)
  • Комментарий удален
    • 2 Июля 2011 12:17
      rubyx
      Ты верующий индии или любитель Гоа музыки? глядя на аву. :)
^ Наверх ^