Во-первых, необходимо отметить, что объектно-ориентированное программирование – это прекрасно. Во-вторых, что Java – это объектно-ориентированный язык и она “подталкивает” разработчиков придерживаться парадигмы ООП. В-третьих – хотя Java объектно-ориентированный язык, в ней можно писать совершенно не объектно-ориентированный код. Ну и в-четвёртых – ООП позволяет делать код чистым и понятным, а архитектуру программы структурированной и гибкой к изменениям.
Содержание:
База
Суть ООП начинается с создания классов и объектов. Класс – это шаблон или чертёж, описывающий атрибуты (поля) и методы (функции) объекта. Объект – сущность, обладающая собственным состоянием и поведением. Микроволновка – это сущность. У неё есть состояние (размеры, масса, материал, цвет и т.д.) и поведение (греть пищу, размораживать, ну и прочие режимы).
Будучи экземпляром класса, объект существует в памяти и может выполнять действия, определённые в классе. Но классы – лишь инструменты ООП. Можно написать классы, которые будут нарушать всевозможные принципы ООП. ООП – это идея, это концепция. Это парадигма.
Микроволновку можно описать в коде. Тогда её состояние превратится в поля класса, а поведение – в его методы.
В ООП программисты пытаются моделировать программу, как мир реальных объектов и их взаимодействий, что ближе к тому, как мы, люди, воспринимаем окружающую нас действительность. Парадигма ООП помогает организовать код вокруг явлений и сущностей, которые нам знакомы, что делает его более интуитивным и лёгким для понимания.
Запомните важные принципы:
Одна функция должна выполнять только одно законченное действие.
Для каждого класса должно быть определено единственное назначение.
Для ООП обычно выделяют 3 кита, три главных положения – инкапсуляция, наследование, полиморфизм. Однако я добавлю к ним четвёртого, на мой счёт, самого главного кита – абстракцию. Эту базовую идею обычно либо забывают, либо подразумевают, что это и так понятно. В свою очередь, мне хочется выделить её как отдельный принцип, причём самый первый. Давайте разберёмся со всеми китами объектно-ориентированного программирования по отдельности.
Абстракция
Абстракция позволяет программистам увидеть только важные для решаемой задачи характеристики и поведение сущности. Игнорируя все остальные моменты, программисты создают модель этой сущности в коде.
Если мы решаем задачу о написании программы для заселения в общежитие студентов, то, создавая класс Student, мы ни за что не будем записывать туда всё-всё-всё, что связано с сущностью “Студент”. Студенты – это сущности очень разносторонние. Но в коде этой программы не будет таких характеристик, как размер ноги, цвет волос и любимые музыкальные группы. Эти характеристики могли бы пригодиться для других программ (скажем, обувной магазин, парикмахерская или какой-нибудь аналог Spotify). Но для конкретной решаемой задачи от сущности “Студент” из реального мира в свою модель в коде мы, скорее всего, перенесём такие свойства, как специальность в университете, номер курса, возраст, пол и материальное положение семьи.
То же самое с поведением сущности. Студенты – сущности непредсказуемые. Можно было бы написать множество методов, и всё равно не исчерпать потенциал этих сущностей. Но мы не будем писать методы sendMemesToGroupChat(), askForComplitedLabWorksFromNextYearStudents(), beStressedAboutStudies() или playLeagueOfLegendsAtNightBeforeExam(). Какие методы тогда напишем? Скорее всего, в приложении, связанном с заселением в общежитие, пригодятся методы setRoom(), isPrioritizedForAccomodation(), calculateFeeDebt() и так далее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Student { ... public void setRoom(Room room) { ... } public boolean isPrioritizedForAccomodation() { ... } public double calculateFeeDebt() { ... } } |
Инкапсуляция
Когда кофемашина варит кофе, под её корпусом творится много вещей. Но от человека требуется лишь насыпать кофе и нажать кнопку.
Когда вы звоните записаться к врачу в поликлинику, вам не нужно знать все смены и отпуска врачей, кто сидит в регистратуре, как заполнять те документы и справки, которые вам нужны от врача, и сколько и каких в них полей.
Мы используем множество механизмов без необходимости вникать, что под капотом. Всё потому, что подробности реализации инкапсулированы в объектах, которые мы используем. Кофемашина инкапсулирует в себе, каким именно образом она варит кофе. Поликлиника инкапсулирует всю информацию по врачам, их сменам и форме документов.
Механизм должен быть неприхотлив в использовании, а также редко меняться со временем. Подумайте об этом, как о наборе публичных (public) методов. Их можно вызывать из любого другого места. Там совсем не нужно знать, как они работают.
Если говорить более конкретно, то инкапсуляция скрывает подробности реализации, объединяя внутренние параметры (поля) объекта с его поведением (методы). Таким образом, объект CoffeeMachine может публичным методом makeCoffee() взаимодействовать со своими private-переменными coffeeGrammsLeft и milkMillilitersLeft. И пользователю, вызывающему метод makeCoffee() абсолютно необязательно проверять перед этим, достаточно ли coffee и milk. Это инкапсулировано внутри класса CoffeeMachine. То есть его объект сам всё проверит и при необходимости выведет на экранчик сообщение с просьбой залить молоко и засыпать кофе.
1 2 3 4 5 6 7 8 9 10 |
public class CoffeeMachine { private double coffeeGrammsLeft; private double milkMillilitersLeft; ... public void makeCoffee() throws NotEnoughIngredientsException { ... } } |
Примерно это и составляет инкапсуляцию в программировании. Поверхностно инкапсуляция выливается в то, что все переменные в классах делаются private – это хороший тон. В том числе в языке Java. Оперируют этими переменными методы класса, скрывая внутреннюю реализацию от внешних глаз. Впоследствии к некоторым из приватных переменных может предоставляться доступ при помощи геттеров и сеттеров. Это специальные методы, занимающиеся исключительно предоставлением и изменением значения конкретного поля объекта.
Наследование
Наследование в ООП – это такой механизм, который позволяет создавать похожие друг на друга классы без копирования кода. Причём классы в чём-то похожие, а в чём-то отличающиеся. Высшая идея их объединяет, а реализация немножко отличается. Или множко. Наследование позволяет создавать новый класс на основе существующего, перенимая его свойства и методы. Класс-наследник может расширить или переопределить функциональность класса-родителя.
Обратная сторона наследования – обобщение. Наследуя свой класс от родительского, вы конкретизируете его (класс History конкретизирует класс AcademicSubject). С другой стороны, родительский класс обобщает свои классы-наследники (класс Clothing обобщает классы Sweater, Jacket и Hat).
Класс Animal обобщает своих наследников Cat, Dog и Humster. У кота, собаки и хомяка есть нечто общее. Общее вынесено в класс Animal, чтобы не приходилось дублировать код их схожих характеристик и поведения. Разница же реализована непосредственно в классах Cat, Dog и Humster. Таким образом, у нас есть три класса-наследника Animal. Все они имеют что-то общее, но и отличаются друг от друга.
Какой-нибудь бунтарский класс-наследник Animal мог бы не только добавить новый функционал и характеристики, но и даже переопределить уже имеющиеся в классе Animal с помощью аннотации @Override над методом. И если у Animal метод jump() вызывал бы прыжок, то у его наследника Fish переопределённый метод jump() бы ничего не делал. Вот, что такое наследование.
1 2 3 4 5 6 7 |
public class Animal { ... public void jump() { ... } } |
1 2 3 4 5 6 7 8 |
public class Fish extends Animal { ... @Override public void jump() { //ничего не делать } } |
Полиморфизм
Полиморфизм – это способность всех музыкальных инструментов издавать звуки, хотя инструменты разные, и звуки эти будут совершенно разные. Это способность всех консультантов консультировать, всех преподавателей преподавать и всех водителей водить. Но! Делать они это будут по-разному. У них будет разная реализация одного и того же понятия.
Существуют классы, которые недореализованы (абстрактные классы). Или совсем не реализованы (в таком случае это уже не классы, а интерфейсы). Но это не потому, что разработчики поленились и не дописали реализацию. Это такая концепция.
Оставляя часть методов без реализации, разработчик предоставляет простор для написания различных наследников этой сущности. Эти наследники определяли бы собственное поведение при вызове одного и того же метода. Ситуация задана в классе-родителе, но реализована она в классе-наследнике.
Если у нас есть абстрактный продавец-консультант Consultant, то пускай у него будет абстрактный метод consult(). Абстрактный – значит нереализованный. Имя есть – тела нету. Как же тогда абстрактный консультант будет консультировать, если он не знает, как?
1 2 3 4 5 |
public class abstract Consultant { ... public abstract void consult(); } |
А ему и не нужно. Знать, как консультировать, будут его наследники. Они реализуют метод consult(), каждый по-своему. Но вот в чём фишка. С самыми разными консультантами из разных стран, с разными убеждениями и вообще консультирующими по разным сферам, можно будет взаимодействовать одним и тем же образом – через метод consult().
1 2 3 4 5 6 7 8 |
public class ClothesConsultant extends Consultant { ... @Override public void consult() { //своя реализация консультирования по одежде } } |
1 2 3 4 5 6 7 8 |
public class LaptopConsultant extends Consultant { ... @Override public void consult() { //своя реализация консультирования по ноутбукам } } |
В магазине Shop будет поле типа данных Consultant. И магазину совершенно необязательно знать, какая именно реализация консультанта к нему попадёт. Ведь магазин знает, как к консультанту обратиться – через метод consult(). А какой конкретный консультант попадётся – разберёмся на месте (во время выполнения программы). Это и есть полиморфизм в Java и других ООП языках.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Shop { private Consultant consultant; ... public Shop(Consultant consultant) { this.consultant = consultant; } public void helpCustomer() { consultant.consult(); ... } } |
1 2 3 4 5 6 7 8 9 10 11 |
public class Main { public static void main(String[] args) { //метод helpCustomer() и магазин в целом работают с консультантом одежды Shop shop = new Shop(new ClothesConsultant()); shop.helpCustomer(); //метод helpCustomer() и магазин в целом работают с консультантом ноутбуков Shop shop = new Shop(new LaptopConsultant()); shop.helpCustomer(); } } |
Таким образом, объектно-ориентированное программирование предоставляет крутые инструменты для создания структурированного и чистого кода. Это позволяет разрабатывать сложные системы с понятными иерархиями и взаимодействиями между объектами. Это лишь введение в тему ООП, а 4 кита в статье рассмотрены поверхностно. Эта концепция имеет глубокие и интересные аспекты, которые можно изучать бесконечно. Сегодня мы только немножко прикоснулись к сути ООП.