Парадигмы объектно ориентированного программирования на примере Java

 

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

 

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

 

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

В качестве элементарного примера приведу класс ExampleFactory, содержащий в себе один метод, создающий "продукт" из полей:

package org.test.technerium.incapsulation;

public class ExampleFactory {
    public int component1 = 6; // Сырье №1
    public int component2 = 13; // Сырье №2
    
    //Ниже представлен единственный метод нашей фабрики
    //Он собирает из исходного сырья некий продукт и отдает его
    public int getProduct(){
        System.out.println("Factory component #1: " + component1); //Напишем в консоль,
                        что из себя представляет первое поле

        System.out.println("Factory component #2: " + component2); //и второе
        int product = component1*component2; // Вычислим результат
        return product; // Отдаем результат
    }
}

Далее пишем простой класс, в котором нам нужен только метод main, чтобы создать объект фабрики и запустить наш "конвейер" ( smiley):

package org.test.technerium.incapsulation;

public class RunTestIncapsulation {
    // При запуске класса этот метод автоматически выполняется.
    public static void main(String[] args){
        ExampleFactory factory = new ExampleFactory(); // Создали объект фабрики
        int product = factory.getProduct(); // Завели новую переменную и сразу присвоили ей значение. При этом у фабрики был запушен метод getProduct()
        System.out.println("Factory product: " + product); // Фабрика уже должна была при запуске своего метода вывести на экран значения своих полей, выведем теперь и результат
    }
}

Если всё сделано правильно, то в консоли получим следующее:

Factory component #1: 6
Factory component #2: 13
Factory product: 78

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

 

System.out.println("Direct access to a field component1: " + factory.component1);

 

Но далеко не все поля и методы должны быть видимы снаружи. Многие из них используются только внутри объекта и потому их лучше скрыть. Объект скрывает приватные (private) поля и методы от внешнего доступа.

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

 

package org.test.technerium.incapsulation;

public class ExampleFactory {
    public int component1 = 6; // Сырье №1
    public int component2 = 13; // Сырье №2
    
    //Ниже представлен единственный метод нашей фабрики
    //Он собирает из исходного сырья некий продукт и отдает его
    public int getProduct(){
        System.out.println("Factory component #1: " + component1); //Напишем в консоль что из себя представляет первое поле
        System.out.println("Factory component #2: " + component2); //и второе
        int component3 = getComponent3();// Вызвали приватный метод
        System.out.println("Factory component #3: " + component3); //третий не лишний
        int product = component1*component2*component3; // Вычислим результат
        return product; // Отдаем результат
    }
    //Этот метод приватный и доступен только внутри этого класса.
    private int getComponent3(){
        return 12345;
    }
}

То есть снаружи мы не знаем, какие приватные методы и поля есть у объекта. Это удобно.

Таким образом, инкапсуляция позволяет рассматривать объекты в качестве "черных ящиков" с жестко заданным набором внешне доступных методов (этот набор называется интерфейсом) и полей, при этом то, как реализованы эти методы, снаружи неизвестно и не важно.

Вообще же объект - это элементарная единица ООП.

 

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

 

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

Посмотрим как работает наследование в Java. Чтобы в класс B наследовать от класса A, пишут public class B extends A{...}. Создаем родительский класс, описывающий некую фабрику, производящую грузовики

package org.test.technerium.inheritance;

public class TruckFactory {    
    // Этот метод будет доступен дочерним классам
    public void produceTruck(){
        System.out.println("Factory created a truck"); //Выведем что-нибудь в консоль
    }
}

Дальше унаследуем методы (точнее один метод) и поля (в нашем примере их нет) родительского класса и добавим к ним свой метод в дочернем классе TruckAndTrailerFactory:

package org.test.technerium.inheritance;

public class TruckAndTrailerFactory extends TruckFactory{
    // Этот метод символизирует производство прицепов
    public void produceTrailer(){
        System.out.println("Child factory created a trailer");
    }
}

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

package org.test.technerium.inheritance;

public class RunTestInheritance {
    public static void main(String[] args){
        TruckAndTrailerFactory childFactory = new TruckAndTrailerFactory(); //Создаем объект дочернего класса
        System.out.println("Access to parent's method: "); 
        childFactory.produceTruck(); // И нам доступен метод родительского класса
        System.out.println("Access to child's method: ");
        childFactory.produceTrailer(); // А также нашего дочернего
        
    }
}

При запуске RunTestInheritance увидим в консоли

Access to parent's method:
Factory created a truck
Access to child's method:
Child factory created a trailer

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

 

Полиморфизм

 

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

Для того, чтобы увидеть этот механизм в действии, создадим новый родительский класс Factory, реализующий метод getProduct():

package org.test.technerium.polymorphism;

public class Factory {
    public String getProduct(){
        return "Base product"; // Вот так у нас реализован этот метод в базовом классе
    }
}

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

Первый класс у нас будет "производить" тягачи.

package org.test.technerium.polymorphism;

public class TruckFactory extends Factory{
    public String getProduct(){
        return "Truck"; // Дочерний класс реализует логику иначе.
    }
}

А второй - прицепы.

package org.test.technerium.polymorphism;

public class TrailerFactory extends Factory{
    public String getProduct(){
        return "Trailer"; // Дочерний класс реализует логику иначе. Совсем иначе.
    }
}

Далее - пишем класс для запуска тестов:

