Object oriented programming for AI Projects
1 Why data science code bases often lack structure
The way we structure our code can make or break a project. But how do you decide when one object should own another, simply use it, or have a more flexible association? Traditionally, software engineering also places a chief focus on code architecture. However, in AI projects there is usually so much to take care o and so much new third party software that the SW architecture often comes last. Even more, many AI engineers have little education in traditional software engineering. Instead, there come from data driven fields in engineering or science.
Things are getting worse by the choice of the language. Python is AI’s favourite language. However, with all the flexibility python allows, it is very easy to program spaghetti code. While this is true for every language, the lack of strong typing acts as a lack of boundaries. Boundaries enforce structure, and if you have no time to think of your own structure, this can be something good.
Read on to increase your understanding of the three fundamental ways objects can be related: aggregation, composition, and dependency.
By the end of this article, you’ll not only grasp these concepts but also know how to implement them effectively in Python, leveraging type hints for clarity and precision.
2 The Importance of Object Relationships
Imagine building a car simulation. You have classes for Car
, Engine
, and Driver
. How these classes interact is crucial:
- Should a
Car
own anEngine
, or just reference one? - Does a
Driver
temporarily use aCar
, or is there a deeper connection? - How can these relationships affect the maintenance and scalability of your code?
Implementation should follow the SOLID principles.
We want
- Single Responsible: only change the class based on one actor
- Open/Close: classes should be closed for modification, but open for extension via new classes
- Interface Segregation: well-defined interfaces
- Dependency Inversion: build on abstractions
Correctly implementing object relationships can lead to:
- Cleaner code
- Better maintainability
- Enhanced scalability
3 Aggregation: The “Has-a” Relationship
Aggregation is a weak association where one class “has-a” reference to another. The lifetimes of the objects are independent.
Think of a smartphone and a SIM card. The phone “has-a” SIM card, but the SIM card isn’t created by the phone and can be inserted into different phones.
When to Use Aggregation
- When objects can exist independently.
- When you want to reuse existing instances.
- When the container doesn’t solely own the contained object.
Example
from typing import Optional
class Engine:
def start(self) -> None:
print("Engine starts.")
class Car:
def __init__(self, engine: Engine) -> None:
self.engine: Engine = engine # Aggregation (Car has an Engine)
def drive(self) -> None:
self.engine.start()
- Ownership: The same
Engine
instance can be shared among multipleCar
instances. - Lifecycle Management: The
Engine
can outlive theCar
, or vice versa. - Encapsulation/Coupling: Swap engines without affecting the
Car
’s structure.
Why Type Hints Elevate Your Code Strongly typed Languages like Java and C++ use a lot of OOP. Until recently a big advantage of python was its weak-typing. However weak typing can make code more complex. Especially if types which were not intended for a function are used in that function. If a member is missing, than the code crashes. Type hints can help. They server for static type checking and as documentation embedded in your code. They:
- Enhance Readability
- Aid in Debugging
- Improve IDE Support
4 Composition: The “Owns-a” Relationship
Composition is a strong association where one class “owns” another. The lifetime of the owned object is tightly coupled to the owner.
Consider the human body and its heart. The body “owns” the heart, and the heart doesn’t exist independently outside the body (at least not for very long).
When to Use Composition
- When the contained object shouldn’t exist without the container.
- When the container is solely responsible for the creation and destruction of the contained object.
- When you want to enforce a strict lifecycle.
Example
class Engine:
def start(self) -> None:
print("Engine starts.")
class Car:
def __init__(self) -> None:
self.engine: Engine = Engine() # Composition (Car owns an Engine)
def drive(self) -> None:
self.engine.start()
- Ownership:
Car
creates and owns theEngine
. - Lifecycle Management: When
Car
is destroyed, so is itsEngine
. - Encapsulation/Coupling:
Engine
is hidden withinCar
, emphasizing a strong bond.
5 Dependency: The “Uses-a” Relationship
Dependency is a temporary relationship where one class “uses” another to perform a function. It’s the loosest form of coupling.
Imagine renting a car. You “use” the car temporarily, but you don’t own it, and your interaction is limited to the rental period.
When to Use Dependency
- When a class needs to perform an action using another class temporarily.
- When you want to minimize coupling between classes.
- When the interaction is brief and method-specific.
Example
class Engine:
def start(self) -> None:
print("Engine starts.")
class Driver:
def operate(self, engine: Engine) -> None: # Dependency (Driver uses Engine)
engine.start()
- Ownership:
Driver
doesn’t own or hold a reference toEngine
beyond the method. - Lifecycle Management:
Driver
usesEngine
within the scope of theoperate
method. - Encapsulation/Coupling: Changes to
Engine
have minimal impact onDriver
.
6 Conclusion
Understanding the correct relationships between classes is a cornerstone of effective object-oriented programming. Keep those concepts in mind. By just wondering what relations your objects have, you write better code.
Takeaways:
- Aggregation is for flexible, independent associations.
- Composition is for strong, dependent ownership.
- Dependency is for temporary, minimal coupling.
Ready to elevate your Python code? Start applying these principles today and experience the difference in your software development journey.