Prerequisites
Before reading this article, you should be familiar with the topics exposed in Part I. Be sure to read and understand the problem described there and the flaws of the initial solution proposed.
Also, try to think about an alternative solution before reading this article. That way you will be able to compare and see what the differences are between the one it's going to be discussed in this article and the one you were able to create on your own.
Share your solution, even if it is not completely accurate. I can guarantee you, there is almost always some flaw in some solution because they are almost always created to solve a particular problem. So, don't be hesitant about sharing yours. That way we all learn more!
After that, dive into this article to find out about better solutions using interfaces in Python.
Initial solution
Right now we have a solution that solves the problem we were asked to solve, but it still has some design issues. The main issue we face is that it is impossible to add a new visitor entity without modifying the Research Center class.
This coupling in the classes can lead to introducing bugs because we are adding code in more places than we should when trying to create a new visitor class. Let's see how we can solve this issue!
Interfaces
To create a better solution to this problem, we are going to use an important concept of object-oriented programming called interfaces.
In programming, an interface is a set of rules or guidelines for a class or object to follow. It defines a contract for the class or object, outlining the methods and properties that must be implemented.
An interface does not provide an implementation for the methods or properties, but instead, it defines a blueprint for classes or objects to follow. It is commonly used to provide a common set of methods for different classes or objects to implement, which can then be used interchangeably.
In Python, interfaces are not a built-in feature like in some other programming languages such as Java or C#. However, you can use abstract base classes (ABCs) to achieve a similar effect.
An abstract base class is a class that cannot be instantiated and is used as a base class for one or more derived classes. Classes that derive from an ABC are required to implement certain methods or properties defined by the ABC.
Note : If you want to have a look at another example of using abstract classes in Python feel free to check this article to see how they can be used to implement different sorting methods.
Using interfaces
So, how can we use the power of interfaces to tackle this problem differently?
First, we must go to the root of our problems. That is the fact that we are letting all the responsibility for checking the access to our Research Center class. What if the visitor classes could verify somehow that they have access to a specific research center? This way, adding a new visitor class could prevent us from adding more code to the Research Center class, leading to a more maintainable, less error-prone, code.
To achieve this using Python we need to create an interface that represents what a visitor is. Essentially, a visitor instance needs to be able to check if it can access a specific research center. Therefore, it needs to have a method that allows performing this check for all concrete instances inheriting from it.
An example of our Visitor interface is the following:
from abc import ABC, abstractmethod
class IVisitor(ABC):
@abstractmethod
def can_access(self, visitors):
"""Returns whether or not a visitor is on the list of authorized visitors.
Args:
visitors (List[Self]): Authorized visitors list.
Returns:
bool: `True` if the given visitor is on the list of authorized visitors. `False` otherwise.
"""
As you can see, this class only contains a method can_access that will verify if this visitor belongs to a list of visitors.
Note : The code above names the class representing the interface IVisitor. This capital I at the beginning of the name is a convention for naming interfaces in languages such as Java or C# , but not in Python. We are just trying to emphasize the fact that this class represents an interface, but it might as well have been called Visitor without any problem.
The Research Center Class
The main advantage of this approach is that we won't have to modify the Research Class once a new visitor type is added. But still, we need to make some modifications to our previous implementation of this class to make it comply with this new way of checking the access of visitors.
Let's see how it would look:
class ResearchCenter:
def __init__ (self):
self.allowed_visitors = []
def add_visitor(self, visitor: IVisitor):
self.allowed_visitors.append(visitor)
def verify_access(self, visitor: IVisitor):
return visitor.can_access(self.allowed_visitors)
This new implementation is a lot better than our previous one. Let's see why:
We don't need a new list to store the allowed visitors of each particular type. We replaced our allowed_persons and allowed_vehicles lists with the allowed_visitors lists that don't care about the type of visitor but the fact that they are visitor instances instead.
We don't need a new method to add visitors for every particular visitor type. We have replaced our add_person and add_vehicle methods with a more general-purpose add_visitor method that adds a new visitor without caring about the specific type.
We don't need a new method for verifying the access for every particular visitor type either. Since the responsibility for verifying access now lies in our visitor classes, we can get rid of our previous verify_person_access and verify_vehicle_access methods, and replace them with a more generic one called verify_access. This new method will receive a visitor as an argument and have that visitor check if it can access the list of visitors stored in the research center.
Do you see now how we won't need to add any more code to this class whenever a new visitor class is added?
Modifying our previous concrete visitor classes
With our interface representing visitors and the modified Research Center class, what we are missing now is modifying our concrete visitor classes so they can handle their mechanism for verifying access to some research centers. This is the more important part of this solution since it will be what we would have to code whenever a new visitor class needs to be added.
Previously, the visitor classes were only storing information in the form of attributes, but now they will be needed to have some "behavior" too. They will be responsible for defining their way of checking if they belong to the visitor list of some research center. Let's see a possible solution:
class Person(IVisitor):
def __init__ (self, id: str, name: str):
self.id = id
self.name = name
def __str__ (self) -> str:
return self.id
def can_access(self, visitors: List[IVisitor]):
for visitor in visitors:
if isinstance(visitor, Person) and visitor.id == self.id:
return True
return False
class VehicleType(Enum):
AUTO = 1
TRUCK = 2
BUS = 3
class Vehicle(IVisitor):
def __init__ (self, license_plate: str, vehicle_type: VehicleType):
self.license_plate = license_plate
self.vehicle_type = vehicle_type
def __str__ (self) -> str:
return self.license_plate
def can_access(self, visitors: List[IVisitor]):
for visitor in visitors:
if (
isinstance(visitor, Vehicle)
and visitor.license_plate == self.license_plate
):
return True
return False
Let's explain this code a little:
Both our visitor classes ( Person and Vehicle ) now inherit from the IVisitor class that defines our interface for visitors.
Because of this, they need to have a concrete implementation of the can_access method. If any of these classes would not implement it, we would receive an error when trying to instantiate it.
Since the visitor classes are now responsible for checking if they belong (or not) to a list of visitors, they are defining a way to be identified in a collection of visitors. In the case of the Person class, we are assuming they can be identified by the attribute id , and in the case of the Vehicle class, they are identified by the license_plate attribute.
There is an important difference between this solution and the previous one when iterating the list of visitors checking for access. And that's the fact that previously we always knew that we were checking for a visitor of some type on a list of visitors of the same type. But now, since the list of visitors can be of any type, we need to be sure that we are checking against the same type that our class has. That is what we achieve when using the isinstance method provided by Python.
With all these pieces in order, we now have a much better mechanism to handle access verification at the research centers. Let's see how easy it is now to add a new visitor type.
Adding a new Visitor Class
We are trying to create a better solution than the one exposed here precisely because of the flaws this solution had when adding new visitor classes. To illustrate how we have solved those issues with this new solution, let's add an Animal class to this scenario.
Let's assume the animals only have a name and an id attribute, and that this id is unique for every animal. A possible implementation in Python would be the following:
class Animal(IVisitor):
def __init__ (self, id: str, name: str):
self.id = id
self.name = name
def __str__ (self) -> str:
return self.id
def can_access(self, visitors: List[IVisitor]):
for visitor in visitors:
if isinstance(visitor, Animal) and visitor.id == self.id:
return True
return False
And that's it! As easy as creating a new class and implementing the can_access method. In this particular case, it looks very similar to the implementation of the Person class because we are also using an id attribute to identify the animals.
The identification can be addressed in whatever way we define, for a specific class. Let's say, for example, that we could have also checked for the name of the animals, in case we were guaranteed that the names were unique for every animal.
Conclusions
In this article, we have learned how to use interfaces in Python to solve problems very similar to what we could encounter in real-life scenarios. We learned how to address all the issues presented in Part I of this series of articles.
With this new solution, we got rid of having to add extra code to the Research Center class whenever we wanted to include a new visitor class in our project. We refactor our code to make it much more maintainable by using interfaces and letting the responsibility for checking access to the visitor classes instead of leaving all the responsibility to the overloaded Research Center class.
We saw how we could add a new visitor class easily now. Just define whatever attributes you need to identify it, and make a concrete implementation of the can_access method.
If you want to check a more complete version of the code examples shown in this article you can do it here. Be sure you understand it and don't be hesitant about modifying it and running it on your local computer. Also, ask any questions regarding this implementation. I will be happy to help you, and very grateful for your feedback.
If you enjoy this article, please show your support by reacting, commenting, or just spending a few minutes thinking about the topics exposed here. Don't be a passive reader, think about what you read and share your thoughts. See you soon!
Also, if you want (or have a friend that wants) to learn about programming in Python in the Spanish language, you might be interested in the other project I'm glad to be a part of:
CodeXL Academy blog: A blog with Python content only in Spanish. Sometimes we publish translations of the articles published here for our English-speaking audience.
CodeXL Academy official website: Learn about everything we have to offer you.
Free newsletter: Receive updates every week about what's happening in the world of Python and our Academy.
Free and Paid courses: Discover our teaching materials in Spanish.
👋 Hello, I'm Alberto, Software Developer at doWhile, Competitive Programmer, Teacher, and Fitness Enthusiast.
🧡 If you liked this article, consider sharing it.
Top comments (0)