Анна ДжанибековаВ наши дни практически все инженеры-программисты достаточно глубоко осведомлены о принципах и подходах к применению объектно-ориентированного программирования (ООП). Абстракция, инкапсуляция, наследование, полиморфизм, S.O.L.I.D. для них – не заклинание, вызывающее дождь, а скорее основа повседневной деятельности. Эти методологические термины многократно и подробно объяснены в Сети. Без них сегодня не обходится практически ни одно собеседование. Разбуди девелопера среди ночи и спроси про любой из них, и он скорее всего не собьется в рассказе, так и не проснувшись до конца. Но, как часто случается с концепциями, гораздо реже встречается ответ на вопрос «зачем». Рассказывает Сергей Кондурушкин, старший инженер программист компании IT_One.
Предпосылки популярности ООП
Разработка программного обеспечения – дорогое удовольствие. Процесс этот должен быть максимально эффективным на всех этапах, от постановки задач до сопровождения продукта, иначе он становится экономически невыгодным. Он, этот процесс, не прост и чреват дорогостоящими ошибками, хотя и сулит выход бизнеса заказчика на новый уровень. Именно здесь кроется ответ на вопрос «зачем» в отношении ООП. Для эффективного (читай – экономически выгодного) преодоления сложностей разработки ПО.
Написание программного кода – это всегда решение той или иной поставленной перед программистом задачи, которую он может реализовать любыми доступными ему методами. Но зачастую приходится решать шаблонные задачи, практически не отличающиеся друг от друга. Или сталкиваться с тем, что задача распадается на отдельные, уже где-то встречавшиеся и решенные. ООП помогает систематизировать такие решения и избегать повторов.
Хорошим примером эффективного ответа ООП в сочетании с дженериками на целый класс типовых проблем могут служить коллекции java. Поверьте, хлеб программиста без них был бы горек…
Многократное использование кода (по сути – многократное использование решений) – залог управляемости, вытекающей из постоянства и предсказуемости знакомых компонентов.
Жизнь программного проекта не линейна. Иногда проектные изменения возникают на позднем этапе, когда много чистого, отлаженного и, что важно, оплаченного кода уже написано. И вот необходимо обеспечить поддержку новых требований, сохраняя решения в актуальном, рабочем состоянии.
ООП, если его «правильно готовить», позволяет предвосхитить подобные проблемы.
В правильно спроектированных и реализованных системах даже драматические на первый взгляд изменения в требованиях адаптируются порой посредством настройки конфигурации. Ну или другой «малой кровью».
Но чаще бывает так, что когда кода много, и он пишется многими людьми, которые приходят и уходят, то без должной дисциплины на уровне самого кода с какого-то момента команда начинает тратить неприемлемо много своего дорогостоящего рабочего времени на адаптацию накопившейся кодовой массы к самым небольшим изменениям. ООП здесь существенно выручает, ибо эта методология сама по себе поощряет разделение задач и решений по функциональности, использование правила «необходимой достаточности», когда большая задача и связанные с ней данные рационально делится на меньшие подзадачи и те в свою очередь находят свои максимально изолированные решения. Связь же между ними, их взаимодействие, оказываются выражены в четких контрактах.
Опыт применения ООП породил интереснейший самостоятельный феномен – шаблоны проектирования. Даже если вы не знакомы с каждым членом этого многочисленного семейства, но обладаете здравым смыслом и поняли «зачем» ООП вообще, то наверняка использовали их. Многие из них естественным образом вытекают из прямых требований самого ООП. На самом деле шаблоны проектирования – это особый вид многократного использования, только в данном случае не кода, а подходов к решению. Описывать шаблоны проектирования и реализовывать их в терминах ООП намного проще, чем, скажем, в терминах процедурного программирования. Хотя реализовать принятое на их основе решение можно на любом языке.
ООП помноженное на грамотное использование шаблонов создает основу для «промышленной» разработки ПО. Это можно сравнить с принципами разработки современных, скажем, автомобилей или компьютеров: чтобы спроектировать новую модель, не нужно заново создавать для нее базовые компоненты – используются уже готовые. Новая модель – она насколько новая? И все-таки… Из готовых блоков с добавлением щепотки инноваций возникает нечто…
Вскоре стало понятно, что шаблоны проектирования существуют и для систем более высокого уровня. Например, в организации существует большое количество приложений для разных функциональных подразделений: логистики, бухгалтерии и так далее. Для каждого из них существуют свои приложения, но они должны каким-то образом связываться между собой: возникает запрос на некие шаблоны проектирования этого взаимодействия. Переходя с уровня application на уровень enterprise мы начинаем мыслить уже не категориями классов и интерфейсов, а категориями модулей и интеграционных каналов.
Кажется, на этом уровне мы уже утрачиваем связь с объектно-ориентированным программированием в том виде, как его задумал Создатель... Здесь уже совсем другие игроки: (микро)сервисы, сторонние АПИ, каналы интеграции, распределенные кэши, протоколы…
Но если мы поищем в Сети практическое определение микросервиса, то оно удивительным образом будет напоминать классическое определение для объекта, принятое в ООП.
Простые примеры
Итак, OOП нам строить и жить помогает, воспитывая практичные и эффективные навыки реализации сложных проектов. По мне так и сам объектно-ориентированный код – штука приятная. Не знаю как вы, а я, когда пользуюсь навигацией по коду в моей любимой среде разработки, порой задумываюсь, насколько ее, навигации, удобство связано с тем, что это код на объектно-ориентированном языке. Думаю, напрямую. Возможно, авторы ООП и не задумывались о таком приятном «побочном эффекте».
Возьмем простой пример практического использования ООП. Допустим, требуется оперировать такой штукой как ИНН. Мы знаем, что ИНН бывает у физических и юридических лиц, но они отличаются количеством цифр в значении и механизмом верификации (да-да, у них по-разному вычисляется контрольная сумма). Значения приходят к нам в виде строк с непредсказуемым содержимым, но дальше в систему должны проникать только верифицированные значения. При этом конкретный интерес к тому, чей это ИНН – «юрика» или «физика» у нас отложен. До поры нам достаточно просто быть уверенными, что это «правильный» ИНН.
(пример кода)
В данном случае мы, сами того не замечая, применили аж несколько шаблонов проектирования. Даже не буду уточнять, каких… Но в итоге имеем две корректные анонимные реализации, а прочие запрещены, ибо в природе другого варианта ИНН пока не существует.
Другой пример: у нас есть набор сервисов для коммуникаций (почтовый, SMS и так далее) с одинаковым интерфейсом и схожим функционалом. Для отправки сообщений через них мы можем использовать этот единый интерфейс, вызывая его метод «send» у переданного нам экземпляра класса, реализующего этот интерфейс. Важно, что при этом возможность переключения канала отправки – например, с почты, на SMS – реализуется без изменения кода в точке отправки. Точка отправки не имеет ровным счетом никакого представления, т.к. для нее это просто какой-то носитель метода «send». Короче, получается что-то вроде
// ОБЪЕКТЫ ДАННЫХ
// Класс сообщения
public class Message {
// данные и методы для получения сведений об отправителе, получателе,
// содержимом и параметрах доставки сообщения
}
// ИНТЕРФЕЙСЫ
// Точка отправки. Отправляемые сообщения стекаются сюда
interface MessageDispatcher {
void dispatch(Message message) throws DispatchingException ;
}
// Канал отправки. Сообщения разлетаются во внешний мир через каналы
interface MessageChannel {
void send(Message message) throws SendingException;
}
// РЕАЛИЗАЦИИ
// Конкретный канал e-mail
class MailSender implements MessageChannel {
@Override
public void send(Message message) throws SendingException {
convertToEmailFormatAndSend(message);
}
}
// Конкретный канал sms
class SmsSender implements MessageChannel {
@Override
public void send(Message message) throws SendingException {
convertToSmsFormatAndSend(message);
}
}
// Конкретный канал-заглушка (чисто для отладки)
class NullSender implements MessageChannel {
@Override
public void send(Message message) throws SendingException {
justLogAndReturnWithoutException(message);
}
}
// Супер-умный канал который сам решает, куда слать сообщения
class DynamicSender implements MessageChannel {
@Override
public void send(Message message) throws SendingException {
dynamicallySelectTheChannelBySomeObscureCriteriaAndDelegateItTheCall(message);
}
}
// Дефолтный диспетчер сообщений, проинициализированный каналом,
// выбранным, например, по настройкам приложения из множества возможных
class DefaultMessageDispatcher implements MessageDispatcher {
// Этот канал был выбран по каким-то причинам как канал по умолчанию
// Но нам до этого нет ровным счетом никакого дела.
private final MessageChannel defaultChannel;
@Override
public void dispatch(Message message) throws DispatchingException {
verifyMessageUsingSomeRulesAndLogWhateverIsNeededAndPossiblyThrowDispatchinException(message);
try {
defaultChannel.send(message);
} catch(SendingException ex) {
logExceptionAndOptionallyThrowDispatchingException(ex, message);
} finally {
cleanupIfNeeded();
}
}
}
Если придерживаться этого каркаса, то, прописав классы исключений, конверторы, верификаторы и логирование, мы, надеюсь, заметим, что функциональность рассылки сообщений распалась не только на подзадачи, но и расслоилась на уровни, каждый из которых прост в понимании и в значительной степени изолирован. А это залог управляемости, тестируемости, стабильности. Что в данном случае мы использовали – инкапсуляцию, наследование, полиморфизм – об этом как-то уже не думаешь.
Преимущества ООП, как и любого инструмента программирования, проявляются только при правильном владении этим инструментом. Недостаточное изучение имеющихся в арсенале программиста средств может привести к «изобретению велосипеда», злоупотреблению абстракцией и усложнению проекта за счет каких-то лишних решений. Ну и здравый смысл… Не пренебрегаем – запрягаем.
Например, если необходимо представить ФИО в виде объекта со свойствами: «фамилия, имя и отчество», которые должны писаться с заглавной буквы, то создание трех отдельных реализаций интерфейса NamePart для каждого из них, хоть и возможно, но, как говорила моя бабушка, «декомпозируя задачу и выделяя доменные сущности, не сходи с ума, внучек». И то правда… Ибо на самом деле они по отдельности и не живут – фамилии с именами.
Компетенции программиста
Мы в IT_One используем язык Java. По крайней мере на том проекте, где я сейчас Java – наше все. И не только потому, что это «с момента зачатия» объектно-ориентированный язык. Java – это давно уже больше, чем язык. Это платформа, хотя само это слово мало что объясняет.
Дело в том, что профессионально разрабатывая современное ПО, инженер оперирует по большей части даже уже не пакетами классов, которых кстати, в Java великое множество, а скорее реализациями обширных спецификаций и фреймворками (Jakarta, Spring, Micronaut, Hibernate и др.), набором библиотек и инструментов, которые создают каркас приложения, задают тон всей разработке. Эти фреймворки – гигантское количество кода и модулей, воплощающих принципы и лучшие практики ООП.
При этом базовые знания о языке и принципы ООП современный developer использует автоматически, с мастерством – как водитель, выжимающий педаль сцепления и перемещающий рычаг переключения передач.
Сегодня в уже готовые, заранее созданные компоненты программист вносит специфическую для конкретной задачи или проекта функциональность, создает композиции из имеющихся компонентов, разрабатывает свои, следуя диктуемым самим фреймворком правилам. Эта «увлекательная рутина» занимает основное время современного разработчика.
Казалось бы, простор для творчества и самовыражения сужается пропорционально громадности фреймворка. Но все совсем не так грустно. Иногда (даже часто) достигнутый результат – «правильно» работающая система – доставляет гигантское удовольствие всей команде и каждому участнику «заплыва». Поверьте, расползающийся, кишащий повторами, плохо читаемый, запутанный, но «авторский» код, ставший таковым просто из-за игнорирования современных стандартов разработки, даже если «взлетит на проде», останется больше проблемой, чем решением.
Итого
Говорят, что объектно-ориентированное программирование сложнее в освоении и требует от программиста несколько больших, чем обычно, компетенций, помимо понимания базовых концепций.
Это мнение справедливо. Ибо то, зачем возникло ООП, уже с нами.
Это особый пестрый мир взаимодействующих решений, новомодных или консервативных, тяжеловесных или облегченных, специфических или общего назначения.
Но всегда управляемых, ясных, выработанных с применением нескольких простых и очевидных до банальности принципах.
И этот мир сегодня на подъеме.