Skip to main content

Developer Guidelines

Version Manager

Akvo is using GitHub as main version control system. By using GitHub, we can ensure that our code-base is well-managed and that changes are thoroughly reviewed and approved before they are added to the code-base, ultimately improving the quality and stability of our software. Here are the most important rules of that have to be consider:

Branch Protection Rules
  • Require pull request reviews before merging: This rule requires that all changes to a branch be submitted as a pull request, and that at least one other team member approves the changes before they can be merged into the main branch.

  • Require status checks to pass before merging: This rule requires that certain conditions be met before a pull request can be merged. For example, we require that all tests pass in the CI and certain code quality coverage metrics are met.

  • Require a minimum number of reviewers (at least one reviewers): This helps to ensure that changes are thoroughly reviewed before merging.
  • Restrict who can push to the branch: This rule limits who can make changes to a branch, helping to prevent accidental or unauthorized changes. 
Branch Naming

It's important to have a clear and consistent naming convention for your branches. 

A good naming convention for Feature Branches is to use the following format: feature/<issue_number>-<issue_description>. For example, feature/13-backend-test-setup.

Here's what each part of the naming convention means:

  • feature/: This is a prefix that identifies the branch as a feature branch.

  • <issue_number>: This is the number of the issue or task that the branch is related to.

  • <issue_description>: This is a brief description of the issue or task. It should be short but descriptive enough to give an idea of what the branch is about.

Using this naming convention makes it easy to identify which branches are related to which issues or tasks. It also helps to keep our branches organized and easy to manage.

Pull Request

Here's a step-by-step guide to creating a pull request:

  1. Create a draft pull request: Instead of immediately creating a regular pull request, create a draft pull request. This signals to reviewers or other contributors that the changes are still in progress and not yet ready for review. If its defined in Asana board, you can add the draft PR to there early.
  2. Add a description: In the description of your draft pull request, briefly explain what the changes are about and which issue / tasks you are assigned to. Be as clear and detailed as possible to help reviewers understand the scope of your changes.
  3. Continue working: Once you've created your draft pull request, continue working on your changes until you are satisfied that they are ready for review.
  4. Update the pull request: Once your changes are complete and ready for review, update the pull request to remove the draft status and request a review from the project maintainers. 

Make sure that all the checks are not failed.

draft-PR.png


Code Quality Standard

These standards is defined by Repo maintainer / Team Lead, it should follow best practices and established coding conventions. There are many aspects to code quality, but some common factors include:

  1. Readability: The code should be easy to understand and follow, with clear and consistent formatting and naming conventions.
  2. Maintainability: The code should be modular and easy to modify, with clear separation of concerns and a well-defined structure.
  3. Efficiency: The code should be optimized for performance and resource usage, with efficient algorithms and data structures.
  4. Robustness: The code should handle errors and unexpected inputs gracefully, with appropriate error handling and testing.
  5. Security: The code should be designed with security in mind, with appropriate measures to protect against potential vulnerabilities and attacks.

Example python code that following code quality standards:

def calculate_average(numbers):
    if not isinstance(numbers, list):
        raise TypeError("Input must be a list of numbers.")
        
    if len(numbers) == 0:
        return 0
        
    total = sum(numbers)
    return total / len(numbers)

This code is a function that calculates the average of a list of numbers. It follows some good coding practices, such as:

  • Checking the input parameter to make sure it's a list before proceeding
  • Handling the case where the input list is empty
  • Using descriptive variable names that make the code easier to understand
  • Using Python's built-in functions like isinstance and sum to write concise and readable code
  • Raising a meaningful exception when the input is not of the expected type

On the other hand, here's an example of bad code that doesn't follow code quality standards:

def avg(num1, num2, num3):
    if not isinstance(num1, (int, float)):
        return "num1 must be a number"
    if not isinstance(num2, (int, float)):
        return "num2 must be a number"
    if not isinstance(num3, (int, float)):
        return "num3 must be a number"

    sum = num1 + num2 + num3
    average = sum / 3
    return average

This code also calculates the average of three numbers, but it's written in a way that violates good coding practices, such as:

  • Hard-coding the number of input parameters, which would require changing the code if we wanted to calculate the average of more or fewer numbers
  • Returning a string message instead of raising an exception when the input is not of the expected type
  • Using a variable name sum that's the same as a built-in Python function, which can cause confusion and errors
  • Not handling cases where the input values might be invalid or lead to errors

