Можете ли вы определить плохой дизайн, когда вы его видите? Можете ли вы сказать, что здесь не так?
if (!response.getStatusCode().is2xxSuccessful()) {
logError(response.getBody());
throwException(response.getStatusCode());
}
Приведенный выше фрагмент является примером шаблона, который мы часто видим в дикой природе:
if(someObject.getSomeAttribute()) {
doSomethingWith(someObject);
} else {
doSomethingElse(someObject);
}
Почему меня это должно волновать?
Чтобы мы могли понять, в чем заключаются проблемы и почему это плохой дизайн, давайте взглянем на полную картину. Вот откуда взялся этот фрагмент:
public class PaymentProcessorClient {
// some code removed for brevity's sake
public Sale processPayment(Payment payment) {
ResponseEntity response = rest.postForEntity("https://payment.com/payment", payment, Sale.class);
if (!response.getStatusCode().is2xxSuccessful()) {
logError(response.getBody());
throwException(response.getStatusCode());
}
logSuccessfulPayment(payment, response.getBody());
return response.getBody();
}
}
Этот класс обычно называют клиентом , его цель – общаться с какой-либо внешней службой. В данном конкретном случае он отвечает за создание платежа на вымышленном payment.com веб-сервис.
(Кстати, это реальный код из реального продукта, над которым сейчас работает моя команда. Я просто внес некоторые незначительные изменения, чтобы сохранить личность нашего клиента и не подвергать его никакому риску. Я также удалил некоторый код, чтобы сэкономить место. Эти изменения не должны мешать пониманию концепции, которую я пытаюсь здесь объяснить.)
Давайте подробнее рассмотрим процесс оплаты метод. В первой строке находится основная функциональность: она выполняет вызов службы и сохраняет ответ в переменной response .
ResponseEntityresponse = rest.postForEntity("https://payment.com/payment", payment, Sale.class);
Затем он просматривает ответ и, если создание платежа не было успешным, он регистрирует ошибку и выдает исключение:
if (!response.getStatusCode().is2xxSuccessful()) {
logError(response.getBody());
throwException(response.getStatusCode());
}
Если создание платежа прошло нормально, он регистрирует результат и возвращает текст ответа.
logSuccessfulPayment(payment, response.getBody()); return response.getBody();
Этот код работает, но, как я уже сказал, он был бы лучше разработан. Я собираюсь показать, почему и как.
Это приводит к дублированию
В нашем примере каждый раз, когда вы звоните во внешнюю службу, вы должны проверять ответ и делать что-то, основываясь на том, как выглядит этот ответ. Если у вас есть 3 операции, такие как “произвести платеж”, “аннулировать платеж” и “вернуть деньги”, вы бы повторили эту структуру 3 раза.
Дублирование кода само по себе плохо, но реальная проблема заключается в дублировании идеи или концепции – основываясь на том, как выглядит ответ, сделайте с ним что-нибудь . Эта концепция выражена в нескольких местах вашего кода, и из-за этого, если она изменится или изменится реализация, вам необходимо изменить все места, где она выражена, что очень неэффективно и подвержено ошибкам.
Дублирование также затрудняет повторное использование кода, поскольку концепция не изолирована и выражена одним уникальным способом и в одном месте.
Это увеличивает сцепление
Когда у нас есть что-то вроде:
response.getStatusCode().is2xxSuccessful()
Класс Клиент платежной системы должен знать, как ответ и объекты Код состояния реализованы. Он должен знать, что у объекта response есть метод getStatusCode и что у объекта StatusCode есть 2 x x успешных метода. Это знание о том, как реализуются другие объекты , является формой связи .
Что произойдет, если реализация одного из этих объектов изменится? Что делать, если метод 2 x x успешен устареет и будет заменен на создан 201 или что-то подобное? Вам придется зайти в свой Клиент платежной системы код и изменить его. Вам придется изменить один класс по другой причине, кроме изменения основной логики или ответственности класса.
Эта чрезмерная и ненужная связь (плюс проблема дублирования, о которой я говорил выше) имеет каскадные эффекты, из-за которых одно изменение в одном классе вызывает волну изменений во всей вашей кодовой базе. Нетрудно понять, почему это опасно и крайне нежелательно.
Это загромождает код
Давайте вспомним, что такое Клиент платежной системы и процесс оплаты способ, который предполагается выполнить: поговорите с внешней платежной службой и произведите платеж.
Теперь обратите внимание на весь код, который не является строго платежом за услугу.
public Sale processPayment(Payment payment) {
ResponseEntity response = rest.postForEntity("https://payment.com/payment", payment, Sale.class);
if (!response.getStatusCode().is2xxSuccessful()) {
logError(response.getBody());
throwException(response.getStatusCode());
}
logSuccessfulPayment(payment, response.getBody());
return response.getBody();
}
У нас есть код, изучающий ответ, информацию о регистрации кода и исключения, вызывающие код. Все это должно произойти, но они больше окружают или поддерживают основную идею этого класса. Их присутствие также затрудняет чтение и понимание идеи платежного процессора Client.processPayment, усложняя и удорожая обслуживание этого кода.
Как вы можете сделать это лучше?
Решение здесь состоит в том, чтобы разделить две концепции, которые запутаны:
Концепция 1: выполнение вызова внешней службы;
Концепция 2: действуйте на основе ответа службы.
Первая концепция реализуется с помощью Клиент платежной системы обрабатывает платеж способом, нам все еще нужно найти дом для второго. Поскольку это предполагает изучение ответа и на основе этого выполнение чего–либо с ответом , реализация этой концепции объектом ответа или чем-то подобным звучит разумно. Давайте посмотрим, как это будет выглядеть.
Решение
Первое, что нам нужно сделать, это переименовать объект Продажа в Продажи В . До сих пор то, что мы называли продажей, – это только то, что внешняя служба отправляет нам обратно после того, как мы произведем платеж. Это просто структура данных, поэтому имеет смысл переименовать ее Продажи В .
public SaleDTO processPayment(Payment payment) {
ResponseEntity response = rest.postForEntity("https://payment.com/payment", payment, SaleDTO.class);
if (!response.getStatusCode().is2xxSuccessful()) {
logError(response.getBody());
throwException(response.getStatusCode());
}
logSuccessfulPayment(payment, response.getBody());
return response.getBody();
}
Продажи В просто содержат данные, которые мы получаем от сервиса. Нам нужен реальный объект, который реализует концепцию продажи и будет вести себя так, как мы хотим, – это было ранее сделано PaymentProcessorClient . Давайте создадим новый класс Продажа , и это будет то, что мы возвращаем из процесс оплаты способ. Продажа будет построена с объектом ResponseEntity и объектом Оплаты .
public Sale processPayment(Payment payment) {
ResponseEntity response = rest.postForEntity("https://payment.com/payment", payment, SaleDTO.class);
if (!response.getStatusCode().is2xxSuccessful()) {
logError(response.getBody());
throwException(response.getStatusCode());
}
logSuccessfulPayment(payment, response.getBody());
return new Sale(payment, response);
}
Затем мы переносим поведение, которое ранее было на Клиенте платежной системы , на Продажу .
public class Sale {
public Sale(Payment payment, ResponseEntity saleResponse) {
this.saleResponse = saleResponse;
if (isResponseUnsuccessful()) {
logError(saleResponseBody());
throw new Exception(saleResponseStatusCode());
}
logSuccessfulPayment(payment, saleResponseBody());
}
private SaleDTO saleResponseBody() {
return this.saleResponse.getBody();
}
private HttpStatusCode saleResponseStatusCode() {
return this.saleResponse.getStatusCode();
}
private boolean isResponseUnsuccessful() {
return !saleResponseStatusCode().is2xxSuccessful();
}
}
Последний шаг – очистить Клиент платежной системы ‘ы обрабатывают платеж способ:
public Sale processPayment(Payment payment) {
ResponseEntity response = rest.postForEntity("https://payment.com/payment", payment, SaleDTO.class);
return new Sale(response, payment);
}
Мы применили здесь Скажи, не спрашивай принцип. Вместо того, чтобы спрашивать объект о чем-то, а затем что-то делать с объектом или просить его что-то сделать, мы просто говорим объекту, чего мы хотим, и он должен позаботиться об этом. В нашем случае мы просто сообщаем объекту о его существовании!
Теперь у нас есть чистый Клиент платежной системы и a умный _ Продажа _ объект, который знает, что делать когда что-то идет не так. Давайте посмотрим, как этот дизайн решает описанные нами проблемы.
Стал ли дизайн лучше сейчас?
Необходимость в дублировании концепции – реагировании на плохой ответ – отпала. Он реализован классом Продажа , который создан для отслеживания реакции на все операции во внешней службе, такие как возврат и аннулирование платежей.
Связь между Клиентом Платежной системы и Ответ и Код статуса классы исчезли. Теперь они связаны с классом Продажа , что мне кажется лучше, потому что они больше связаны друг с другом.
Мы сделали эти классы немного менее связанными, изолировав зависимость от частных методов с помощью метода SelfEncapsulation . Проверить тело ответа на продажу , Код ответа на продажу и является ли Ответ Неудачным методы.
Некоторая связь всегда будет существовать, поскольку объекты должны что-то знать друг о друге, чтобы сотрудничать и работать вместе.
Код также выглядит более чистым и понятным, так как мы переместили его в более подходящие места.
Легко обнаружить, легко исправить, большое влияние
Каждый раз, когда вы видите этот шаблон:
if(someObject.getSomeAttribute()) {
doSomethingWith(someObject);
} else {
doSomethingElse(someObject);
}
Есть очень хороший шанс, что есть возможность для улучшения дизайна.
Такая конструкция приводит к дублированию, ненужной связи и загромождению кода.
Определение некоторой базовой концепции и перемещение ее в соответствующий объект позволит вам создать код, который будет проще, дешевле и приятнее писать и поддерживать.
Сообщение Решение проблемы плохого дизайна в реальном мире путем применения принципа “Скажи, не спрашивай” появилось первым на Графический интерфейс Froes .
Оригинал: “https://dev.to/guifroes/solving-real-world-bad-design-by-applying-the-tell-don-t-ask-principle-607”