Featured image of post Clean Code - A Software Engineer’s Perspective - Part II

Clean Code - A Software Engineer’s Perspective - Part II

In the first part of our exploration into clean code, we delved into the principles of what constitutes clean code, how to write it, and the importance of clean naming and structure. In this part, we'll focus on writing clean functions and methods, how to control structures well and handle errors. A cornerstone of maintainable and understandable code.

Functions & Methods

In order to write clean code and functions, it is important that we relect on what makes up a function. And for that, we should probable look at a function, for example, this dummy example add function here.

1
2
3
4
5
function add(n1, n2) {
    return n1 + n2;
};

add(5,7);

And just as a side note, when i say function and whaht i show throughtout this blog will also apply to methods since methods are really just functions inside of an object.
So this is a funcion or method, when we talk about clean code, both parts should be clean. Calling the function should be readable and also easy for us as a developer.
If we are calling a function, we want to have an easy time doing so. For example, the argument order should be clear. And of course, writing code in an existing function, maintaining existing functions should also be easy.

So the function body, the definition of a function, should also be readable. In this part, we are going to have a look at both aspects.

Keep the Number of Parameters Low

Functions with fewer parameters are easier to understand, test, and maintain. Ideally, a function should have zero to three parameters. More than that, and you should reconsider the design.

1
2
3
4
5
6
7
8
9
// Bad Example
public void createUser(String firstName, String lastName, String email, String address, String phoneNumber) {
    // Implementation
}

// Good Example
public void createUser(User user) {
    // Implementation
}

In the good example, we pass a User object instead of individual parameters, making the function call simpler and more intuitive.

Handling a Dynamic Number of Parameters

When you need to handle a dynamic number of parameters, use varargs or collections. So that you can have infinite parameters functions with more flexible.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Using Varargs
public void sendEmails(String... emails) {
    for (String email : emails) {
        // Send email
    }
}

// Using Collections
public void sendEmails(List<String> emails) {
    for (String email : emails) {
        // Send email
    }
}

Varargs and collections make the function more flexible and easier to use.

Functions Should Be Small and Do One Thing

A function should do one thing and do it well. This makes the function easier to understand, test, and maintain. If a function does more than one thing, consider splitting it into smaller functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// Bad Example
function renderContent(renderInformation) {
  const element = renderInformation.element;
  if (element === 'script' || element === 'SCRIPT') {
    throw new Error('Invalid element.');
  }

  let partialOpeningTag = '<' + element;

  const attributes = renderInformation.attributes;

  for (const attribute of attributes) {
    partialOpeningTag =
      partialOpeningTag + ' ' + attribute.name + '="' + attribute.value + '"';
  }

  const openingTag = partialOpeningTag + '>';

  const closingTag = '</' + element + '>';
  const content = renderInformation.content;

  const template = openingTag + content + closingTag;

  const rootElement = renderInformation.root;

  rootElement.innerHTML = template;
}

// Good Example
function renderContent(renderInformation) {
  const element = renderInformation.element;
  const rootElement = renderInformation.root;

  validateElementType(element);

  const content = createRenderableContent(renderInformation);

  renderOnRoot(rootElement, content);
}

function validateElementType(element) {
  if (element === 'script' || element === 'SCRIPT') {
    throw new Error('Invalid element.');
  }
}

function createRenderableContent(renderInformation) {
  const tags = createTags(
    renderInformation.element,
    renderInformation.attributes
  );
  const template = tags.opening + renderInformation.content + tags.closing;
  return template;
}

function renderOnRoot(root, template) {
  root.innerHTML = template;
}

function createTags(element, attributes) {
  const attributeList = generateAttributesList(attributes);
  const openingTag = buildTag({
    element: element,
    attributes: attributeList,
    isOpening: true,
  });
  const closingTag = buildTag({
    element: element,
    isOpening: false,
  });

  return { opening: openingTag, closing: closingTag };
}

function generateAttributesList(attributes) {
  let attributeList = '';
  for (const attribute of attributes) {
    attributeList = `${attributeList} ${attribute.name}="${attribute.value}"`;
  }

  return attributeList;
}

function buildTag(tagInformation) {
  const element = tagInformation.element;
  const attributes = tagInformation.attributes;
  const isOpeningTag = tagInformation.isOpening;

  let tag;
  if (isOpeningTag) {
    tag = '<' + element + attributes + '>';
  } else {
    tag = '</' + element + '>';
  }

  return tag;
}

In the detailed example, the “renderContent” function is broken down into smaller functions, each responsible for a single task. This improves readability, maintainability, and testability.

Stay DRY - Don’t Repeat Yourself

The principle of DRY, or “Don’t Repeat Yourself,” is a fundamental tenet of clean code. It aims to reduce the redundancy in code by ensuring that any piece of knowledge has a single, unambiguous representation within the system. By avoiding repetition, you make your code easier to maintain, understand, and extend.

 1
 2
 3
 4
 5
 6
 7
 8
 9
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
// Bad Example: Repeated Business Logic
public void processOrder(Order order) {
    double total = calculateTotal(order.getItems());
    applyDiscount(order);
    chargeCustomer(order);
    sendConfirmationEmail(order);
}

