Статьи

Как избежать множества блоков If для проверки правильности

В некоторых случаях мы хотим проверить входные данные, прежде чем отправлять их на уровень бизнес-логики для обработки, вычислений и т. Д. Эта проверка в большинстве случаев выполняется изолированно или может включать некоторую перекрестную проверку с внешними данными или другими входными данными. Взгляните на следующий пример, который проверяет ввод пользователя для регистрационных данных.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public void register(String email, String name, int age) {
 String EMAIL_PATTERN =
 "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
 + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
 Pattern pattern = Pattern.compile(EMAIL_PATTERN);
  
 List<String> forbiddenDomains = Arrays.asList("domain1", "domain2");
 if ( email == null || email.trim().equals("")){
   throw new IllegalArgumentException("Email should not be empty!");
 }
  
 if ( !pattern.matcher(email).matches()) {
   throw new IllegalArgumentException("Email is not a valid email!");
 }
  
 if ( forbiddenDomains.contains(email)){
   throw new IllegalArgumentException("Email belongs to a forbidden email");
 }
  
 if ( name == null || name.trim().equals("")){
   throw new IllegalArgumentException("Name should not be empty!");
 }
1
2
3
if ( !name.matches("[a-zA-Z]+")){
   throw new IllegalArgumentException("Name should contain only characters");
 }
1
2
3
if ( age <= 18){
   throw new IllegalArgumentException("Age should be greater than 18");
 }
1
// More code to do the actual registration
1
}

Цикломатическая сложность этого метода действительно высока, и она может ухудшиться, если будет больше полей для проверки или если мы добавим фактическую бизнес-логику. Конечно, мы можем разделить код на два приватных метода (validate, doRegister), но проблема с несколькими, если блоки будут перемещены в приватные методы. Кроме того, этот метод делает больше, чем одну вещь, и его сложно проверить. Когда я прошу начинающих разработчиков реорганизовать этот код и сделать его более читабельным, тестируемым и обслуживаемым, они смотрят на меня как на инопланетянина: «Как мне сделать его проще? Как я могу заменить их, если блоки? » Хорошо, вот решение, которое отлично работает, учитывает Шаблон единой ответственности и облегчает чтение кода.

Чтобы лучше понять решение, представьте каждый из этих блоков if как правило проверки. Теперь пришло время смоделировать эти правила.

Сначала создайте интерфейс одним методом. В терминах Java 8 это называется функциональным интерфейсом , как показано ниже.

1
2
3
public interface RegistrationRule{
 void validate();
}

Теперь пришло время преобразовать каждую проверочную проверку в правило регистрации. Но прежде чем мы это сделаем, нам нужно решить небольшую проблему. Реализация нашего интерфейса должна быть в состоянии обрабатывать регистрационные данные, но, как вы видите, у нас есть разные типы данных. Итак, что нам нужно, это инкапсулировать регистрационные данные в один объект, подобный этому:

1
2
3
4
5
6
public class RegistrationData{
 private String name;
 private String email;
 private int age;
// Setters - Getters to follow
}

Теперь мы можем улучшить наш функциональный интерфейс:

1
2
3
public interface RegistrationRule{
void validate(RegistrationData regData);
}

и начать писать наш набор правил. Например, давайте попробуем реализовать проверку электронной почты.

1
2
3
4
5
public class EmailValidatationRule implements RegistrationRule{
 private static final String EMAIL_PATTERN =
 "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
 + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
 private final Pattern pattern = Pattern.compile(EMAIL_PATTERN);
1
2
3
4
5
6
@Override
 public void validate(RegistrationData regData) {
 if ( !pattern.matcher(regData.email).matches()) {
   throw new IllegalArgumentException("Email is not a valid email!");
 }
}

Понятно, что в приведенном выше классе мы выделили проверку электронной почты. Мы можем сделать то же самое для всех правил нашей первоначальной реализации. Теперь мы можем переписать наш метод register, чтобы использовать правила проверки.

01
02
03
04
05
06
07
08
09
10
List<RegistrationRule> rules = new ArrayList<>();
 rules.add(new EmailValidatationRule());
 rules.add(new EmailEmptinessRule());
 rules.add(new ForbiddenEmailDomainsRule());
 rules.add(new NameEmptinessRule());
 rules.add(new AlphabeticNameRule());
  
 for ( RegistrationRule rule : rules){
  rule.validate(regData);
 }

Чтобы сделать его еще лучше, мы можем создать класс Rules, используя шаблон Factory и статический метод get (), который будет возвращать список правил. И наша окончательная реализация будет выглядеть так

1
2
3
for ( RegistrationRule rule : Rules.get()){
  rule.validate(regData);
}

Сравнение начальной версии нашего метода регистрации с последней оставляет место для сомнений. Наша новая версия более компактна, удобочитаема и, конечно, более тестируема. Фактические проверки были перенесены в отдельные классы (которые также легко тестируются), и все методы делают только одно (старайтесь всегда помнить об этом).