Статьи

Чистый код: не смешивайте разные уровни абстракций

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

Многие люди связывают удобочитаемость кода с соглашениями о кодировании, такими как следование стандартным соглашениям об именах, закрытие файла, ресурсы БД и т. Д. Когда речь заходит о проверках кода, большинство людей сосредотачиваются только на этих тривиальных вещах, таких как проверка нарушений соглашения об именах, правильное освобождение ресурсов. в конечном итоге блок или нет.

Нужны ли нам «старшие ресурсы» в команде (я ненавижу называть человека ресурсом), чтобы делать такие вещи? Инструменты типа Findbugs, PMD, Checkstyle, Sonar могут сделать это для вас. Я согласен, что соблюдение стандартного соглашения об именах всеми членами команды — это хорошо. Но это не повышает читаемость кода.

Давайте возьмем простой пример. Я хотел бы реализовать сценарий перевода средств, и следующие правила для реализации:

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

Предположим, у нас есть следующая реализация для приведенного выше варианта использования:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class UglyMoneyTransferService
{
 public void transferFunds(Account source, Account target, BigDecimal amount, boolean allowDuplicateTxn) throws IllegalArgumentException, RuntimeException {
 Connection conn = null;
 try {
  conn = DBUtils.getConnection();
  PreparedStatement pstmt = conn.prepareStatement("Select * from accounts where acno = ?");
  pstmt.setString(1, source.getAcno());
  ResultSet rs = pstmt.executeQuery();
  Account sourceAccount = null;
  if(rs.next()) {
   sourceAccount = new Account();
   populate account properties from ResultSet
  }
  if(sourceAccount == null){
   throw new IllegalArgumentException("Invalid Source ACNO");
  }
  Account targetAccount = null;
  pstmt.setString(1, target.getAcno());
  rs = pstmt.executeQuery();
  if(rs.next()) {
   targetAccount = new Account();
   populate account properties from ResultSet
  }
  if(targetAccount == null){
   throw new IllegalArgumentException("Invalid Target ACNO");
  }
  if(!sourceAccount.isOverdraftAllowed()) {
   if((sourceAccount.getBalance() - amount) < 0) {
    throw new RuntimeException("Insufficient Balance");
   }
  }
  else {
   if(((sourceAccount.getBalance()+sourceAccount.getOverdraftLimit()) - amount) < 0) {
    throw new RuntimeException("Insufficient Balance, Exceeding Overdraft Limit");
   }
  }
  AccountTransaction lastTxn = .. ; JDBC code to obtain last transaction of sourceAccount
  if(lastTxn != null) {
   if(lastTxn.getTargetAcno().equals(targetAccount.getAcno()) && lastTxn.getAmount() == amount && !allowDuplicateTxn) {
   throw new RuntimeException("Duplicate transaction exception");ask for confirmation and proceed
   }
  }
  sourceAccount.debit(amount);
  targetAccount.credit(amount);
  TransactionService.saveTransaction(source, target,  amount);
 }
 catch(Exception e){
  logger.error("",e);
 }
 finally {
  try {
   conn.close();
  }
  catch(Exception e){
   Not everything is in your control..sometimes we have to believe in GODJamesGosling and proceed
  }
 }
}
}

