Software Design Principles: Single Level of Abstraction
Single Level of Abstraction (SLA) is a software design principle that emphasizes the importance of having expressions at the same level of abstraction within a function, aiming to enhance readability. It also ensures that it conforms to the Single Responsibility (SR) principle.
What is Abstraction Level?
Every function performs an action, which often includes sub-actions. The level of these sub-actions indicates the abstraction level. That's essentially what it is.
Let's illustrate this with an example of a coffee machine.
If we want to buy a coffee from a machine, we put the money in, choose the type we want, wait for it to be prepared, and take it. These steps are at the first level of abstraction because they are the basic steps for this case.
These user interactions trigger other actions in the background. During the coffee preparation phase, the machine takes the cup, adds coffee, pours hot water, and shuffles. These steps are at the second level of abstraction (the sub-actions of the money insertion are the same way). They are actions that the user who wants to buy coffee should not focus on. It would be an unnecessary detail if you put the cup in the machine or stir the coffee yourself. But if there is no cup in the machine, then you need to check the relevant functionality.
Let's see the instruction sequence dummy code that violates the principle.
1getCoffee() {
2 checkMoney(); // abstraction lvl 2
3 addToBallance(); // abstraction lvl 2
4 chooseCoffeType(); // abstraction lvl 1
5 getGlass(); // abstraction lvl 2
6 putCoffee(); // abstraction lvl 2
7 putWater(); // abstraction lvl 2
8 shuffleGlass(); // abstraction lvl 2
9 giveCoffee(); // abstraction lvl 1
10}
We don't add the balance ourselves or check the money, right? So this is abstracted from the user. Let's refactor the code to add extra abstraction.
1getCoffee() {
2 getMoney(); // abstraction lvl 1
3 chooseCoffeType(); // abstraction lvl 1
4 prepareCoffee(); // abstraction lvl 1
5 giveCoffee(); // abstraction lvl 1
6}
Code Examples
calculateSum()
conforms to the SLA principle, only including the details of calculating the sum. It defines a variable that holds the calculation result, adds the other numbers to it and returns it.
1public int calculateSum(int[] numbers) {
2 int sum = 0;
3
4 for (int num : numbers) {
5 sum += num;
6 }
7
8 return sum;
9}
But
validateUser()
contains different abstraction levels. isValidEmail()
is at the second level of abstraction. The main purpose of the function is to check if the user is valid. However, when looking at the code, we also see the details of the validation process and error handling, indicating the presence of multiple abstraction levels. To compare their readability, take a closer look at the validateUser()
function.
1public boolean validateUser(User user) {
2 if (user.getName().isEmpty()) {
3 System.out.println("Name cannot be empty.");
4 return false;
5 }
6
7 if (user.getEmail().isEmpty()) {
8 System.out.println("Email cannot be empty.");
9 return false;
10 }
11
12 if (!isValidEmail(user.getEmail())) {
13 System.out.println("Invalid email format.");
14 return false;
15 }
16
17 if (user.getPassword().isEmpty()) {
18 System.out.println("Password cannot be empty.");
19 return false;
20 }
21
22 if (user.getPassword().length() < 8) {
23 System.out.println("Password must be at least 8 characters long.");
24 return false;
25 }
26
27 return true;
28}
The refactored version of
validateUser()
is as follows.
1public boolean validateUser(User user) {
2 if (!hasValidName(user)) { // name validation details abstracted
3 logError("Invalid name"); // logging details abstracted
4 return false;
5 }
6
7 if (!hasValidEmail(user)) { // email validation details abstracted
8 logError("Invalid email"); // logging details abstracted
9 return false;
10 }
11
12 if (!hasValidPassword(user)) { // şifre validation details abstracted
13 logError("Invalid password"); // logging details abstracted
14 return false;
15 }
16
17 return true
18}
Even if the amount of code increases with new functions, pay attention to how easy it is to read and how easy it is to maintain. For example, imagine you are checking for age verification in the validation phase:
- You examine the function.
- You quickly see the validation steps and notice that age verification is missing.
- If necessary, you create a validation function for age verification and integrate it into the code.
It's done. Since the abstraction is very sharp, you don't need to go through hundreds of lines of code to see which code verifies age by extracting password and email verification details.
Practice
Assume you have a function that calculates the total price of a product list, and take a minute to analyze why it violates the SLA principle. Try to refactor it yourself following the principle of No Pain, No Gain (my principle).
1public int calculateTotalCartPrice(CartItem[] cartItems) {
2 int totalPrice = 0;
3
4 for (CartItem cartItem : cartItems) {
5 totalPrice += cartItem.getPrice();
6
7 if (cartItem.isTaxable()) {
8 totalPrice += cartItem.getPrice() * 0.18;
9 }
10
11 if (cartItem.getCategory().equals("Electronics")) {
12 if (cartItem.getPrice() > 500) {
13 totalPrice -= 50;
14 }
15 }
16 }
17
18 return totalPrice;
19}
When we examine the function, we can see that it adds the product price to the total amount, adds the product tax to it and finally deducts the discount amount if available. We can split the code into three parts. Let's refactor it step by step:
-
The logic for calculating the product price within the loop in
is at a different level of abstraction than the rest of the code. Let's extract it into a separate function.calculateTotalCartPrice()
1public int calculateTotalCartPrice(CartItem[] cartItems) {
2 int totalPrice = 0;
3
4 for (CartItem cartItem : cartItems) {
5 totalPrice += calculateCartItemPrice(cartItem);
6 }
7
8 return totalPrice;
9}
10
11private int calculateCartItemPrice(CartItem cartItem) {
12 int cartItemPrice = cartItem.getPrice(); // abstraction level 1
13
14 if (cartItem.isTaxable()) {
15 cartItemPrice += cartItem.getPrice() * 0.18; // abstraction level 2
16 }
17
18 if (cartItem.getCategory().equals("Electronics")) {
19 if (cartItem.getPrice() > 500) { // abstraction level 2
20 cartItemPrice -= 50;
21 }
22 }
23
24 return cartItemPrice;
25}
-
We have successfully refactored the
function, but the new function still has different levels of abstraction. The main action of this function is to return the price of the product, but the calculation of tax and discount are sub-actions.calculateTotalCartPrice()
1public int calculateTotalCartPrice(CartItem[] cartItems) {
2 int totalPrice = 0;
3
4 for (CartItem cartItem : cartItems) {
5 totalPrice += calculateCartItemPrice(cartItem);
6 }
7
8 return totalPrice;
9}
10
11private int calculateCartItemPrice(CartItem cartItem) {
12 int cartItemPrice = cartItem.getPrice();
13
14 if (cartItem.isTaxable()) {
15 cartItemPrice += calculateTax(cartItem.getPrice());
16 }
17
18 if (isElectronicItem(cartItem)) {
19 cartItemPrice -= calculateDiscount(cartItem.getPrice());
20 }
21
22 return cartItemPrice;
23}
24
25private int isElectronicItem(CartItem cartItem) {
26 return cartItem.getCategory().equals("Electronics");
27}
28
29private int calculateTax(int price) {
30 return price * 0.18;
31}
32
33private int calculateDiscount(int price) {
34 return price > 500 ? 50 : 0;
35}
As a result of the refactor, we get a clean code and the blessings of future generations.
We want to write our code in a way that minimizes bugs and can be easily flexed in the future. Principles like Single Level of Abstraction also encourage writing clean code by refining the details.