OCP
#программирование

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/

Понравилось? Подписывайтесь на меня в соцсетях!

 
Twitter
VK
guest
0 Комментариев
Inline Feedbacks
View all comments
Social media & sharing icons powered by UltimatelySocial
0
Нравится? Оставьте комментарий!x
()
x