Запах кода Катас — Грациозная обработка исключений

В компьютерном программировании запах кода — это любой признак в исходном коде программы, который может указывать на более глубокую проблему. Определение того, что является или не является запахом кода, субъективно и зависит от языка, разработчика и методологии разработки. — википедия

Запахи кода часто являются симптомами плохого проектирования или выбора реализации.

Давайте разберем ката «Запах кода» в этой статье блога.

Рассмотрим бизнес-сценарий, в котором необходимо сделать HTTP-вызов к сервису для обновления документа, если он существует, или создания нового документа, если он не существует.

Нижеприведенный класс имеет только один публичный метод createOrupdateDocument, а все остальные методы являются приватными и не видны другим классам.

public class CodeSmellHttpClient {
  public static final Logger LOG = LoggerFactory.getLogger(CodeSmellHttpClient.class);
  private final String baseUrl = "https://some.service.com/documents/";

  private HttpClient httpClient = HttpClients.createDefault();

  private String bearerToken = "some bearer token";

  public void createOrUpdateDocument(String requestBody, String documentId) {
    if (this.isDocumentExistingWithId(documentId)) {
      this.updateDocument(requestBody, documentId);
    } else {
      this.createDocument(requestBody);
    }
  }

  private void createDocument(String requestBody) {
    try {
      HttpPost httpPostRequest = new HttpPost(baseUrl);
      StringEntity requestEntity = new StringEntity(requestBody, ContentType.APPLICATION_JSON);

      httpPostRequest.setEntity(requestEntity);
      httpPostRequest.addHeader(getAuthorizationHeader());

      HttpResponse response = httpClient.execute(httpPostRequest);
      LOG.info(response.toString());
    } catch (IOException e) {
      LOG.error(e.getMessage());
      e.printStackTrace();
    }
  }

  private void updateDocument(String requestBody, String documentId) {
    final String updateDocumentUrl = baseUrl.concat(documentId);

    HttpPatch httpPatchRequest = new HttpPatch(updateDocumentUrl);
    StringEntity requestEntity = new StringEntity(requestBody, ContentType.APPLICATION_JSON);

    httpPatchRequest.setEntity(requestEntity);
    httpPatchRequest.setHeader(getAuthorizationHeader());
    try {
      HttpResponse response = httpClient.execute(httpPatchRequest);
      LOG.info(response.toString());
    } catch (IOException e) {
      LOG.error(e.getMessage());
      e.printStackTrace();
    }
  }

  private boolean isDocumentExistingWithId(String documentId) {
    final String getTemplateUrl = baseUrl.concat(documentId);

    HttpGet httpGetRequest = new HttpGet(getTemplateUrl);
    httpGetRequest.setHeader(getAuthorizationHeader());
    HttpResponse response = null;

    try {
      response = httpClient.execute(httpGetRequest);
      LOG.info(response.toString());
    } catch (IOException e) {
      LOG.error(e.getMessage());
      e.printStackTrace();
    }

    if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
      LOG.info(documentId + "already exists");
      return true;
    }

    LOG.info(documentId + "does not exists");
    return false;

  }

  private Header getAuthorizationHeader() {
    return new BasicHeader(HttpHeaders.AUTHORIZATION,
        "Bearer " + bearerToken);
  }

}
Вход в полноэкранный режим Выход из полноэкранного режима

Код выше: https://gist.github.com/akhil-ghatiki/98339ea96a6a39876aa3745f9cf185cb#file-codesmell1-java

Давайте пока немного отвлечемся от запахов кода и посмотрим на приведенный выше класс. Почему этот класс должен заботиться о создании HttpClient? Как вы можете протестировать этот класс для модульного тестирования? Что вы можете протестировать для модульного тестирования?

Рассмотрите возможность переноса создания HttpClient в другой класс и передачи его этому классу в качестве параметра конструктора, как показано ниже, или в качестве автозависимого боба, если вы используете фреймворк Spring.

private HttpClient httpClient;