public void processReturn(Order order) {
    double total = calculateTotal(order.getItems());
    processRefund(order);
    updateInventory(order);
    sendReturnConfirmationEmail(order);
}

// Good Example: Modular Business Logic
public void processOrder(Order order) {
    processOrderSteps(order, this::chargeCustomer, this::sendConfirmationEmail);
}

public void processReturn(Order order) {
    processOrderSteps(order, this::processRefund, this::sendReturnConfirmationEmail);
}

private void processOrderSteps(Order order, Consumer<Order> paymentStep, Consumer<Order> notificationStep) {
    double total = calculateTotal(order.getItems());
    applyDiscount(order);
    paymentStep.accept(order);
    notificationStep.accept(order);
}

private void chargeCustomer(Order order) {
    // Logic to charge customer
}

private void processRefund(Order order) {
    // Logic to process refund
}

private void sendConfirmationEmail(Order order) {
    // Logic to send confirmation email
}

private void sendReturnConfirmationEmail(Order order) {
    // Logic to send return confirmation email
}

Understanding & Avoiding (Unexpected) Side Effects

Functions should avoid side effects, which are changes in state that affect other parts of the system. Side effects make functions harder to understand and test. Aim to make your functions pure, meaning they only depend on their input parameters and produce no side effects.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Bad Example
public void updateUserAge(User user, int newAge) {
    user.setAge(newAge);
    // Logs the change (side effect)
    logger.info("User age updated to " + newAge);
}

// Good Example
public User updateUserAge(User user, int newAge) {
    User updatedUser = new User(user);
    updatedUser.setAge(newAge);
    return updatedUser;
}

In the good example, the function returns a new user object instead of modifying the existing one, avoiding unexpected side effects.

Embrace Errors & Error Handling

In the journey of writing clean code, handling errors effectively is mostly important. Good error handling not only prevents applications from crashing but also makes them robust and user-friendly. In this part, we will discuss several key practices for embracing errors and handling them efficiently.

Creating More Error Guards

Error guards are checks you put in place to prevent erroneous states or actions in your code. These guards act as sentinels that validate conditions before proceeding with operations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public void processOrder(Order order) {
    if (order == null) {
        throw new IllegalArgumentException("Order cannot be null");
    }
    if (order.getItems().isEmpty()) {
        throw new IllegalArgumentException("Order must contain at least one item");
    }
    // Proceed with order processing
    processPaymentOrder(order);
    processCancelOrder(order);
}

public void processPaymentOrder(Order order) {
  /// process payment order
}

public void processCancelOrder(Order order) {
  // process cancel order
}

In this example, the error guards ensure that the order object is not null and contains items before processing. This proactive error checking prevents downstream errors and makes the code more robust.

Extracting Validation Code

Extracting validation code into separate methods or classes helps keep the main logic clean and focused on its primary responsibility. This separation of concerns also makes validation logic reusable and easier to test.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class OrderValidator {
    public void validate(Order order) {
        if (order == null) {
            throw new IllegalArgumentException("Order cannot be null");
        }
        if (order.getItems().isEmpty()) {
            throw new IllegalArgumentException("Order must contain at least one item");
        }
    }
}

public class OrderService {
    private OrderValidator validator = new OrderValidator();

    public void processOrder(Order order) {
        validator.validate(order);
        // Proceed with order processing
    }
}

Here, the OrderValidator class is responsible for validation, while the OrderService focuses on order processing. This makes both classes easier to understand and maintain.

Error Handling Is One Thing!

Each function or method should have a single responsibility, including handling errors. Mixing error handling with business logic can make the code convoluted and harder to maintain.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class PaymentService {
    public void processPayment(Payment payment) {
        try {
            // Payment processing logic
        } catch (PaymentProcessingException e) {
            handleError(e);
        }
    }

    private void handleError(PaymentProcessingException e) {
        // Error handling logic
        log.error("Payment processing failed: " + e.getMessage());
    }
}

In this example, processPayment handles payment processing, while handleError deals with errors. This separation of concerns keeps the code clean and focused.

Working with Default Parameters

Using default parameters can simplify function calls and reduce the likelihood of errors by providing default values when none are supplied.

1
2
3
4
5
6
7
8
function generateReport(reportType = "summary", format = "pdf") {
        // Generate report logic
        // no need to add any if here
}

generateReport("employee", "csv");
generateReport();
generateReport("profit");

Conclusion

Writing clean code is essential for creating maintainable, readable, and robust software. In this series, we’ve discussed various aspects of clean code:

  • Clean Functions & Methods: Keep functions small and focused on a single task. Limit the number of parameters, manage multiple values effectively, and avoid unexpected side effects. Always stay DRY (Don’t Repeat Yourself).
  • Embrace Errors & Error Handling: Implement error guards, extract validation logic, and handle errors separately from business logic. Use default parameters to simplify function calls and enhance code resilience.

By adhering to these principles, you can ensure your codebase remains efficient and easy to maintain. Stay tuned for more insights on advanced clean code practices, including dependency management and maintaining simplicity.

STAY TUNED TO ELEVATE YOUR CODING STANDARDS AND EMBRACE CLEANER!

HAPPY CODING!

Reference: https://udemy.com - Clean Code course by Academind

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy