Объектно-ориентированное программирование (ООП) — это методология, основанная на представлении программы как совокупности взаимодействующих объектов. Ее ключевые принципы: Абстракция (выделение главного), Наследование (повторное использование кода), Инкапсуляция (скрытие реализации) и Полиморфизм (единый интерфейс для разных типов). Это формальное определение, которое лежит в основе современных языков программирования.
А теперь давай углубимся в тему и рассмотрим простые примеры из реального мира, который нас окружает.
Ты когда-нибудь задумывался, почему Java устроена именно так, как она устроена? В том смысле, что ты создаешь классы, на их основе — объекты, у классов есть методы и т.д.
Но почему структура языка такова, что программы состоят именно из классов и объектов, а не из чего-то другого? Зачем было придумано понятие «объект» и поставлено во главу угла? Все ли языки устроены так и, если нет, какие преимущества это дает Java?
Вопросов, как видишь, много :) Попробуем ответить на каждый из них в сегодняшней лекции.
Java является объектно-ориентированным языком. Это означает, что писать программы на Java нужно с применением объектно-ориентированного стиля. И стиль этот основан на использовании в программе объектов и классов.![Принципы объектно-ориентированного программирования - 2]()
Кристен Нюгор и Оле Йохан Даль - создатели Simula
Казалось бы, Simula — древний язык по меркам программирования, но их «родственную» связь с Java видно невооруженным глазом.
Скорее всего, ты легко прочтешь написанный на нем код и в общих чертах объяснишь, что он делает :)
![Принципы объектно-ориентированного программирования - 3]()
Теперь с помощью абстракции мы можем выделить в этой иерархии объектов общую информацию: общий абстрактный тип объектов — телефон, общую характеристику телефона — год его создания, и общий интерфейс — все телефоны способны принимать и посылать вызовы.
Вот как это выглядит на Java:
Что такое объект?
Мир, в котором мы живем, состоит из объектов. Если мы посмотрим вокруг, то увидим, что нас окружают дома, деревья, автомобили, мебель, посуда, компьютеры. Все эти предметы являются объектами, и каждый из них обладает набором определенных характеристик, поведением и назначением. Мы привыкли к объектам, и мы их используем всегда для вполне конкретных целей. Например, если нам необходимо доехать до работы, мы пользуемся автомобилем, если захотим поесть – посудой, а если отдохнуть – нам понадобится удобный диван. Человек привык мыслить объектно для решения задач в повседневной жизни. Это послужило одной из причин использования объектов в программировании, а такой подход к созданию программ назвали объектно-ориентированным. Приведём пример. Представьте, что вы разработали новую модель телефона и хотите наладить её серийное производство. Как разработчик телефона, вы знаете для чего он нужен, как он будет функционировать, и из каких деталей он будет состоять (корпус, микрофон, динамик, провода, кнопки и т.д.). При этом только вы знаете, как соединить эти детали. Однако вы не планируете выпускать телефоны лично, для этого у вас есть целый штат работников. Чтобы вам не пришлось каждый раз объяснять, как соединить детали телефона, и чтобы все телефоны при производстве получались одинаковыми, прежде чем начать их выпуск, вам понадобиться сделать чертеж в виде описания устройства телефона. В ООП такое описание, чертеж, схема или шаблон называется классом, из которого при выполнении программы создается объект. Класс — это описание еще не созданного объекта, как бы общий шаблон, состоящий из полей, методов и конструктора, а объект – экземпляр класса, созданный на основе этого описания.Что такое объектно-ориентированное программирование (ООП)
Конечно, Java не просто так состоит из объектов и классов. Это не прихоть ее создателей, и даже не их изобретение. Есть множество других языков, в основе которых лежат объекты. Первый такой язык назывался Simula, и его изобрели еще в 1960-х годах в Норвегии. Помимо всего прочего, в Simula появились понятия «класс» и «метод».
Begin
Class Rectangle (Width, Height); Real Width, Height;
Begin
Real Area, Perimeter;
Procedure Update;
Begin
Area := Width * Height;
OutText("Rectangle is updating, Area = "); OutFix(Area,2,8); OutImage;
Perimeter := 2*(Width + Height);
OutText("Rectangle is updating, Perimeter = "); OutFix(Perimeter,2,8); OutImage;
End of Update;
Update;
OutText("Rectangle created: "); OutFix(Width,2,6);
OutFix(Height,2,6); OutImage;
End of Rectangle;
Rectangle Class ColouredRectangle (Color); Text Color;
Begin
OutText("ColouredRectangle created, color = "); OutText(Color);
OutImage;
End of ColouredRectangle;
Ref(Rectangle) Cr;
Cr :- New ColouredRectangle(10, 20, "Green");
End;
Пример кода взят из статьи Simula — 50 лет ООП.
Как видишь, Java и его предок не так уж сильно отличаются друг от друга :)
Это связано с тем, что появление Simula ознаменовало собой рождение новой концепции — объектно-ориентированного программирования.
Википедия дает такое определение ООП: Объе́ктно-ориенти́рованное программи́рование (ООП) — методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определенного класса, а классы образуют иерархию наследования.
Оно, на мой взгляд, очень удачное. Ты недавно начал изучать Java, но в нем вряд ли найдутся незнакомые тебе слова:)
Сегодня ООП — самая распространенная методология программирования.
Помимо Java принципы ООП используются во многих популярных языках, о которых ты, возможно, слышал. Это C++ (его активно применяют разработчики компьютерных игр), Objective-C и Swift (на них пишут программы для устройств Apple), Python (наиболее востребован в машинном обучении), PHP (один из самых популярных языков веб-разработки), JavaScript (проще сказать, чего на нем не делают) и многие другие.
Собственно говоря, что это за «принципы» ООП? Расскажем подробнее.
Принципы ООП
Это основа основ. 4 главные особенности, которые вместе образуют парадигму объектно-ориентированного программирования. Их понимание — ключ к становлению успешного программиста. Принципы ООП:
Принцип 1. Абстракция
Это очень простой принцип. Абстракция означает выделение главных, наиболее значимых характеристик предмета и наоборот — отбрасывание второстепенных, незначительных. Не будем изобретать велосипед и вспомним пример из старой лекции про классы. Скажем, мы создаем картотеку работников компании. Для создания объектов «работник» мы написали классEmployee. Какие характеристики важны для их описания в картотеке компании? ФИО, дата рождения, номер социального страхования, ИНН. Но вряд ли в карточке такого типа нам нужны его рост, цвет глаз и волос. Компании эта информация о сотруднике ни к чему.
Поэтому для класса Employee мы зададим переменные String name, int age, int socialInsuranceNumber и int taxNumber, а от лишней для нас информации вроде цвета глаз откажемся, абстрагируемся.
А вот если мы создаем картотеку фотомоделей для агентства, ситуация резко меняется. Для описания фотомодели нам как раз очень важны рост, цвет глаз и цвет волос, а номер ИНН не нужен.
Поэтому в классе Model мы создаем переменные String height, String hair, String eyes.Пример абстракции с иерархией телефонов☎️
Давайте теперь подумаем, как нам перейти от объекта из реального мира к объекту в программе на примере телефона. История этого средства связи превышает 100 лет и современный телефон, в отличие от своего предшественника из 19 века, представляет собой куда более сложное устройство. Когда мы пользуемся телефоном, то не задумываемся о его устройстве и процессах, происходящих внутри него. Мы просто используем функции, предоставленные разработчиками телефона — кнопки или сенсорный экран для выбора номера и совершения вызовов. Одним из первых интерфейсов телефона была рукоятка, которую нужно было вращать, чтобы сделать вызов. Разумеется, это было не очень удобно. Тем не менее, свою функцию рукоять исправно выполняла. Если посмотреть на самый современный и на самый первый телефон, можно сразу выделить самые важные детали, которые важны и для устройства конца 19-го века, и для суперсовременного смартфона. Это совершение вызова (набор номера) и приём вызова. По сути это то, что делает телефон телефоном, а не чем-то другим. Сейчас мы применили принцип в ООП — выделение наиболее важных характеристик и информации об объекте. Этот принцип ООП называется абстракцией. Абстракцию в ООП можно также определить, как способ представления элементов задачи из реального мира в виде объектов в программе. Абстракция всегда связана с обобщением некоторой информации о свойствах предметов или объектов, поэтому главное — это отделить значимую информацию от незначимой в контексте решаемой задачи. При этом уровней абстракции может быть несколько. Попробуем применить принцип абстракции к нашим телефонам. Для начала выделим наиболее распространённые типы телефонов от самых первых и до наших дней. Например, их можно представить в виде диаграммы, приведенной на рисунке.
Теперь с помощью абстракции мы можем выделить в этой иерархии объектов общую информацию: общий абстрактный тип объектов — телефон, общую характеристику телефона — год его создания, и общий интерфейс — все телефоны способны принимать и посылать вызовы.
Вот как это выглядит на Java:
public abstract class AbstractPhone {
private int year;
public AbstractPhone(int year) {
this.year = year;
}
public abstract void call(int outputNumber);
public abstract void ring (int inputNumber);
}
На основании этого абстрактного класса мы сможем создавать в программе новые типы телефонов с использованием других базовых принципов ООП, которые рассмотрим ниже.Принцип 2. Наследование
Хорошая новость: с некоторыми из принципов ООП ты уже знаком! :) Наследование нам уже пару раз встречалось в лекциях, и мы успели с ним поработать. Наследование — механизм, который позволяет описать новый класс на основе существующего (родительского). При этом свойства и функциональность родительского класса заимствуются новым классом. Для чего нужно наследование и какие преимущества оно дает? Прежде всего — повторное использование кода. Поля и методы, описанные в родительских классах, можно использовать в классах-потомках. Если у всех типов автомобилей есть 10 общих полей и 5 одинаковых методов, тебе достаточно вынести их в родительский классAuto. Ты сможешь использовать их в классах-потомках безо всяких проблем.
Сплошные плюсы: и количественно (меньше кода), и, как следствие, качественно (классы становятся гораздо проще).
При этом механизм наследования очень гибкий, и недостающую в потомках функциональность ты можешь дописать отдельно (какие-то специфические для конкретного класса поля или поведение).
В общем, как и в обычной жизни: все мы чем-то похожи на наших родителей, а чем-то отличаемся от них :)Пример наследования с иерархией телефонов☎️
Давайте посмотрим еще раз на диаграмму телефонов. Можно заметить, что она представляет собой иерархию, в которой модель, расположенная ниже обладает всеми признаками моделей, расположенных выше по ветке, плюс своими собственными. Например, смартфон, использует сотовую сеть для связи (обладает свойствами сотового телефона), является беспроводным и переносным (обладает свойствами беспроводного телефона) и может принимать и делать вызовы (свойствами телефона). В этом случае мы можем говорить о наследовании свойств объекта. В программировании наследование заключается в использовании уже существующих классов для описания новых. Рассмотрим пример создания класса смартфон с помощью наследования. Все беспроводные телефоны работают от аккумуляторных батарей, которые имеют определенный ресурс работы в часах. Поэтому добавим это свойство в класс беспроводных телефонов:
public abstract class WirelessPhone extends AbstractPhone {
private int hour;
public WirelessPhone(int year, int hour) {
super(year);
this.hour = hour;
}
}
Сотовые телефоны наследуют свойства беспроводного телефона, мы также добавили в этот класс реализацию методов call и ring:
public class CellPhone extends WirelessPhone {
public CellPhone(int year, int hour) {
super(year, hour);
}
@Override
public void call(int outputNumber) {
System.out.println("Вызываю номер " + outputNumber);
}
@Override
public void ring(int inputNumber) {
System.out.println("Вам звонит абонент " + inputNumber);
}
}
И, наконец, класс смартфон, который в отличие от классических сотовых телефонов имеет полноценную операционную систему. В смартфон можно добавлять новые программы, поддерживаемые данной операционной системой, расширяя, таким образом, его функциональность. С помощью кода класс можно описать так:
public class Smartphone extends CellPhone {
private String operationSystem;
public Smartphone(int year, int hour, String operationSystem) {
super(year, hour);
this.operationSystem = operationSystem;
}
public void install(String program){
System.out.println("Устанавливаю " + program + "для" + operationSystem);
}
}
Как видите, для описания класса Smartphone мы создали совсем немного нового кода, но получили новый класс с новой функциональностью. Использование принципа наследование ООП позволяет значительно уменьшить объем кода, а значит, и облегчить работу программисту.Принцип 3. Инкапсуляция
С ним мы уже сталкивались. Инкапсуляция в Java означает ограничение доступа к данным и возможностям их изменения. Как видишь, в его основе лежит слово «капсула». В эту «капсулу» мы прячем какие-то важные для нас данные, которые не хотим, чтобы кто-то менял. Простой пример из жизни. У тебя есть имя и фамилия. Их знают все твои знакомые. Но у них нет доступа к изменению твоего имени и фамилии. Этот процесс, можно сказать, «инкапсулирован» в паспортном столе: поменять имя фамилию можно только там, и сделать это можешь только ты. Остальные «пользователи» имеют доступ к твоему имени и фамилии «только на чтение» :) Еще один пример — деньги в твоей квартире. Оставлять их на виду посреди комнаты — не лучшая идея. Любой «пользователь» (человек, пришедший к тебе домой) сможет изменить число твоих денег, т.е. забрать их. Лучше инкапсулировать их в сейфе. Доступ будет только у тебя и только по специальному коду. Очевидные примеры инкапсуляции, с которыми ты уже работал, — это модификаторы доступа (private, public и т.д.), а также геттеры-сеттеры.
Если поле age у класса Cat не инкапсулировать, кто угодно сможет написать:
Cat.age = -1000;
А механизм инкапсуляции позволяет нам защитить поле age при помощи метода-сеттера, в который мы можем поместить проверку того, что возраст не может быть отрицательным числом.Пример инкапсуляции с иерархией телефонов☎️
С помощью абстракции мы выделяем общее для всех объектов. Однако каждая модель телефона — индивидуальна и чем-то отличается от других. Как же нам в программе провести границы и обозначить эту индивидуальность? Как сделать так, чтоб никто из пользователей случайно или преднамеренно не смог сломать наш телефон, или попытаться переделать одну модель в другую? Для мира реальных объектов ответ очевиден: нужно поместить все детали в корпус телефона. Ведь если этого не сделать и оставить все внутренности телефона и провода, соединяющие их снаружи, обязательно найдется любознательный экспериментатор, который захочет “улучшить” работу нашего телефона. Для исключения подобного вмешательства в конструкцию и работу объекта в ООП используют принцип инкапсуляции – еще один базовый принцип ООП, при котором атрибуты и поведение объекта объединяются в одном классе, внутренняя реализация объекта скрывается от пользователя, а для работы с объектом предоставляется открытый интерфейс. Задача программиста — определить, какие атрибуты и методы будут доступны для открытого доступа, а какие являются внутренней реализацией объекта и должны быть недоступны для изменений.Инкапсуляция и управление доступом
Допустим, при производстве на тыльной стороне телефона гравируется информация о нем: год его выпуска или логотип компании производителя. Эта информация вполне конкретно характеризует данную модель — его состояние. Можно сказать, разработчик телефона позаботился о неизменности этой информации — вряд ли кому-то придет в голову удалять гравировку. В мире Java состояние будущих объектов описывается в классе с помощью полей, а их поведение – с помощью методов. Возможность же изменения состояния и поведения осуществляется с помощью модификаторов доступа к полям и методам –private, protected, public, а также default (доступ по умолчанию).
Например, мы решили, что год создания, название производителя телефона и один из методов относятся к внутренней реализации класса и не подлежат изменению другими объектами в программе.
С помощью кода класс можно описать так:
public class SomePhone {
private int year;
private String company;
public SomePhone(int year, String company) {
this.year = year;
this.company = company;
}
private void openConnection(){
//findComutator
//openNewConnection...
}
public void call() {
openConnection();
System.out.println("Вызываю номер");
}
public void ring() {
System.out.println("Дзынь-дзынь");
}
}
Модификатор private делает доступными поля и методы класса только внутри данного класса. Это означает, что получить доступ к private полям извне невозможно, как и нет возможности вызвать private методы.
Сокрытие доступа к методу openConnection, оставляет нам также возможность к свободному изменению внутренней реализации этого метода, так как этот метод гарантированно не используется другими объектами и не нарушит их работу.
Для работы с нашим объектом мы оставляем открытыми методы call и ring с помощью модификатора public.
Предоставление открытых методов для работы с объектом также является частью механизма инкапсуляции, так как если полностью закрыть доступ к объекту – он станет бесполезным.Принцип 4. Полиморфизм
Полиморфизм — это возможность работать с несколькими типами так, будто это один и тот же тип. При этом поведение объектов будет разным в зависимости от типа, к которому они принадлежат. Звучит сложновато? Сейчас разберемся. Возьмем самый простой пример — животных. Создадим классAnimal с единственным методом — voice(), и двух его наследников — Cat и Dog.
public class Animal {
public void voice() {
System.out.println("Голос!");
}
}
public class Dog extends Animal {
@Override
public void voice() {
System.out.println("Гав-гав!");
}
}
public class Cat extends Animal {
@Override
public void voice() {
System.out.println("Мяу!");
}
}
Теперь попробуем создать ссылку Animal и присвоить ей объект Dog.
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
dog.voice();
}
}
Как ты думаешь, какой метод будет вызван? Animal.voice() или Dog.voice()?
Будет вызван метод класса Dog:
Гав-гав!
Мы создали ссылку Animal, но объект ведет себя как Dog. При необходимости он может вести себя как кошка, лошадь или другое животное. Главное — присвоить ссылке общего типа Animal объект конкретного класса-наследника. Это логично, ведь все собаки являются животными.
Именно это мы имели в виду, когда говорили «поведение объектов будет разным, в зависимости от того, к какому типу они принадлежат».
Если бы мы создали объект Cat —
public static void main(String[] args) {
Animal cat = new Cat();
cat.voice();
}
метод voice() вывел бы «Мяу!».
А что значит «возможность работать с несколькими типами так, как будто это один и тот же тип»?
Это тоже довольно легко.
Давайте представим, что мы создаем парикмахерскую для животных. В нашей парикмахерской должны уметь стричь всех животных, поэтому мы создадим метод shear() («постричь») с параметром Animal — животным, которое мы будем стричь.
public class AnimalBarbershop {
public void shear(Animal animal) {
System.out.println("Стрижка готова!");
}
}
И теперь мы можем передавать в метод shear и объекты Cat, и объекты Dog!
public static void main(String[] args) {
Cat cat = new Cat();
Dog dog = new Dog();
AnimalBarbershop barbershop = new AnimalBarbershop();
barbershop.shear(cat);
barbershop.shear(dog);
}
Вот и наглядный пример: класс AnimalBarbershop работает с типами Cat и Dog так, как будто это один и тот же тип. При этом у Cat и Dog разное поведение: они по-разному подают голос.Пример полиморфизма с иерархией телефонов☎️
Если мы посмотрим на все модели телефонов, то, несмотря на различия во внешнем облике и устройстве моделей, мы можем выделить у них некое общее поведение – все они могут принимать и совершать звонки и имеют достаточно понятный и простой набор кнопок управления. Применяя известный нам уже один из основных принципов ООП абстракцию в терминах программирования можно сказать, что объект телефон имеет один общий интерфейс. Поэтому пользователи телефонов могут вполне комфортно пользоваться различными моделями, используя одни и те же кнопки управления (механические или сенсорные), не вдаваясь в технические тонкости устройства. Так, вы постоянно пользуетесь сотовым телефоном, и без труда сможете совершить звонок с его стационарного собрата. Принцип в ООП, когда программа может использовать объекты с одинаковым интерфейсом без информации о внутреннем устройстве объекта, называется полиморфизмом. Давайте представим, что нам в программе нужно описать пользователя, который может пользоваться любыми моделями телефона, чтобы позвонить другому пользователю. Вот как можно это сделать:
public class User {
private String name;
public User(String name) {
this.name = name;
}
public void callAnotherUser(int number, AbstractPhone phone){
// вот он полиморфизм - использование в коде абстактного типа AbstractPhone phone!
phone.call(number);
}
}
}
Теперь опишем различные модели телефонов. Одна из первых моделей телефонов:
public class ThomasEdisonPhone extends AbstractPhone {
public ThomasEdisonPhone(int year) {
super(year);
}
@Override
public void call(int outputNumber) {
System.out.println("Вращайте ручку");
System.out.println("Сообщите номер абонента, сэр");
}
@Override
public void ring(int inputNumber) {
System.out.println("Телефон звонит");
}
}
Обычный стационарный телефон:
public class Phone extends AbstractPhone {
public Phone(int year) {
super(year);
}
@Override
public void call(int outputNumber) {
System.out.println("Вызываю номер" + outputNumber);
}
@Override
public void ring(int inputNumber) {
System.out.println("Телефон звонит");
}
}
И, наконец, крутой видеотелефон:
public class VideoPhone extends AbstractPhone {
public VideoPhone(int year) {
super(year);
}
@Override
public void call(int outputNumber) {
System.out.println("Подключаю видеоканал для абонента " + outputNumber );
}
@Override
public void ring(int inputNumber) {
System.out.println("У вас входящий видеовызов..." + inputNumber);
}
}
Создадим объекты в методе main() и протестируем метод callAnotherUser:
AbstractPhone firstPhone = new ThomasEdisonPhone(1879);
AbstractPhone phone = new Phone(1984);
AbstractPhone videoPhone=new VideoPhone(2018);
User user = new User("Андрей");
user.callAnotherUser(224466,firstPhone);
// Вращайте ручку
//Сообщите номер абонента, сэр
user.callAnotherUser(224466,phone);
//Вызываю номер 224466
user.callAnotherUser(224466,videoPhone);
//Подключаю видеоканал для абонента 224466
Используя вызов одного и того же метода объекта user, мы получили различные результаты. Выбор конкретной реализации метода call внутри метода callAnotherUser производился динамически на основании конкретного типа вызывающего его объекта в процессе выполнения программы. В этом и заключается основное преимущество полиморфизма – выбор реализации в процессе выполнения программы.Причины появления ООП
Почему вообще возникла эта новая концепция программирования — ООП? У программистов были работающие инструменты: например, процедурные языки. Что же побудило их изобретать что-то принципиально новое? Прежде всего, усложнение задач, которые перед ними стояли. Если 60 лет назад задача программиста выглядела как «вычислить математическое уравнение такое-то», сейчас она может звучать как «реализовать 7 различных концовок для игры S.T.A.L.K.E.R. в зависимости от того, какие решения принимал пользователь в игровых моментах A, B, C , D, E, F и комбинаций этих решений». Задачи, как видишь, за прошедшие десятилетия явно усложнились. А значит, усложнились и типы данных. Это еще одна причина возникновения ООП. Пример с уравнением легко можно решить с помощью обычных примитивов, никаких объектов тут не надо. А вот задачу с концовками игры сложно будет даже описать, не используя каких-то придуманных тобой же классов. Но при этом описать ее в классах и объектах достаточно легко: нам явно будет нужен класс Игра, класс Сталкер, класс Концовка, класс РешениеИгрока, класс ИгровойМомент и так далее. То есть даже не приступив к решению задачи, мы в голове можем легко представить «наброски» ее решения. Усложнение задач поставило программистов перед необходимостью делить задачу на части. Но в процедурном программировании сделать это было не так просто. И очень часто программа представляла собой «дерево» из кучи веток со всеми возможными вариантами ее работы. В зависимости от каких-то условий, программа выполнялась по той или иной ветке. Для небольших программ такой вариант был удобен, но поделить на части объемную задачу было очень сложно. Эта необходимость стала еще одной причиной возникновения ООП. Эта концепция предоставила программистам возможность делить программу на кучу «модулей»-классов, каждый из которых делает свою часть работы. Все объекты, взаимодействуя между собой, образуют работу нашей программы. Кроме того, написанный нами код можно повторно использовать в другом месте программы, что также экономит большое количество времени. В итоге, чтобы стиль вашей программы соответствовал концепции ООП и принципам ООП следуйте следующим советам:- выделяйте главные характеристики объекта;
- выделяйте общие свойства и поведение и используйте наследование при создании объектов;
- используйте абстрактные типы для описания объектов;
- старайтесь всегда скрывать методы и поля, относящиеся к внутренней реализации класса.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