public CodeSmellHttpClient(HttpClient httpClient, String bearerToken) {
  this.httpClient = httpClient;
  this.bearerToken = bearerToken;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Это поможет вам сымитировать создание HttpClient и проверить вызовы httpClient.execute(httpGetRequest), httpClient.execute(httpPostRequest), httpClient.execute(httpPatchRequest) соответственно, когда get, post и patch запросы выполняются через приватные методы.

ProTip: Если вы следуете принципу разработки, управляемой тестами, вы не столкнетесь с большинством запахов кода, как в приведенной выше реализации. Разработка на основе тестов заставляет вас писать более чистый код во многих отношениях. Да здравствует TDD!

Теперь просто посмотрите на метод isDocumentExistingWithId(String documentId) в приведенном выше коде, потратьте минуту и попробуйте перечислить запахи кода в нем.

Что произойдет, если возникнет исключение?

В счастливом сценарии здесь все работает гладко. Но в случае исключения метод ловит исключение и регистрирует его, а затем оператор if после catch проверяет код состояния в ответ. Так и до появления в консоли огромной красной строки журнала NullPointerException при возникновении исключения недалеко.

Хорошо, теперь давайте сделаем некоторые изменения, чтобы справиться со сценарием исключения, когда в этом методе возникает исключение. Как насчет того, чтобы изменить метод на следующий ? — переместить условие if в блок try.

private boolean isDocumentExistingWithId(String documentId) {  final String getTemplateUrl = baseUrl.concat(documentId);

  HttpGet httpGetRequest = new HttpGet(getTemplateUrl);
  httpGetRequest.setHeader(getAuthorizationHeader());
  HttpResponse response = null;

  try {
    response = httpClient.execute(httpGetRequest);
    LOG.info(response.toString());    if(response.getStatusLine().getStatusCode()==HttpStatus.SC_OK){
      LOG.info(documentId + "already exists");
      return true;
    }
  } catch (IOException e) {
    LOG.error(e.getMessage());
    e.printStackTrace();
  }
  LOG.info(documentId + "does not exists");
  return false;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Кажется, это смягчает проблему, которую мы обсуждали некоторое время назад. Но подождите, мы посмотрели на вызывающий метод этого метода? Посмотрите на createOrUpdateDocument(String requestBody, String documentId) в примере кода. (Извините, что заставил вас прокручивать страницу так много раз :p)

createDocument(newRequest) выполняется, потому что мы возвращаем false, когда исключение поймано в isDocumentExistingWithId(String documentId), а это вызывает метод createDocument. Мы не хотим, чтобы это произошло.

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

.

.

.

.

.

.

.

.
Если выброс исключения резонирует в коридорах вашего мозга, да, вы правы!

Выбросьте исключение в isDocumentExistingWithId(String documentId) вместо того, чтобы обрабатывать его. Позвольте вызывающему методу решать, что делать с этим исключением. Это то, что я называю изящной обработкой исключения. В своих реализациях относитесь к исключениям как к гражданам первого класса. Не игнорируйте их. Функциональное программирование делает удивительную работу по обращению с исключениями как с гражданами первого класса. Я не буду подробно рассказывать о функциональном программировании, поскольку это выходит за рамки данной статьи.

ProTip: Относитесь к исключениям как к гражданам первого класса в своих реализациях.

Ниже приведена реализация броска исключения, о котором шла речь.

public class CodeSmellHttpClient {

  public static final Logger LOG = LoggerFactory.getLogger(CodeSmellHttpClient.class);
  private final String baseUrl = "https://some.service.com/documents/";

  private HttpClient httpClient;

  private String bearerToken = "some bearer token";

  public CodeSmellHttpClient(HttpClient httpClient, String bearerToken) {
    this.httpClient = httpClient;
    this.bearerToken = bearerToken;
  }

  public void createOrUpdateDocument(String requestBody, String documentId) {
    boolean isDocumentExisting;
    try {
      isDocumentExisting = isDocumentExistingWithId(documentId);
    } catch (IOException e) {
      LOG.error("Unable to get template details", e);
      throw new RuntimeException("Unable to get template details", e);
    }
    if (isDocumentExisting) {
      this.updateDocument(requestBody, documentId);
    } else {
      this.createDocument(requestBody);
    }
  }

  private void createDocument(String requestBody) {

    try {

      HttpPost httpPostRequest = new HttpPost(baseUrl);
      StringEntity requestEntity = new StringEntity(requestBody, ContentType.APPLICATION_JSON);

      httpPostRequest.setEntity(requestEntity);
      httpPostRequest.addHeader(getAuthorizationHeader());

      HttpResponse response = httpClient.execute(httpPostRequest);
      LOG.info(response.toString());
    } catch (IOException e) {
      LOG.error(e.getMessage());
      e.printStackTrace();
    }
  }

  private void updateDocument(String requestBody, String documentId) {
    final String updateDocumentUrl = baseUrl.concat(documentId);

    HttpPatch httpPatchRequest = new HttpPatch(updateDocumentUrl);
    StringEntity requestEntity = new StringEntity(requestBody, ContentType.APPLICATION_JSON);

    httpPatchRequest.setEntity(requestEntity);
    httpPatchRequest.setHeader(getAuthorizationHeader());
    try {
      HttpResponse response = httpClient.execute(httpPatchRequest);
      LOG.info(response.toString());
    } catch (IOException e) {
      LOG.error(e.getMessage());
      e.printStackTrace();
    }
  }

  private boolean isDocumentExistingWithId(String documentId) throws IOException {
    final String getTemplateUrl = baseUrl.concat(documentId);

    HttpGet httpGetRequest = new HttpGet(getTemplateUrl);
    httpGetRequest.setHeader(getAuthorizationHeader());

    HttpResponse response = httpClient.execute(httpGetRequest);
    LOG.info(response.toString());

    if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
      LOG.info(documentId + "already exists");
      return true;
    }
    LOG.info(documentId + "does not exists");
    return false;
  }

  private Header getAuthorizationHeader() {
    return new BasicHeader(HttpHeaders.AUTHORIZATION,
        "Bearer " + bearerToken);
  }

}
Вход в полноэкранный режим Выход из полноэкранного режима

Код выше: https://gist.github.com/akhil-ghatiki/37594c5852e7293a2d97b2e0028aa6b3#file-codesmell2-java

Относитесь к исключениям хорошо, и они будут относиться к вам лучше.

Я намеренно оставил несколько запахов кода без внимания. Если вы обнаружите их, напишите в комментариях.

Эта статья первоначально опубликована на: https://akhil-ghatiki.github.io/#/code-smell-katas-graceful-exception-handling

Оцените статью
Procodings.ru
Добавить комментарий