public class RunTestPolymorphism {
    public static void main(String[] args){
        Factory factory = new TruckFactory();
        System.out.println("Created Truck factory, product: " + factory.getProduct());
        
        factory = new TrailerFactory();
        System.out.println("Created Trailer factory, product: " + factory.getProduct());
        
        factory = new Factory();
        System.out.println("Created Base factory, product: " + factory.getProduct());
    }
}

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

В консоли мы увидим

Created Truck factory, product: Truck
Created Trailer factory, product: Trailer
Created Base factory, product: Base product

 

А что если мы не знаем, как именно должен быть реализован тот или иной метод родительского класса? Понятно, что в таком случае мы вообще не планируем создавать объекты этого класса, только объекты дочерних классов. Java предоставляет нам возможность вообще не определять логику метода класса. Такой класс становится абстрактным, а его нереализованные методы - абстрактными, абстрактные классы не позволяют создавать объекты. Если мы наследуем абстрактный класс, то должны написать реализацию абстрактных методов, иначе дочерний класс также будет абстрактным. Для демонстрации использования абстрактных классов, напишем новый, абстрактный родительский класс AFactory:

package org.test.technerium.polymorphism;

public abstract class AFactory { // модификатор abstract указывает на абстрактность класса
    public abstract String getProduct(); // этот метод не содержит тела, поскольку он абстрактен
}

а также новый дочерний класс FactoryImpl:

package org.test.technerium.polymorphism;

public class FactoryImpl extends AFactory{
    
    public String getProduct() {
        return "implemented Product"; // собственно, реализация метода
    }
}

Добавим пару строк в наш предыдущий класс для запуска теста:

package org.test.technerium.polymorphism;

public class RunTestPolymorphism {
    public static void main(String[] args){
        Factory factory = new TruckFactory();
        System.out.println("Created Truck factory, product: " + factory.getProduct());
        
        factory = new TrailerFactory();
        System.out.println("Created Trailer factory, product: " + factory.getProduct());
        
        factory = new Factory();
        System.out.println("Created Base factory, product: " + factory.getProduct());
        
        AFactory realFactory = new FactoryImpl(); // создаем новый объект базового, абстрактного класса
        System.out.println("Using implemented method: " + realFactory.getProduct()); // используем конструктор класса, реализующего методы абстрактного родителя
    }
}

Запускаем тест и, помимо уже виденных выше строк, видим результат работы реализованного метода:

Created Truck factory, product: Truck
Created Trailer factory, product: Trailer
Created Base factory, product: Base product
Using implemented method: implemented Product

 

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

package org.test.technerium.polymorphism;

public interface FactoryInterface {
    public String getProduct();
}

Добавляем класс, реализующий этот интерфейс:

package org.test.technerium.polymorphism;

public class FactoryImpl2 implements FactoryInterface{

    public String getProduct() {
        return "implemented method from interface";
    }

}

Думаю, здесь всё понятно. Добавляем еще пару строк в код, запускающий тест:

package org.test.technerium.polymorphism;

public class RunTestPolymorphism {
    public static void main(String[] args){
        Factory factory = new TruckFactory();
        System.out.println("Created Truck factory, product: " + factory.getProduct());
        
        factory = new TrailerFactory();
        System.out.println("Created Trailer factory, product: " + factory.getProduct());
        
        factory = new Factory();
        System.out.println("Created Base factory, product: " + factory.getProduct());
        
        AFactory realFactory = new FactoryImpl();
        System.out.println("Using implemented method: " + realFactory.getProduct());
        
        FactoryInterface yetAnotherRealFactory = new FactoryImpl2();
        System.out.println("Interface implementation: " + yetAnotherRealFactory.getProduct());
    }
}

После запуска теста имеем на выходе "выхлоп" нашего реализованного метода:

 

Created Truck factory, product: Truck
Created Trailer factory, product: Trailer
Created Base factory, product: Base product
Using implemented method: implemented Product
Interface implementation: implemented method from interface // вот это

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

 

 

P. S. На десерт: абстракция

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

Заключение

Все приведенные примеры вы можете скачать по ссылке ниже, в архиве проекта для Eclipse.

TechNeriumTestProject.zip

Была ли статья полезна: 

Комментарии

Спасибо за статью, все ОЧЕНЬ понятно. Но есть вопрос, что, если при определении объекта использовать тип не базового класса, а класса чью реализацию хотим использовать, т.е. наследованного от базового?

Спасибо за комментарий)) Рад, что статья была понятна.

 

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

 

Есть нюанс: если у нас в родительском классе определен метод method1() а в дочернем классе метод method2(),  то при использовании базового типа мы не можем обратиться напрямую к методу потомка (method2()), компилятор выдаст ошибку что такой метод не найден в интерфейсе. Для компиляции кода с вызовом такого метода, нам придется привести тип объекта к дочернему классу (то есть cast object to class на английском) чтобы компилятор увидел в интерфейсе метод дочернего класса. Либо изначально использовать класс наследник как тип создаваемого объекта.

Спасибо! Сначала все стало еще более запутанно, но я взяла в руки ручку и google и вроде прояснилось))

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

Добавить комментарий

HTML

  • Разрешённые HTML-теги: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <pre> <p>
  • Строки и параграфы переносятся автоматически.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Поместите примеры вашего исходного кода в теги <code>...</code> or <source>...</source> и он будет красиво отформатирован.

Plain text

  • Поместите примеры вашего исходного кода в теги <code>...</code> or <source>...</source> и он будет красиво отформатирован.
  • Строки и параграфы переносятся автоматически.
  • Разрешённые HTML-теги: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <code> <source>
CAPTCHA
Пожалуйста, подтвердите, что вы человек.