There are also several principles that are widely recognized as important for writing high-quality code. Here are some of the most important ones:

SOLID

SOLID is an acronym for a set of principles that were developed to guide object-oriented design. The principles are:

  1. Single Responsibility Principle: Each class should have a single responsibility.
  2. Open-Closed Principle: Classes should be open for extension but closed for modification.
  3. Liskov Substitution Principle: Sub-types should be substitutable for their base types.
  4. Interface Segregation Principle: Clients should not be forced to depend on interfaces they don't use.
  5. Dependency Inversion Principle: High-level modules should not depend on low-level modules; both should depend on abstractions.

Code example:

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * (self.radius ** 2)

class Square:
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side * self.side

shapes = [Circle(5), Square(10)]
total_area = sum(shape.area() for shape in shapes)

This code defines two classes, Circle and Square, each with a single responsibility of calculating its own area. This adheres to the Single Responsibility Principle of SOLID. Both classes use a common interface (the area() method) that makes them interchangeable, which adheres to the Liskov Substitution Principle. The code is also open to extension (adding new shapes) but closed to modification, which adheres to the Open-Closed Principle.

Example of code that does not follow SOLID:

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def save(self):
        # Code to save user to database
        pass

class UserManager:
    def __init__(self):
        self.users = []

    def add_user(self, name, email):
        user = User(name, email)
        self.users.append(user)
        user.save()

This code defines two classes, User and UserManager. The User class has the responsibility of storing information about a single user, and the UserManager class has the responsibility of managing a list of users and saving them to a database. However, the UserManager class violates the Single Responsibility Principle by having both responsibilities of creating users and saving them to the database. This makes the code harder to maintain and test.

DRY - Don't Repeat Yourself

This principle states that we should avoid duplicating code and instead aim to write code that is reusable and modular.

Code Example:

# DRY approach
def calculate_sum(numbers):
    return sum(numbers)

def calculate_average(numbers):
    if not numbers:
        return 0
    return calculate_sum(numbers) / len(numbers)

This code calculates the sum and average of a list of numbers. Instead of duplicating the code to sum and average the list of numbers, the calculate_sum function is defined and reused in the calculate_average function. This makes the code more concise, easier to maintain and less error-prone.

Example of code that does not follow DRY:

# Not DRY approach
def calculate_sum(numbers):
    total = 0
    for num in numbers:
        total += num
    return total

def calculate_average(numbers):
    total = 0
    for num in numbers:
        total += num
    if len(numbers) == 0:
        return 0
    return total / len(numbers)

This code also calculates the sum and average of a list of numbers, but it repeats the code to calculate the sum in both functions. This makes the code longer, harder to maintain and more error-prone. If a bug is found in the sum calculation, it would have to be fixed in both functions.

By refactoring the code to follow the DRY principle, we could improve the code quality and avoid duplicating code. This would lead to more maintainable and efficient code in the long run.

KISS - Keep It Simple, Stupid

This principle suggests that we should aim for simplicity in our code and avoid unnecessary complexity.

Code Example:

def is_palindrome(word):
    return word == word[::-1]

Above code defines a function that checks whether a given word is a palindrome (i.e. reads the same backward as forward). The function is concise and easy to understand, adhering to the KISS principle.

Example of code that does not follow KISS:

def calculate_fibonacci(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return calculate_fibonacci(n-1) + calculate_fibonacci(n-2)

This code defines a function that calculates the nth number in the Fibonacci sequence recursively. While this code is functional, it can be hard to read and understand, especially for those who are not familiar with the Fibonacci sequence or recursive functions. This violates the KISS principle by introducing unnecessary complexity.

YAGNI - You Aren't Gonna Need It

This principle suggests that we should avoid adding functionality to our code until we actually need it.

Code Example:

def multiply_numbers(num1, num2):
    return num1 * num2

This code defines a function that multiplies two numbers together. It does only what is needed for the immediate task and does not add any unnecessary functionality.

Example of code that does not follow YAGNI:

class Question:
    def __init__(self, name: str, options: list, type: TypeEnum):
        self.name = name
        self.options = options
        self.type = type
        
    def set_options(self, options):
        self.options = options

This code defines a Question class with several attributes and methods, including attributes for options, and methods for adding options for the options attribute. However, it's unclear whether all of these attributes and methods will be used in the immediate project, and whether they will be needed in the future. By Adding unnecessary functionality and complexity to the code violates the YAGNI principle.

TDD - Test-Driven Development

This principle suggests that we should write automated tests for our code before we write the code itself, in order to ensure that the code is correct and meets the requirements. Example:

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)
        
