Becoming the Best Software Engineer: Pythonic External Coupling

Spread the love

In this blog post, you will learn what external coupling is, and why it’s important. You will see a code sample that exemplifies external coupling, and you will also learn how to improve code that is externally coupled so that it is kept at a minimum. This is the fourth blog post of a series of blog posts on pythonic coupling. In my previous blog post of the series, I wrote about Pythonic control coupling, which can be found at the following link: Becoming the Best Software Engineer: Pythonic Control Coupling.

What Is External Coupling and Why Is It Important?

External coupling is when modules share an external resource, such as the file system, database, network resource, and so on (Ingeno, 2018). External coupling is important to be aware of because it creates highly coupled code, which can make code more difficult to test and maintain.

An Example of External Coupling

The following code exhibits external coupling where two separate modules access the file system to persist Comma Separated Value (CSV) files of businesses and employees to the storage device.

Note: There is a Python API for reading and writing CSV files; however, the below code was developed to demonstrate external coupling.

The following is the code for the business.py module, which is used to encapsulate data pertaining to a business:

class Business:
    def __init__(self, name, net_worth) -> None:
        self.__name = name
        self.__net_worth = net_worth
    
    @property
    def name(self) -> str:
        return self.__name
    
    @property
    def net_worth(self) -> str:
        return self.__net_worth
    
    def __str__(self) -> str:
        return f"{self.__name}, {self.__net_worth}"

The following is the code for the businesses.py module, which is used to maintain a list of business objects and persist the businesses to a CSV file when the “persist” method is invoked, which means it is externally coupled with the file system:

import sys
from business import Business

class Businesses:
    def __init__(self) -> None:
        self.__businesses = []
    
    def add_business(self, business : Business) -> None:
        self.__businesses.append(business)

    def persist(self) -> None:
        BUSINESSES_CSV_FILE = "businesses.csv"

        try:
            with open(BUSINESSES_CSV_FILE, "w") as fhandle:
                for business in self.__businesses:
                    fhandle.write(str(business))
                    fhandle.write("\n")
        except FileExistsError:
            print(f"File {BUSINESSES_CSV_FILE} exists.", file=sys.stderr)
        except IOError:
            print(f"IO error writing to {BUSINESSES_CSV_FILE}", file=sys.stderr)

The following is the code for the employee.py module, which encapsulates information pertaining to an employee.

class Employee:
    def __init__(self, first_name, last_name, salary) -> None:
        self.__first_name = first_name
        self.__last_name = last_name
        self.__salary = salary
    
    @property
    def first_name(self) -> str:
        return self.__first_name
    
    @property
    def last_name(self) -> str:
        return self.__last_name
    
    @property
    def salary(self) -> str:
        return self.__salary
    
    def __str__(self) -> str:
        return f"{self.__first_name},{self.__last_name},{self.__salary}"
        

The following is the code for the employees.py module, which is similar to the businesses.py module except that it manages and persists employee objects. Also, the employees.py is externally coupled with the file system, too.

import sys
from employee import Employee

class Employees:
    def __init__(self) -> None:
        self.__employees = []
    
    def add_employee(self, employee : Employee) -> None:
        self.__employees.append(employee)

    def persist(self) -> None:
        EMPLOYEE_CSV_FILE = "employees.csv"

        try:
            with open(EMPLOYEE_CSV_FILE, "w") as fhandle:
                for employee in self.__employees:
                    fhandle.write(str(employee))
                    fhandle.write("\n")
        except FileExistsError:
            print(f"File {EMPLOYEE_CSV_FILE} exists.", file=sys.stderr)
        except IOError:
            print(f"IO error writing to {EMPLOYEE_CSV_FILE}", file=sys.stderr)

The following is the main.py module, which creates some business objects and employee objects, and saves them to “businesses.csv” and “employees.csv” files, respectively:

from businesses import Business, Businesses
from employees import Employee, Employees

