Единый уровень абстракции (SLA) — это принцип проектирования программного обеспечения, который подчеркивает важность наличия выражений на одном уровне абстракции внутри функции для повышения удобочитаемости. Это также гарантирует соответствие принципу Single Responsibility (SR).

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

Что такое уровень абстракции?

Каждая функция выполняет действие, которое часто включает в себя поддействия. Уровень этих поддействий указывает уровень абстракции. Вот что это такое.

Проиллюстрируем это на примере кофемашины.

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

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

Давайте посмотрим на фиктивный код последовательности инструкций, который нарушает этот принцип.

getCoffee() {
  checkMoney();      // abstraction lvl 2
  addToBallance();   // abstraction lvl 2
  chooseCoffeType(); // abstraction lvl 1
  getGlass();        // abstraction lvl 2
  putCoffee();       // abstraction lvl 2
  putWater();        // abstraction lvl 2
  shuffleGlass();    // abstraction lvl 2
  giveCoffee();      // abstraction lvl 1
}

Мы не добавляем баланс сами и не проверяем деньги, верно? Таким образом, это абстрагируется от пользователя. Давайте рефакторим код, чтобы добавить дополнительную абстракцию.

getCoffee() {
  getMoney();        // abstraction lvl 1
  chooseCoffeType(); // abstraction lvl 1
  prepareCoffee();   // abstraction lvl 1
  giveCoffee();      // abstraction lvl 1
}

Примеры кода

calculateSum() соответствует принципу SLA, включая детали расчета суммы. Он определяет переменную, которая содержит результат вычисления, добавляет к ней другие числа и возвращает их.

public int calculateSum(int[] numbers) {
  int sum = 0;
 
  for (int num : numbers) {
   sum += num;
  }
 
  return sum;
}

Но validateUser() содержит разные уровни абстракции. isValidEmail() находится на втором уровне абстракции. Основная цель функции — проверить, является ли пользователь действительным. Однако при просмотре кода мы также видим детали процесса проверки и обработки ошибок, что указывает на наличие нескольких уровней абстракции. Чтобы сравнить их удобочитаемость, присмотритесь к функции validateUser().

public boolean validateUser(User user) {
  if (user.getName().isEmpty()) {
    System.out.println("Name cannot be empty.");
    return false;
  }
 
  if (user.getEmail().isEmpty()) {
    System.out.println("Email cannot be empty.");
    return false;
  }
 
  if (!isValidEmail(user.getEmail())) {
    System.out.println("Invalid email format.");
    return false;
  }
 
  if (user.getPassword().isEmpty()) {
    System.out.println("Password cannot be empty.");
    return false;
  }
 
  if (user.getPassword().length() < 8) {
    System.out.println("Password must be at least 8 characters long.");
    return false;
  }
 
  return true;
}

Рефакторинговая версия validateUser() выглядит следующим образом.

public boolean validateUser(User user) {
  if (!hasValidName(user)) { // name validation details abstracted
    logError("Invalid name"); // logging details abstracted
    return false;
  }
 
  if (!hasValidEmail(user)) { // email validation details abstracted
    logError("Invalid email"); // logging details abstracted
    return false;
  }
 
  if (!hasValidPassword(user)) { // şifre validation details abstracted
    logError("Invalid password"); // logging details abstracted
    return false;
  }
 
  return true
}

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

  • Вы исследуете функцию.
  • Вы быстро видите шаги проверки и замечаете, что проверка возраста отсутствует.
  • При необходимости вы создаете функцию проверки возраста и интегрируете ее в код.

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

Упражняться

Предположим, у вас есть функция, которая вычисляет общую стоимость списка продуктов, и потратьте минуту, чтобы проанализировать, почему она нарушает принцип SLA. Попробуйте провести рефакторинг самостоятельно, следуя принципу Нет боли, нет выгоды (NPNG).

public int calculateTotalCartPrice(CartItem[] cartItems) {
  int totalPrice = 0;
 
  for (CartItem cartItem : cartItems) {
    totalPrice += cartItem.getPrice();
 
    if (cartItem.isTaxable()) {
      totalPrice += cartItem.getPrice() * 0.18;
    }
 
    if (cartItem.getCategory().equals("Electronics")) {
      if (cartItem.getPrice() > 500) {
        totalPrice -= 50;
      }
    }
  }
 
  return totalPrice;
}

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

  • Логика вычисления цены продукта в цикле calculateTotalCartPrice() находится на другом уровне абстракции, чем остальная часть кода. Выделим его в отдельную функцию.

Если функция содержит цикл, вероятно, она содержит фрагмент кода, нарушающий единый уровень абстракции.

public int calculateTotalCartPrice(CartItem[] cartItems) {
  int totalPrice = 0;
 
  for (CartItem cartItem : cartItems) {
    totalPrice += calculateCartItemPrice(cartItem);
  }
 
  return totalPrice;
}
 
private int calculateCartItemPrice(CartItem cartItem) {
  int cartItemPrice = cartItem.getPrice();       // abstraction lvl 1
 
  if (cartItem.isTaxable()) {
    cartItemPrice += cartItem.getPrice() * 0.18; // abstraction lvl 2
  }
 
  if (cartItem.getCategory().equals("Electronics")) { 
    if (cartItem.getPrice() > 500) {             // abstraction lvl 2
      cartItemPrice -= 50;
    }
  }
 
  return cartItemPrice;
}
  • Мы успешно провели рефакторинг функции calculateTotalCartPrice(), но новая функция по-прежнему имеет другие уровни абстракции. Основное действие этой функции — возврат цены товара, но расчет налога и скидки — это поддействия.

Если вы используете глагол, описывая, что делает ваш код, есть фрагмент кода, который вам нужно извлечь в функцию. Давайте посмотрим на пример выше:

- Получить цену товара (это действие уже является причиной создания этой функции)

- Рассчитать налог (должен быть извлечен для работы)

- Рассчитать скидку (должен быть извлечен для работы)

- Возвращаем окончательно цену (это действие уже является причиной создания этой функции)

public int calculateTotalCartPrice(CartItem[] cartItems) {
  int totalPrice = 0;
 
  for (CartItem cartItem : cartItems) {
    totalPrice += calculateCartItemPrice(cartItem);
  }
 
  return totalPrice;
}
 
private int calculateCartItemPrice(CartItem cartItem) {
  int cartItemPrice = cartItem.getPrice();
 
  if (cartItem.isTaxable()) {
    cartItemPrice += calculateTax(cartItem.getPrice());
  }
 
  if (isElectronicItem(cartItem)) {
    cartItemPrice -= calculateDiscount(cartItem.getPrice());
  }
 
  return cartItemPrice;
}
 
private int isElectronicItem(CartItem cartItem) {
  return cartItem.getCategory().equals("Electronics");
}
 
private int calculateTax(int price) {
  return price * 0.18;
}
 
private int calculateDiscount(int price) {
  return price > 500 ? 50 : 0;
}

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

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

Оставаться здоровым.

Первоначально опубликовано на https://www.enesbaspinar.com 29 июня 2023 г.