Объектно-ориентированное программирование(ООП) относится к методологии программирования, основанной на объектах, а не только на функциях и процедурах, как в функциональном программировании. Эти объекты могут содержать данные(атрибут) и методы(поведение), как и реальные сущности, которые мы моделируем в наших приложениях.
Этот урок научит нас четырем основным принципам – абстракция, инкапсуляция, наследование и полиморфизм. Они также известны как четыре столпа парадигмы объектно-ориентированного программирования.
1. Что такое ООП или объектно-ориентированное программирование?
В ранние дни люди писали программы с помощью двоичного кода и использовали механические переключатели для загрузки программ. Позже, по мере развития аппаратных возможностей, эксперты пытались упростить программирование, используя языки высокого уровня, где мы использовали компиляторы для генерации машинных инструкций из программы.
С дальнейшей эволюцией специалисты создали структурное программирование на основе небольших функций. Эти функции помогли во многих отношениях, например, повторное использование кода, локальные переменные, отладка кода и поддержка кода.
С развитием вычислительной техники и спросом на более сложные приложения ограничения структурного программирования стали очевидны. Сложные приложения должны были быть более тесно смоделированы с реальным миром и вариантами использования.
Позже специалисты разработали объектно-ориентированное программирование. В основе ООП лежат объекты и классы. Как и реальная сущность, объект имеет две существенные характеристики:
- данные – рассказывают об атрибутах и состоянии объекта
- поведение – дает ему возможность изменять себя и общаться с другими объектами
1.1 Класс и объект
Объект — это экземпляр класса. Каждый объект имеет свое состояние, поведение и идентичность. Класс — это чертеж или шаблон для своих объектов.
Объекты могут общаться с другими объектами, вызывая функции. Иногда это называют передачей сообщений.
Например, если мы работаем над приложением по управлению персоналом, то оно состоит из сущностей/актеров, например, сотрудник, менеджер, отдел, расчетные листки, отпуск, цели, учет рабочего времени и т. д. Чтобы смоделировать эти сущности в компьютерных программах, мы можем создать классы с такими же атрибутами данных и поведением, как в реальной жизни.
Например, сущность «сотрудник» может быть представлена как класс «Сотрудник»:
public class Employee{private long id;private String title;private String firstName;private String middleName;private String lastName;private Date dateOfBirth;private Address mailingAddress;private Address permanentAddress;// More such attributes, getters and setters according to application requirements}
Вышеуказанный Employee действует как шаблон. Мы можем использовать этот класс для создания в приложении столько различных объектов employee, сколько нам нужно.
Employee e = new Employee(111);e.setFirstName("Alex");....int age = e.getAge();
Поле идентификатора помогает хранить и извлекать данные о любом отдельном сотруднике.
Идентификация объекта обычно поддерживается средой выполнения приложения, например, его виртуальной машиной Java(JVM) для приложений Java. Каждый раз, когда мы создаем объект Java, JVM создает хэш-код для этого объекта и назначает его. Таким образом, даже если программист забудет добавить поле id, JVM гарантирует, что все объекты будут уникально идентифицированы.
1.2.Конструктор
Конструкторы — это специальные методы без возвращаемого значения. Их имя всегда совпадает с именем класса, но они могут принимать параметры, которые помогают задать начальное состояние объекта до того, как приложение начнет его использовать.
JVM назначает классу конструктор по умолчанию, если мы не предоставляем никакого конструктора. Этот конструктор по умолчанию не принимает никаких параметров.
Помните, если мы назначаем конструктор любому классу, то JVM не назначает ему конструктор по умолчанию. При необходимости нам нужно явно указать конструктор по умолчанию для класса.
public class Employee{// Default constructorpublic Employee(){}// Custom constructorpublic Employee(int id){this.id = id;}}
2. 4 столпа ООП
Четыре основные особенности объектно-ориентированного программирования:
- Абстракция
- Инкапсуляция
- Наследование
- Полиморфизм

2.1 Абстракция
Абстракцию очень легко понять, если связать ее с примером из реального времени. Например, когда мы едем на машине, нам не нужно беспокоиться о точной внутренней работе машины. Мы обеспокоены взаимодействием с машиной через ее интерфейсы, такие как руль, педаль тормоза, педаль акселератора и т. д. Здесь наши знания об машине абстрактны.
В информатике абстракция — это процесс, посредством которого данные и программы определяются с помощью представления, аналогичного по форме их значению(семантике), при этом скрывая детали реализации.
Проще говоря, абстракция скрывает информацию, не имеющую отношения к контексту, или, скорее, показывает только релевантную информацию и упрощает ее, сравнивая с чем-то подобным в реальном мире.
Абстракция фиксирует только те детали объекта, которые имеют отношение к текущей точке зрения.
Обычно абстракцию можно рассматривать двумя способами:
2.1.1 Абстракция данных
Абстракция данных — это способ создания сложных типов данных из нескольких меньших типов данных, что более близко к реальным сущностям. Например, класс Employee может быть сложным объектом, имеющим различные небольшие ассоциации.
public class Employee{private Department department;private Address address;private Education education;//So on...}
Итак, если вы хотите получить информацию о сотруднике, вы обращаетесь к объекту Employee — как вы это делаете в реальной жизни, обращаетесь к самому человеку.
2.1.2 Управление абстракцией
Абстракция управления достигается путем сокрытия последовательности действий для сложной задачи — внутри простого вызова метода — таким образом, логика выполнения задачи может быть скрыта от клиента и может быть изменена без влияния на клиентский код.
public class EmployeeManager{public Address getPrefferedAddress(Employee e){//Get all addresses from database//Apply logic to determine which address is preferred//Return address}}
В приведенном выше примере, если завтра вы захотите изменить логику так, чтобы каждый раз внутренний адрес был предпочтительным адресом, вы измените логику внутри метода getPrefferedAddress(), и клиент не пострадает.
2.2 Инкапсуляция
Оборачивание данных и методов в классах в сочетании с сокрытием реализации(через контроль доступа) часто называется инкапсуляцией. Результатом является тип данных с характеристиками и поведением.
«Что бы ни изменилось, инкапсулируй это» — известный принцип дизайна
Инкапсуляция по сути представляет собой как сокрытие информации, так и сокрытие реализации.
- Сокрытие информации осуществляется с помощью модификаторов контроля доступа(публичный, закрытый, защищенный), а сокрытие реализации достигается путем создания интерфейса для класса.
- Скрытие реализации позволяет дизайнеру изменять то, как объект выполняет свою обязанность. Это особенно ценно, когда проекты(или даже требования) могут измениться.
Давайте рассмотрим пример, чтобы было понятнее.
2.2.1. Сокрытие информации
class InformationHiding{//Restrict direct access to inward dataprivate ArrayList items = new ArrayList();//Provide a way to access data - internal logic can safely be changed in futurepublic ArrayList getItems(){return items;}}
2.2.2. Сокрытие реализации
interface ImplemenatationHiding {Integer sumAllItems(ArrayList items);}class InformationHiding implements ImplemenatationHiding{//Restrict direct access to inward dataprivate ArrayList items = new ArrayList();//Provide a way to access data - internal logic can safely be changed in futurepublic ArrayList getItems(){return items;}public Integer sumAllItems(ArrayList items) {//Here you may do N number of things in any sequence//Which you do not want your clients to know//You can change the sequence or even whole logic//without affecting the client}}
2.3 Наследование
Наследование — еще одна важная концепция объектно-ориентированного программирования. Наследование — это механизм, с помощью которого один класс приобретает свойства и поведение родительского класса. По сути, это создание родительско-дочерних отношений между классами. В Java мы будем использовать наследование в основном для повторного использования кода и удобства обслуживания.
Ключевое слово «extends» используется для наследования класса в Java. Ключевое слово «extends» указывает, что мы создаем новый класс, который является производным от существующего класса.
В терминологии Java класс, который наследуется, называется суперклассом. Новый класс называется подклассом.
Подкласс наследует все не закрытые члены(поля, методы и вложенные классы) из своего суперкласса. Конструкторы не являются членами, поэтому они не наследуются подклассами, но конструктор суперкласса может быть вызван из подкласса.
2.3.1 Пример наследования
public class Employee{private Department department;private Address address;private Education education;//So on...}public class Manager extends Employee {private List<Employee> reportees;}
В приведенном выше коде Manager является специализированной версией Employee и повторно использует отдел, адрес и образование из класса Employee, а также определяет собственный список подчиненных.
2.3.2 Типы наследования
Одиночное наследование — дочерний класс происходит от одного родительского класса.
class Parent {//code}class Child extends Parent {//code}
Множественное наследование – дочерний элемент может происходить от нескольких родительских элементов. До JDK 1.7 множественное наследование было невозможно в Java с помощью классов. Но начиная с JDK 1.8 множественное наследование возможно с помощью интерфейсов с методами по умолчанию.
interface MyInterface1 {}interface MyInterface2 {}class MyClass implements MyInterface1, MyInterface2 {}
Многоуровневое наследование — относится к наследованию между более чем тремя классами таким образом, что дочерний класс будет выступать в качестве родительского класса для другого дочернего класса.
class A {}class B extends A {}class C extends B {}
Иерархическое наследование относится к такому наследованию, когда существует один суперкласс и более одного подкласса, расширяющего суперкласс.
class A {}class B extends A {}class C extends A {}class D extends A {}
Гибридное наследование – это комбинация двух или более типов наследования. Поэтому, когда связь между классами содержит наследование двух или более типов, то мы говорим, что классы реализуют гибридное наследование.
interface A {}interface B extends A {}class C implements A {}class D extends C impements B {}
2.4 Полиморфизм
Полиморфизм — это способность, с помощью которой мы можем создавать функции или ссылочные переменные, которые ведут себя по-разному в разных программных контекстах. Его часто называют одним именем со многими формами.
Например, в большинстве языков программирования оператор '+' используется для сложения двух чисел и объединения двух строк. В зависимости от типа переменных оператор меняет свое поведение. Это известно как перегрузка оператора.
В Java полиморфизм по существу подразделяется на два типа:
2.4.1 Полиморфизм времени компиляции
При полиморфизме времени компиляции компилятор может привязывать соответствующие методы к соответствующим объектам во время компиляции, поскольку он имеет всю необходимую информацию и знает, какой метод вызывать во время компиляции программы.
Его часто называют статическим связыванием или ранним связыванием.
В Java это достигается с помощью перегрузки методов. При перегрузке методов параметры метода могут различаться по числу, порядку или типу параметра.
class PlusOperator {int sum(int x, int y) {return x + y;}double sum(double x, double y) {return x + y;}String sum(String s1, String s2) {return s1.concat(s2);}}
2.4.2 Полиморфизм времени выполнения
В полиморфизме времени выполнения вызов переопределенного метода разрешается динамически во время выполнения. Объект, на котором будет выполнен метод, определяется во время выполнения – как правило, в зависимости от контекста, управляемого пользователем.
Его часто называют динамическим связыванием или переопределением методов. Мы могли слышать его под названием динамическая диспетчеризация методов.
В полиморфизме времени выполнения у нас обычно есть родительский класс и минимум один дочерний класс. В классе мы пишем оператор для выполнения метода, присутствующего в родительском и дочернем классах.
Вызов метода осуществляется с использованием переменной типа родительского класса. Фактический экземпляр класса определяется во время выполнения, поскольку переменная типа родительского класса может хранить ссылку на экземпляр родительского класса, а также на экземпляр дочернего класса.
class Animal {public void sound() {System.out.println("Some sound");}}class Lion extends Animal {public void sound() {System.out.println("Roar");}}class Main {public static void main(String[] args) {//Parent class reference is pointing to a parent objectAnimal animal = new Animal();animal.sound(); //Some sound//Parent class reference is pointing to a child objectAnimal animal = new Lion();animal.sound(); //Roar}}
3. Дополнительные концепции объектно-ориентированного программирования
Помимо четырех вышеперечисленных основных элементов ООП, у нас есть еще несколько концепций, которые играют важную роль в построении целостного понимания.
Прежде чем углубляться, давайте разберемся с термином модуль. В общем программировании модуль — это класс или подприложение, которое выполняет уникальную функциональность. В приложении HR класс может выполнять различные функции, такие как отправка писем по электронной почте, генерация зарплатных листов, расчет возраста сотрудника и т. д.
3.1.Сцепление
Связанность — это мера степени взаимозависимости между модулями. Связанность относится к тому, насколько сильно элемент программного обеспечения связан с другими элементами. Хорошее программное обеспечение будет иметь низкую связанность.
Это означает, что класс должен выполнять уникальную задачу или только задачи, которые независимы от других задач. Например, класс EmailValidator будет только проверять электронную почту. Аналогично, класс EmailSender будет только отправлять электронные письма.
Если мы включим обе функции в один класс EmailUtils, то это будет примером тесной связи.
3.2 Сплоченность
Сплоченность — это внутренний клей, который удерживает модуль вместе. Хороший дизайн программного обеспечения будет иметь высокую сплоченность.
Это означает, что класс/модуль должен включать всю информацию, необходимую для выполнения его функции без какой-либо зависимости. Например, класс EmailSender должен иметь возможность настраивать SMTP-сервер и принимать электронную почту отправителя, тему и содержимое. По сути, он должен фокусироваться только на отправке электронных писем.
Приложение не должно использовать EmailSender для какой-либо другой функции, кроме отправки электронной почты. Низкая связность приводит к монолитным классам, которые трудно поддерживать, понимать и которые снижают возможность повторного использования.
3.3 Ассоциация
Ассоциация подразумевает связь между объектами, имеющими независимые жизненные циклы и не владеющими друг другом.
Давайте рассмотрим пример учителя и ученика. Несколько учеников могут быть связаны с одним учителем, а один ученик может быть связан с несколькими учителями, но у обоих есть свои жизненные циклы.
Оба элемента можно создавать и удалять независимо друг от друга, поэтому, когда учитель покидает школу, нам не нужно удалять учеников, а когда ученик покидает школу, нам не нужно удалять учителей.
3.4 Агрегация
Ассоциация относится к отношениям между объектами, имеющими независимые жизненные циклы, но «С владением». Это отношения между дочерними и родительскими классами, где дочерние объекты не могут принадлежать другому родительскому объекту.
Давайте рассмотрим пример сотового телефона и аккумулятора сотового телефона. Один аккумулятор может принадлежать только одному телефону одновременно. Если телефон перестает работать, и мы удаляем его из нашей базы данных, аккумулятор телефона не будет удален, поскольку он все еще может быть функциональным. Таким образом, в агрегации, пока есть владение, объекты имеют свой собственный жизненный цикл.
3.5 Состав
Композиция относится к отношениям, когда объекты не имеют независимого жизненного цикла. Все дочерние объекты будут удалены, если родительский объект удален.
Например, связь между вопросами и ответами. Отдельные вопросы могут иметь несколько ответов, но ответы не могут принадлежать нескольким вопросам. Если мы удалим вопрос, все его ответы будут автоматически удалены.
4. Лучшие практики
4.1. Предпочтение композиции наследованию
Наследование и композиция, оба способствуют повторному использованию кода. Но использование композиции предпочтительнее наследования.
Реализация композиции поверх наследования обычно начинается с создания различных интерфейсов, представляющих поведение, которое должна демонстрировать система. Интерфейсы обеспечивают полиморфное поведение. Классы, реализующие идентифицированные интерфейсы, создаются и добавляются в классы бизнес-домена по мере необходимости. Таким образом, поведение системы реализуется без наследования.
interface Printable {print();}interface Convertible {print();}class HtmlReport implements Printable, Convertible{}class PdfReport implements Printable{}class XmlReport implements Convertible{}
4.2 Программа для интерфейса, а не для реализации
Это приводит к гибкому коду, который может работать с любой новой реализацией интерфейса. Мы должны стремиться использовать интерфейсы как переменные, как возвращаемые типы метода или как тип аргумента методов.
Интерфейсы действуют как типы суперкласса. Таким образом, мы можем создавать больше специализаций интерфейса в будущем, не изменяя существующий код.
4.3. СУХОЙ(не повторяйтесь)
Не пишите дублирующийся код, вместо этого используйте абстракцию, чтобы объединить общие вещи в одном месте.
Как правило, если вы пишете один и тот же фрагмент кода в двух местах, рассмотрите возможность выделения его в отдельную функцию и вызова этой функции в обоих местах.
4.4. Инкапсулируйте то, что меняется
Все программное обеспечение со временем меняется. Поэтому инкапсулируйте код, который, как вы ожидаете или подозреваете, будет изменен в будущем.
В Java используйте закрытые методы, чтобы скрыть такие реализации от клиентов, чтобы при внесении изменений клиенту не приходилось менять свой код.
Также рекомендуется использовать шаблоны проектирования для достижения инкапсуляции. Например, шаблон проектирования фабрики инкапсулирует код создания объекта и обеспечивает гибкость для введения нового типа позже, не влияя на существующих клиентов.
4.5 Принцип единой ответственности
Это один из принципов Solid проектирования классов ООП. Он подчеркивает, что один класс должен иметь одну и только одну ответственность.
Другими словами, мы должны писать, изменять и поддерживать класс только для одной цели. Это даст нам гибкость для внесения будущих изменений, не беспокоясь о влиянии изменений на другую сущность.
4.6 Принцип открытости-закрытости
Подчеркивается, что компоненты программного обеспечения должны быть открыты для расширения, но закрыты для модификации.
Это означает, что наши классы должны быть спроектированы таким образом, чтобы всякий раз, когда коллеги-разработчики захотят изменить поток управления в определенных условиях в приложении, им нужно было бы просто расширить наш класс и переопределить некоторые функции, и все.
Если другие разработчики не могут спроектировать желаемое поведение из-за ограничений, накладываемых нашим классом, то нам следует пересмотреть вопрос об изменении нашего класса.
В парадигме ООП есть много других концепций и определений, с которыми мы познакомимся в других уроках.
5. Резюме
В этом руководстве по Java OOP обсуждаются 4 основных столпа ООП в Java с простыми для понимания программами и фрагментами. Задавайте свои вопросы в разделе комментариев.