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

Вы спросите у меня: как это все может быть связано с темой этой записи? - об этом я и собираюсь Вам поведать.

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

Три основных компонента

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

  • наследование
  • инкапсуляция
  • полиморфизм

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

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

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

class Human
{
  public int height; // рост
  public int age; // возраст
  public String gender; // пол
  public String eyesColor; // цвет глаз
  public String hairColor; // цвет волос
}

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

class Human
{
  public int height; // рост
  public int age; // возраст
  public String gender; // пол
  public String eyesColor; // цвет глаз
  public String hairColor; // цвет волос
  public void walk()
  {
    System.out.println("Я иду!");
  }
  public void work()
  {
    System.out.println("Я работаю в большом офисном здании");
  }
}

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

Наследование

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

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

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

class Director
{
  public int height; // рост
  public int age; // возраст
  public String gender; // пол
  public String eyesColor; // цвет глаз
  public String hairColor; // цвет глаз
  public void walk()
  {
    System.out.println("Я иду!");
  }
  public void work()
  {
    System.out.println("Я работаю в большом офисном здании");
  }
  public void idle()
  {
    System.out.println("Я ничего не делаю!");
  }
}

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

class Director extends Human
{
  public void idle()
  {
    System.out.println("Я ничего не делаю!");
  }
}

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

Полиморфизм

Это слово пришло к нам из греческого языка, понимания этого термина легко достичь, просто переведя его на русский язык: πολύμορφος - многоформенность. То есть в наиболее простом случае подразумевается использование одной и той же переменной (или массива) для хранения информации об объектах, описываемых разными классами. Представим, что нам необходим стандартизованный способ узнать кем же работает тот или иной человек, естественно для этого необходимо описание соответствующего метода для выполнения этой функции в каждом классе, причем он должен одинаково называться в каждом из них. Для реализации этого примера на языке Java нет необходимости использовать дополнительных ключевых слов (в отличие от, например, C#, где необходимо использование слова virtual в заголовке метода в базовом классе и override - в производных). Продолжая приводить примеры на Java имеем три производных класса (для упрощения опустим дополнительные методы, которые могли бы присутствовать):

class Director extends Human
{
  public void work()
  {
    System.out.println("Я работаю директором!");
  }
}

class Programmer extends Human
{
  public void work()
  {
    System.out.println("Я работаю программистом!");
  }
}

class Manager extends Human
{
  public void work()
  {
    System.out.println("Я работаю менеджером!");
  }
}

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

class AskHuman
{
  public static void main(String[] args)
  {
    public Human person;
    person = new Director();
    person.work();
    person = new Manager();
    person.work();
    person = new Programmer();
    person.work();
}

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

Я работаю директором! Я работаю менеджером! Я работаю программистом!

Этот механизм расширяет возможности использования классов, позволяя более гибко использовать переменные и методы (да, этот механизм справедлив и для методов), а также позволяет писать более абстрактные программы и существенно упрощает работу программ, имеющих модульную структуру.

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

interface Worker
{
  public void work();
}

class Director extends Human implements Worker
{
  public void work()
  {
    System.out.println("Я работаю директором!");
  }
}

class Programmer extends Human implements Worker
{
  public void work()
  {
    System.out.println("Я работаю программистом!");
  }
}

class Manager extends Human implements Worker
{
  public void work()
  {
    System.out.println("Я работаю менеджером!");
  }
}

В этом случае производные классы будут обязаны иметь метод work, что даст гарантию классу AskHuman, что он не вызовет несуществующий метод.

Инкапсуляция

Наверняка Вы уже задавались вопросом о том, что же значит слово public во всех предыдущих примерах. Это ключевое слово является частью реализации механизма инкапсуляции в языке Java, суть его состоит в том, чтобы дать возможность определить область видимости для составных частей класса, это очень актуально при написании ПО, использующего библиотеки, plug-in'ы или при написании программы группой людей. Ведь если Ваш класс подразумевает какие-либо ограничения для переменных или методов (например - возраст не может быть отрицательным), то их легко обойти воспользовавшись прямым доступом к ним из-за пределов класса или просто выполнив наследование.

Для предотвращения этого используется система параметров, назначаемых переменным и методам внутри класса для присвоения им "уровней доступа" (на примере опять же Java, но в большинстве известных мне высокоуровневых языков используется та же система):

  • public - назначается по-умолчанию - полностью свободный доступ
  • private - доступ предоставляется только другим компонентам класса
  • protected - доступ предоставляется остальным компонентам класса, а также всем наследникам данного класса

Данный механизм является незаменимым помощником разработчиков любых более-менее крупных проектов, следующих принципам ООП.

Вместо заключения

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

05 января 2008 |  Иван Блинков  |  Теория