How to put SOLID principles into practice
Tags: solidprinciples, architecture, python, beginners
Published: 2024-03-26 16:28:24

#Introduction

This article is a continuation of my previous one (“[How can applying the SOLID principles make the code better?](https://dev.to/antonov_mike/how-can-applying-the-solid-principles-make-the-code-better-3fam)”) and aims to show the implementation of SOLID principles using the example of an application architecture that is closer to real life, although it is still a demo. The implementation of a scooter rental system will be used for example.

This article uses two branches from [my repository on GitLab](https://gitlab.com/antonovmike/scooter-solid-example). One demonstrates a starting point for building simple relationships between classes. The second shows the development of this idea through adding extra conditions.

#1 Basic architecture

[Link to the full version of the code for this chapter](https://gitlab.com/antonovmike/scooter-solid-example/-/blob/1_template/scooter.py)

1. The system should allow for the management of scooters, including their statuses. This includes the ability to change the status of a scooter (e.g., from available to rented) and to check the current status of a scooter.

2. Clients should be able to rent scooters, and employees should be able to service scooters. 

3. System should be able to change scooter’s status. For example, a scooter can be reserved, rented or serviced.

Now let’s look how it is implemented:

##Scooter Class

```python
class Scooter:
    def __init__(self, status):
        self.status = status
        self.logger = logging.getLogger(__name__)

    def change_status(self, new_status):
        self.status = new_status
        self.logger.info(f"Scooter status changed to {new_status}")

    def is_available(self):
        return self.status == ScooterStatus.AVAILABLE
```

**SRP**: The Scooter class has a single responsibility, which is to manage the status of a scooter. It encapsulates the logic related to changing the status and logging relevant information. The `change_status` method handles only one aspect of the scooter’s behavior.
**OCP**: The Scooter class is not explicitly open for extension or closed for modification. However, it adheres to the OCP because its behavior can be extended by creating new subclasses without modifying the existing code.

##ClientInterface and EmployeeInterface Abstract Classes:

```python
class ClientInterface(ABC):
    @abstractmethod
    def rent_scooter(self, scooter, status_checker):
        """Rent a scooter."""
        pass

class EmployeeInterface(ABC):
    @abstractmethod
    def service_scooter(self, scooter, status_checker):
        """Service a scooter."""
        pass
```

**ISP**: These abstract classes define specific methods (`rent_scooter` for clients and `service_scooter` for employees) that adhere to the ISP. Clients and employees only need to implement the relevant methods, avoiding unnecessary dependencies.

##Client and Employee Classes:

```python
class Client(ClientInterface):
    def __init__(self):
        self.logger = logging.getLogger(__name__)

    def rent_scooter(self, scooter, status_checker):
        scooter.change_status("rented")
        self.logger.info("Scooter rented by client")

class Employee(EmployeeInterface):
    def __init__(self):
        self.logger = logging.getLogger(__name__)

    def service_scooter(self, scooter, status_checker):
        scooter.change_status("service")
        self.logger.info("Scooter serviced by employee")
```

**SRP**: Both classes have a single responsibility: Client rents a scooter, and Employee services a scooter. Their methods are focused on their respective tasks.
**OCP**: These classes are open for extension because they can be subclassed to add more behavior (e.g., additional client or employee actions) without modifying the existing code.

##Example Usage (main.py)
```python
from scooter import Scooter, Client, Employee, ScooterStatus, ScooterStatusChecker

scooter = Scooter(ScooterStatus.AVAILABLE)
print(scooter.is_available())

client = Client()
employee = Employee()
status_checker = ScooterStatusChecker()

client.rent_scooter(scooter, status_checker)

employee.service_scooter(scooter, status_checker)

scooter.change_status(ScooterStatus.AVAILABLE)
print(scooter.is_available())
```
Now let's get this skeleton up and running.

#Extended architecture

[Link to the full version of the code for this chapter](https://gitlab.com/antonovmike/scooter-solid-example/-/blob/2_simple_example/scooter.py)

1. The system should ensure that a scooter can only be rented if it is available and that it can only be serviced if it is not currently rented.

2. The system should support different types of rentals, such as regular rentals, discounted rentals, and service rentals. Each type of rental should have it's own logic for changing the scooter's status.

3. It should be designed to be flexible, maintainable, and scalable, adhering to the SOLID principles. It should support different types of rentals, can be easily extended to support new rental types, and depends on abstractions rather than concrete implementations.

##ClientInterface and EmployeeInterface Abstract Classes:

```python
class Client(ClientInterface):
    def __init__(self):
        self.logger = logging.getLogger(__name__)
        self.rental_manager = RentalManager()

    def rent_scooter(self, scooter, status_checker):
        if status_checker:
            rental_type = self.rental_manager.determine_rental_type()
            rental = self.rental_manager.create_rental_instance(rental_type, scooter)
            rental.rent()
            self.logger.info("Scooter rented by client")
        else:
            self.logger.error(f"Scooter is unavailable for rent: {scooter.status}")

class Employee(EmployeeInterface):
    def __init__(self):
        self.logger = logging.getLogger(__name__)

    def service_scooter(self, scooter, status_checker):
        if status_checker:
            scooter.change_status("service")
            self.logger.info("Scooter serviced by employee")
        else:
            self.logger.error(f"Unawailable for service: {scooter.status}")
```

**ISP**: The methods of this class have been extended, but they are still define specific methods, and clients and employees only need to implement the relevant methods, avoiding unnecessary dependencies.

##RentalManager

```python
class RentalManager:
    def __init__(self):
        self.logger = logging.getLogger(__name__)

    def determine_rental_type(self):
        current_hour = datetime.now().hour
        if 6 <= current_hour < 18:
            return RentType.REGULAR
        else:
            return RentType.DISCOUNTED

    def create_rental_instance(self, rental_type, scooter):
        if rental_type == RentType.REGULAR:
            return RegularRental(scooter)
        elif rental_type == RentType.DISCOUNTED:
            return DiscountedRental(scooter)
        else:
            raise ValueError("Invalid rental type")
```
**DIP**: `RentalManager` class depends on the abstraction of the Rental class rather than concrete implementations like `RegularRental` or `DiscountedRental`. This is evident in the `create_rental_instance` method, where it decides which type of rental to create based on the rental type, without needing to know the specifics of how each rental type is implemented. This design allows for flexibility and makes it easier to add new types of rentals in the future without modifying the `RentalManager` class.

##Example Usage (main.py)

```python
from scooter import (
    Scooter,
    Client,
    Employee,
    RegularRental,
    DiscountedRental,
    RentalSystem,
    ServiceRental,
    ScooterStatus,
    ScooterStatusChecker,
)

scooter = Scooter(ScooterStatus.AVAILABLE)
client = Client()
employee = Employee()
status_checker = ScooterStatusChecker()

# Client rents a scooter
client.rent_scooter(scooter, scooter.is_available())
# Client tries to rent rented scooter
client.rent_scooter(scooter, scooter.is_available())
# Employee tries to service rented scooter
employee.service_scooter(scooter, scooter.is_available())
# Termination of rent
scooter.change_status(ScooterStatus.AVAILABLE)
# Employee services a scooter
employee.service_scooter(scooter, scooter.is_available())
print("Client tries to rent scooter while it is in services")
client.rent_scooter(scooter, scooter.is_available())
```

#Conclusion
If we make the assumption that the application architecture was developed based on the specification, we can now move on to writing the business logic. For this purpose we need business requirements. Requirements should be designed to leverage the existing code structure and provide a comprehensive set of features for managing a scooter rental system effectively. They can be further refined and expanded based on additional features or specific business needs.
Example:

###User Authentication: 
Implement a user authentication system that interfaces with the Client class to verify identity before allowing scooter rental.
###Scooter Availability Check: 
Utilize the `is_available` method in the Scooter class to check scooter availability in real-time before a client can rent it. 
###Status Update Notifications: 
Integrate a notification system that uses the change_status method to inform users of status updates on their rented scooters. 
###Maintenance Workflow: 
Use the Employee class to create a workflow for regular scooter maintenance and servicing, triggered by the `service_scooter` method. 
###Dynamic Pricing: 
Develop a dynamic pricing strategy that can be applied in the `RegularRental` and `DiscountedRental` classes based on factors like demand, time of day, and special promotions. 
###Battery Level Monitoring: 
Incorporate a feature that monitors and reports the battery level of scooters, tying into the `LOW_BATTERY` status. 
###Damage Reporting: 
Allow users to report scooter malfunctions or damages, which would change the scooter's status to `MALFUNCTION` and trigger a service workflow. 
###Lost Scooter Tracking: 
Implement a system to track and manage scooters that are marked as `LOST`. 
###Rental History: 
Create a rental history feature that logs all rental activities associated with a user, utilizing the rent method in the Rental classes. 
###Customer Support Integration: 
Set up a customer support system that can interface with the Client class to handle queries and issues.

This is just an example of business requirements. In real life, the requirements will of course be very different.

#Outroduction
I hope these two examples ([basic architecture](https://gitlab.com/antonovmike/scooter-solid-example/-/blob/1_template/scooter.py) and [extended architecture](https://gitlab.com/antonovmike/scooter-solid-example/-/blob/2_simple_example/scooter.py)) of creating an application using SOLID principles will be useful to someone.

##P.S.
I will be glad to comments, corrections and additions in the comments, if they will be stated in an acceptable form, and if in an unacceptable form, I will be glad too, but less so 😅

Take care