# TEST
def test_factorial():
    assert factorial(0) == 1
    assert factorial(1) == 1
    assert factorial(2) == 2
    assert factorial(5) == 120

This code defines a function that calculates the factorial of a given number using a recursive approach. Before writing the code, the developer first writes tests to ensure that the function behaves as expected for various inputs. Only then does the developer write the code to pass those tests. This adheres to the Test-Driven Development principle.

Example of code that does not follow TDD:

def calculate_tax(price, tax_rate):
    return price * tax_rate

# TEST
def test_calculate_tax():
    assert calculate_tax(10, 0.1) == 1
    assert calculate_tax(20, 0.2) == 4
    assert calculate_tax(30, 0.3) == 9

This code defines a calculate_tax() function that calculates the amount of tax to be added to a given price based on a given tax rate. This violates the Test-Driven Development principle by not writing the tests first and using them to guide the development of the function.

Clean Code

The idea of clean code is to write code that is easy to read, understand, and maintain. This involves using clear and descriptive variable and function names, following good coding conventions, and breaking code up into small, modular functions. Example:

def find_missing_number(numbers):
    """Find the missing number in a list of consecutive numbers."""
    n = len(numbers)
    expected_sum = (n + 1) * (n + 2) // 2
    actual_sum = sum(numbers)
    return expected_sum - actual_sum

This code defines a function that finds the missing number in a list of consecutive numbers. The function is well-organized, has a clear and descriptive function name and documentation, and uses clear and concise variable names. This adheres to the Clean Code principle of writing code that is easy to read, understand, and maintain.

Example of code that does not follow Clean Code:

def fmnm(nmbrs):
    n = len(nmbrs)
    es = (n + 1) * (n + 2) // 2
    ac = sum(nmbrs)
    return es - ac

This code defines the same function as the previous example, but with poorly named variables and an unclear function name. It's harder to understand what the code does and what the variables represent. 


Tests & Code Coverage

Draft


CI/CD (Semaphore)

By implementing CI/CD with Semaphore, we continuously build, test, and deploy code changes in a repeatable, automated way. This means that our development and operations teams can collaborate more effectively and identify issues early on in the development process, leading to faster resolution and a more stable application.

Slack Notification

Typically you will get notification of the CI status in different channels on Slack (e.g #proj-wcaro-mis-dev-notifications)

ci-cd-notification.png

When a build fails in your CI/CD pipeline, it's important for developers to take prompt action to resolve the issue. Here are some steps that developers can take when a build fails:

  1. Review the build logs: The first step is to review the build logs to identify the cause of the failure. The logs should provide detailed information about what went wrong, such as error messages or stack traces.
  2. Reproduce the issue: Once the cause of the failure has been identified, the next step is to reproduce the issue locally.
  3. Fix the issue: Once the issue has been reproduced, developers should work to fix the problem. 
  4. Test the fix: After making the necessary changes, developers should test the fix locally to ensure that it resolves the issue. 
  5. Communicate with the team: Finally, it's important for developers to communicate with their team about the issue and the steps taken to resolve it.

FAQ

I did git commit and found that my local branch is not updated, how to justify it?

If you merge your local branch into the origin branch, there will be only a single commit reported on the origin branch you've merged even if your local branch has multiple commits, regardless of whether you use a plain git pull or with --squash merge. You should always do git pull --rebase instead of git merge.

I've run the test locally and everything is fine, but why did the build fails?

Become friends with your broken builds. So, your build fails… what exactly does that mean? You can always check the pipeline build from https://akvo.semaphoreci.com/projects/<repo_name>. Checking the job output should point you to a failing test. There are several reasons:

  • Build or Deploy code has an error, DevOps to blame.
  • Network test (eg. basic.sh) is error. Meaning that there are missing URLs or the network/proxy is misconfigured.
  • Code Quality (Flake, ESLint, or Prettier) warning