def main():

    # Create a couple of business objects.
    google : Business = Business("Google", "$1.796.000.000.000")
    apple : Business = Business("Apple", "$2.850.000.000.000")

    # Add the business objects to the businesses manager
    businesses : Businesses = Businesses()
    businesses.add_business(google)
    businesses.add_business(apple)

    # Save the businesses to a CSV file.
    businesses.persist()

    # Create an employee object.
    employee1 : Employee = Employee("Sergey", "Brin", "$21.000.000.000")

    # Create an employees manager object.
    employees : Employees = Employees()
    employees.add_employee(employee1)

    # Save the employees to a CSV file.
    employees.persist()

if __name__ == "__main__":
    main()

The problem with the above code is that there are two modules that are coupled with the file system to write files, which are the employees.py and businesses.py modules.

How Can We Improve the Code Above?

The first thing we can do to improve the above code is to minimize the amount of external coupling between the modules and file system by creating a new class called “FileWriter”, which has the following implementation:

import sys

from writer import Writer

class FileWriter(Writer):
    def __init__(self, file_name : str) -> None:
        self.file_name = file_name

    def write(self, content : str) -> None:
        try:
            with open(self.file_name, "w") as fhandle:
                fhandle.write(content)
        except FileExistsError:
            print(f"File {self.file_name} exists.", file=sys.stderr)
        except IOError:
            print(f"IO error writing to {self.file_name}", file=sys.stderr)

The “FileWriter” module extends the abstract class “Writer”, which is the following:

from abc import ABC, abstractmethod

class Writer(ABC):
    @abstractmethod
    def write(self, content : str) -> None:
        pass

Another change that can be made to improve the code is to remove the persist methods from the “Employees” and “Businesses” classes. Instead of having the classes directly access the “FileWriter” module, a looser coupling by data can be achieved by having these classes override the __str__ method to return CSVs of the data, which can then be passed to the write method. Therefore, the implementation of the “businesses.py” module becomes:

import sys
from business import Business

class Businesses:
    def __init__(self) -> None:
        self.__businesses = []
    
    def add_business(self, business : Business) -> None:
        self.__businesses.append(business)
    
    def __str__(self) -> str:
        out = ""

        for business in self.__businesses:
            out += str(business) + "\n"
        
        return out

The implementation of the “employees.py” becomes the following:

import sys
from employee import Employee

class Employees:
    def __init__(self) -> None:
        self.__employees = []
    
    def add_employee(self, employee : Employee) -> None:
        self.__employees.append(employee)

    def __str__(self) -> str:
        content = ""
        for employee in self.__employees:
            content += str(employee) + "\n"
        
        return content

The implementation of the “main.py” becomes the following:

from businesses import Business, Businesses
from employees import Employee, Employees
from file_writer import Writer, FileWriter

def print_to_csv_file(file_name, content):
    file_writer : Writer = FileWriter(file_name)
    file_writer.write(content)

def main():
    # Create a couple of business objects.
    google : Business = Business("Google", "$1.796.000.000.000")
    apple : Business = Business("Apple", "$2.850.000.000.000")

    # Add the business objects to the businesses manager
    businesses : Businesses = Businesses()
    businesses.add_business(google)
    businesses.add_business(apple)

    business_content = str(businesses)
    print_to_csv_file("businesses.csv", business_content)

    # Create an employee object.
    employee1 : Employee = Employee("Sergey", "Brin", "$21.000.000.000")

    # Create an employees manager object.
    employees : Employees = Employees()
    employees.add_employee(employee1)

    # Save the employees to a CSV file.
    employee_content = str(employees)
    print_to_csv_file("employees.csv", employee_content)

if __name__ == "__main__":
    main()

As you can see in the code above, the persist method is no longer used to write the CSV file to the storage device, but the “print_to_csv_file” function is used, which utilizes the “FileWriter” module.

Another advantage of this implementation is it follows the Don’t Repeat Yourself (DRY) principle of software engineering.

In this blog post, you learned what external coupling is and why it is important. Also, you saw an example of external coupling between modules and how to improve code that is externally coupled.


If you found this blog post to be valuable, then you should consider doing the following:

Subscribe

* indicates required

Intuit Mailchimp


References

Ingeno, J. (2018). Software architect’s handbook. Packt Publishing.


Posted

in

by