В своей работе мы будем встречаться с двумя типами объектов, одни имеют внутреннее состояние, другие - нет. Ко вторым может, например, относиться объект, задача которого превращать формат markdown в HTML. Подобный объект используется для превращения текста этого урока написанного в формате markdown, в HTML для вывода на сайте. Вот как выглядит использование такого объекта:
// Гипотетический пример, реальные библиотеки устроены сложнее
var md = new Markdown();
// ** - означает жирность в markdown
var html = md.render("**Hexlet**");
// <b>Hexlet</b>
В этом примере метод render()
никак не влияет на объект md
. Метод принимает на вход данные, трансформирует их и возвращает наружу. С таким же успехом, мы могли бы сделать обычный статический метод и использовать его вместо объекта.
Markdown.render("**Hexlet**");
Зачем в таком случае нам нужен объект? Есть несколько причин, одна из которых - конфигурация. Преобразование markdown в HTML делается по определенным правилам, которые можно менять, например, делать из урлов html-ссылки или нет.
var md = new Markdown(/* сюда передаются опции */);
Таким образом мы можем создать несколько разных видов объектов с разной конфигурацией и затем использовать их в приложении одновременно. В каком-то смысле это тоже состояние, но это не начальное состояние объекта, которое в процессе меняется с помощью методов. Это, как правило, неизменяемое (иммутабельное) состояние, которое используется методами в процессе работы. Сами методы на объект не влияют, поэтому сюда относятся и data-классы с неизменяемым содержимым. Подобные объекты очень просты в создании и работе. С ними редко возникают какие-то сложности.
Совсем другое дело, когда речь идет про объекты с состоянием. Классический пример это пользователь. Внутри такого объекта хранятся данные, которые могут меняться в процессе жизни этого объекта.
var user = new User("Mark");
user.getName(); // Mark
user.setName("Makarello");
user.getName(); // Makarello
Точно таким же объектом с состоянием является и массив
String[] planets = {"Mars", "Jupiter", "Saturn", "Uranus", "Neptune"};
System.out.println(planets[0]); // Mars
planets[0] = "Red Planet";
System.out.println(planets[0]); // Red Planet
Чем дальше в программировании, тем больше вы будете замечать, как управление состоянием объектов становится одной из самых сложных частей приложения. Разберем несколько примеров.
Многопоточность
Эта тема изучается в самом конце, но о ней нельзя не сказать говоря о состоянии объектов. Изменение одного объекта из разных потоков (поток это единица выполнения, в одной запущенной программе может быть много потоков) может приводить к непредсказуемому результату, когда данные не соответствуют тому что ожидается. Для решения этой проблемы используются специальные механизмы синхронизации.
Конструирование и изменение
Постоянно возникает вопрос, как правильно создавать объект, передавая все данные в конструктор или заполняя их через сеттеры? Во втором случае мы можем получить объект, который прямо сейчас находится в состоянии, когда его нельзя использовать, так как он не готов. Представьте что у нас есть требование создавать пользователя с обязательным заполнением имени и email. Вот что мы можем получить.
// Объект не валиден, его нельзя использовать
var user = new User();
// Все еще не валиден
user.setEmail("support@hexlet.io");
// Вот теперь можно
user.setFirstName("Olga");
В ситуациях когда это возможно, лучше предпочитать способ с заполнением через конструктор, тогда объекты будут готовы сразу после создания.
var user = new User("Olga", "support@hexlet.io");
Целостность состояния (Инварианты)
В случае объектов, инвариантами называются правила, которым должно соответствовать внутреннее состояние объекта. Если эти правила нарушаются, значит в программе есть баги. Например, у объекта описывающего банковский счет, при снятии денег, должна быть проверка на достаточность денег на счету.
var account = new BankAccount(100.0);
// Не должно сработать, так как на счету недостаточно денег
account.withdraw(150.0);
Одна из ключевых задач программиста при проектировании классов, учитывать инварианты и реализовывать их в коде.
Зависимые объекты
На практике объекты часто хранят внутри себя ссылки на другие объекты. Что может легко приводить к нарушению инвариантов без возможности это контролировать. Представьте что у нас в коде есть сотрудник и есть компания, которую можно получить так employee.getCompany()
.
var company = /* Создаем или получаем объект компании */;
var employee = new Employee("Mike", company);
employee.getCompany();
Так как компания это отдельный объект, то ничто не мешает менять его напрямую без взаимодействия с employee
. Причем сразу двумя способами.
// Напрямую
company.changeSomething(/* параметры */);
// Через объект employee
employee.getCompany().changeSomething(/* параметры */);
Если состояние объекта employee
зависит от состояния company
, то в момент таких изменений может произойти нарушение инвариантов, так как объект employee
ничего не знает об изменении.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.