Вышеприведенный код читабелен .. верно ?? … потому что:

  1. Мы следовали соглашениям об именах, таких как имена переменных в верблюжьей оболочке
  2. У нас есть все открытые скобки ({) в строке определения метода
  3. Мы закрыли соединение с БД в блоке finally
  4. мы зарегистрировали исключение вместо использования System.err.println ()
  5. и самое главное, он работает, как ожидалось.

Так читаемый и чистый код? На мой взгляд, абсолютно нет. В приведенном выше коде много проблем с точки зрения читабельности.

  1. Смешивание кода взаимодействия БД с бизнес-логикой
  2. Создание исключений IllegalArgumentException, RuntimeException и т. Д. Из бизнес-методов вместо бизнес-исключений
  3. Самое главное, код смешан с различными уровнями абстракций.

Позвольте мне объяснить, что я имею в виду под различными уровнями абстракций.

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

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

Но в приведенном выше коде все перемешано.

Читая код, вы начинаете смотреть на код JDBC, и ваш мозг работает в техническом режиме, а после получения объекта Account из ResultSet вы проверяете наличие нуля и выдает исключение, если оно равно нулю, что является бизнес-требованием. Поэтому вам сразу нужно переключиться на бизнес-режим и подумать: «Хорошо, если учетная запись недействительна, мы хотим немедленно прервать операцию».

Несмотря на то, что вам удалось переключиться между Техническим и Бизнес-режимами, как насчет улучшения одной подзадачи, такой как «Перевод средств считается дубликатом, только если он совпадает с последней транзакцией, которая произошла только через час». Чтобы сделать это усовершенствование, вы должны пройти весь метод, потому что вы не модульные свои подзадачи и нет разделения интересов.

Давайте перепишем описанный выше метод следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class FundTransferTxn
{
 private Account sourceAccount;
 private Account targetAccount;
 private BigDecimal amount;
 private boolean allowDuplicateTxn;
 setters & getters
}
 
public class CleanMoneyTransferService
{
 public void transferFunds(FundTransferTxn txn) {
  Account sourceAccount = validateAndGetAccount(txn.getSourceAccount().getAcno());
  Account targetAccount = validateAndGetAccount(txn.getTargetAccount().getAcno());
  checkForOverdraft(sourceAccount, txn.getAmount());
  checkForDuplicateTransaction(txn);
  makeTransfer(sourceAccount, targetAccount, txn.getAmount());
 }
 
 private Account validateAndGetAccount(String acno){
  Account account = AccountDAO.getAccount(acno);
  if(account == null){
   throw new InvalidAccountException("Invalid ACNO :"+acno);
  }
 }
 
 private void checkForOverdraft(Account account, BigDecimal amount){
  if(!account.isOverdraftAllowed()){
   if((account.getBalance() - amount) < 0) {
    throw new InsufficientBalanceException("Insufficient Balance");
   }
  }
  else{
   if(((account.getBalance()+account.getOverdraftLimit()) - amount) < 0){
    throw new ExceedingOverdraftLimitException("Insufficient Balance, Exceeding Overdraft Limit");
   }
  }
 }
 
 private void checkForDuplicateTransaction(FundTransferTxn txn){
  AccountTransaction lastTxn = TransactionDAO.getLastTransaction(txn.getSourceAccount().getAcno());
  if(lastTxn != null) {
   if(lastTxn.getTargetAcno().equals(txn.getTargetAccount().getAcno())
     && lastTxn.getAmount() == txn.getAmount()
     && !txn.isAllowDuplicateTxn()) {
    throw new DuplicateTransactionException("Duplicate transaction exception");
   }
  }
 }
 
 private void makeTransfer(Account source, Account target, BigDecimal amount){
  sourceAccount.debit(amount);
  targetAccount.credit(amount);
  TransactionService.saveTransaction(source, target,  amount);
 }
}

Вышеупомянутый улучшенный метод делает именно то, что делает первоначальная версия, но теперь он выглядит намного лучше, чем предыдущая версия.

  • Мы разделили всю задачу на подзадачи и реализовали каждую подзадачу отдельным методом.
  • Мы делегировали взаимодействие с БД в DAO
  • Мы бросаем бизнес-специфические исключения вместо исключений языка Java
  • В целом мы разделили уровни абстракций.

На первом уровне у нас самый высокий уровень абстракции в методе TransferFunds (FundTransferTxn txn). Рассматривая этот метод, мы можем понять, что мы делаем в рамках операции «Перевод средств», не сильно заботясь о реализации или технических деталях.

На втором уровне у нас есть методы реализации бизнес-логики checkForOverdraft, checkForDuplicateTransaction и т. Д., Которые выполняют бизнес-логику, опять же, не сильно заботясь о технических деталях.

На самом низком уровне у нас есть технические детали реализации в AccountDAO и TransactionDAO, которые содержат логику взаимодействия с БД.

Таким образом, читатель (будущий разработчик / сопровождающий) вашего кода может легко понять, что вы делаете на высоком уровне, и найти метод, который ему интересен.

Как я уже говорил ранее, если мы должны внести изменение, чтобы рассматривать транзакцию как дублирующую транзакцию, только если она произошла в течение часа, мы можем легко понять, что checkForDuplicateTransaction () — это та, которую мы должны рассмотреть и внести изменения.

Удачного кодирования!