SOLID. Что значит O
Это второй пост из цикла статей про SOLID. Сегодня мы разберем, что означает буква O в этой аббревиатуре. Как обычно, мы рассмотрим принцип на примере и поговорим о том, как он нам помогает, что было бы без него и зачем он вообще нужен.
O значит OCP
Что, в свою очередь, расшифровывается как Open-Closed Principle, он же принцип открытости/закрытости. Чисто теоретическое описание доступно на википедии, а мы сразу переходим к примеру.
OCP на практике
Представьте, что вы возвращаетесь в московский аэропорт «Домодедово» из своего отпуска по Европе. Вы выходите из самолета, проходите пару длинных коридоров и оказываетесь у стоек паспортного контроля.
Перед вами две стойки – для граждан Российской Федерации и для граждан других государств. В аэропорт одновременно приходит множество рейсов, и вполне логично для своих граждан выделить отдельную стойку, чтобы они получили приоритет и максимально быстро вернулись домой. Вы пользуетесь этой возможностью.
Это – правильная и грамотная реализация OCP. Давайте теперь посмотрим, что произошло бы, если бы проектировщики аэропорта нарушили этот принцип.
Игнорирование OCP
Давайте на минуту представим, что аэропорт по-прежнему хочет предоставить своим гражданам преимущество, но при этом не следует принципу открытости/закрытости.
Итак, перед стойкой паспортного контроля стоит очередь из граждан разных государств, в том числе и РФ. Вдруг выходит сотрудник аэропорта и объявляет: «Внимание! Ближайшие два часа обслуживаются только граждане Российской Федерации!».
Граждане РФ ликуют. Все остальные в аэропорту:
Что произошло?
А произошло вот что: аэропорт решил добавить новую фичу (предоставление приоритета своим гражданам) и при этом изменил реализацию паспортного контроля таким образом, что это оказалось неожиданностью для всех.
Как нужно было сделать? Оставить стойку в покое и никак не менять порядок паспортного контроля на ней. Вместо этого добавить еще одну стойку для граждан РФ. Собственно, как это и реализовано во всех аэропортах.
Пример кода
Давайте теперь посмотрим, как бы это выглядело в коде. Представим, что вы – разработчик класса PassportControl
.
Вы отвечаете за паспортный контроль прилетевших граждан. Вы проводите идентификацию пассажира и разного рода проверки. Например, граждане с просроченными паспортами и визами не должны пройти паспортный контроль. Если же все хорошо, вы ставите печать о въезде в страну и пропускаете гражданина.
public class PassportControl {
public void process(List<Passenger> passengers) {
passengers.forEach(passenger -> pass(passenger));
}
private void pass(Passenger passenger) {
if (validate(passenger)) {
// поставить печать и пропустить
} else {
// вызвать сотрудников для дальнейших действий
}
}
private boolean validate(Passenger passenger) {
Passport passport = passenger.getPassport();
boolean passportValid = checkPassportIsValid(passport);
boolean visaValid = checkVisas(passport.getVisas());
return passportValid && visaValid;
}
private boolean checkPassportValid(Passport passport) {
// return false если паспорт просрочен или невалиден
return true;
}
private boolean checkVisas(List<Visa> visas) {
// return false, если среди активных виз нет визы РФ
return true;
}
}
Аэропорт пользуется вашим классом в одном из своих сервисов:
public class ArrivalService {
private PassportControl passportControl;
public ArrivalService(PassportControl passportControl) {
this.passportControl = passportControl;
}
public void process(List<Passenger> passengers) {
passportControl.process(passengers);
}
}
И вроде бы все хорошо
Правда, лишь до тех пор, пока вы не решаете поменять реализацию своего метода pass
. Вы хотите, чтобы с 18:00 до 20:00 обслуживались только граждане РФ, и обновляете метод соответствующим образом:
private void pass(Passenger passenger) {
if (getCurrentHour() >= 18 && getCurrentHour() < 20) {
if (passenger.getCountryCode() != CountryCode.RU) {
return; // выходим из метода и не обслуживаем пассажира
}
}
if (validate(passenger)) {
// поставить печать и пропустить
} else {
// вызвать сотрудников для дальнейших действий
}
}
Аэропорт, в свою очередь, обновляется до последней версии вашего класса и продолжает им как ни в чем не бывало пользоваться. Правда, ровно до 18 часов, когда у стойки вдруг образуется толпа недовольных иностранцев. Как результат, аэропорту придется разбираться в причинах недовольства, проводить анализ логов и выполнять дебаг.
И все это при условии, что ни строчки кода в самом сервисе аэропорта не поменялось! Поменялась лишь реализация сторонней функции, которая аэропортом используется.
Проигнорировав OCP, вы породили ошибки в логике зависимого от вашего кода приложения.
Как надо было сделать
Нужно было оставить функцию pass
в исходном состоянии, вместо этого добавить новую функцию processPriority
с дополнительной функциональностью:
public class PassportControl {
public void process(List<Passenger> passengers) {
passengers.forEach(passenger -> pass(passenger));
}
public void processPriority(List<Passenger> passengers, CountryCode priority) {
passengers
.stream()
.filter(passenger -> priority.equals(passenger.getCountryCode()))
.forEach(passenger -> pass(passenger));
}
private void pass(Passenger passenger) {
if (validate(passenger)) {
// поставить печать и пропустить
} else {
// вызвать сотрудников для дальнейших действий
}
}
private boolean validate(Passenger passenger) {
Passport passport = passenger.getPassport();
boolean passportValid = checkPassportValid(passport);
boolean visaValid = checkVisa(passport.getVisas());
return passportValid && visaValid;
}
private boolean checkPassportValid(Date until) {
// return false если паспорт просрочен или невалиден
return true;
}
private boolean checkVisa(List<Visa> visas) {
// return false, если среди активных виз нет визы РФ
return true;
}
}
Второй метод, processPriority(passenger, countryCode)
, фильтрует граждан по заданному коду страны и проверяет только тех пассажиров, которые удовлетворяю условию.
Так как вы не меняете реализацию текущего метода, а лишь добавляете новый, работа аэропорта никак не меняется, а все пассажиры остаются довольны.
Когда вы рассказываете о своей новой функции своим клиентам (аэропорту), они вполне могут захотеть обновить свой код, чтобы начать эту функцию использовать:
public class ArrivalService {
private PassportControl passportControl;
public ArrivalService(PassportControl passportControl) {
this.passportControl = passportControl;
}
public void process(List<Passenger> passengers) {
passportControl.process(passengers);
}
public void processRussianCitizens(List<Passenger> passengers) {
passportControl.process(passengers, CountryCode.RU);
}
}
Вывод
OCP помогает расширять функциональность вашего кода таким образом, чтобы это не отразилось на работе завязанных на вас сервисов.
В примере выше в коде аэропорта не поменялась ни одной строчки, но работоспособность была нарушена. И все это из-за того, что всего лишь одна приватная функция библиотеки, от которой зависит аэропорт, изменила реализацию.
Если бы мы следовали принципу открытости/закрытости изначально и расширяли бы функциональность грамотно, то никаких проблем не возникло бы.
Заключение
Не нарушай работу зависимых от тебя частей системы. Следуй принципу OCP.
Предыдущие статьи из цикла SOLID:
S: https://baddev.ru/solid-srp/
Понравилось? Подписывайтесь на меня в соцсетях!