Object-Oriented Programming
Object-Oriented Programming (OOP) is a programming paradigm that has become indispensable nowadays. This approach models real-world elements as “objects” that have properties and behaviours, which allows for more intuitive and maintainable programmes to be created. In this article we will look at the basic concepts of OOP and its advantages over other paradigms like procedural programming. Let’s get started!
This paradigm is based on two fundamental concepts:
- Objects: entities that combine state (data) and behaviour (operations) in a single unit. For example, a “car” object would have properties like colour, number of doors, maximum speed, etc. And behaviours like accelerate, brake, steer, etc.
- Classes: specifications that define the common structure and behaviour of a group of objects. The “car” class would serve as a template for creating car objects with the same characteristics.
As explained by programmer Alan Kay, one of the creators of OOP:
“The big idea is to design programmes in terms of conceptual objects and concepts from the real world. The interfaces with the real world should, therefore, be constructed in terms of these conceptual objects.”
That is, OOP conceptually models real-world elements to make programming more intuitive.
Programming paradigms
Before delving into OOP, it is worth understanding that there are different paradigms or approaches to tackle programming. The main ones are:
Procedural programming
Ordered sequence of instructions that the programme must follow step-by-step. The focus is on procedures and functions. For example, C is a language geared towards procedural programming.
Procedural programming is better for:
- Simple problems or sequential algorithms.
- Code that won’t need heavy reusing or expanding.
- Cases where performance and efficiency are critical.
Object-Oriented programming
Model based on objects that contain data and code in cohesive units. The focus is on classes and the interaction between objects. For example, Java and Python are object-oriented languages.
OOP allows modelling real-world elements more directly, better encapsulating data, and reusing code through inheritance between classes.
The main advantages of OOP over procedural programming are:
- Modularity: objects group related data and operations, encapsulating internal complexity. This allows working with independent modules.
- Information hiding: Objects can expose a simple interface and hide internal implementation details. This reduces coupling.
- Reusability: Classes enable code reuse. An abstract class can inherit to multiple subclasses.
- Extensibility: We can extend the behaviour of parent classes by creating new subclasses.
- Conceptual mapping: Objects represent real-world entities, which eases the translation of requirements into code.
However, OOP also has disadvantages. According to programmer Paul Graham:
“Object-oriented programming often makes things harder than they need to be.”
For example, for simple problems OOP can be excessive. And in large projects there is a risk of overusing inheritance and polymorphism, making the code difficult to follow.
Ultimately, OOP is more suitable when:
- The problem to be modelled has clear, structured entities.
- We want to reuse encapsulated code in modular classes.
- We work on systems that need to be easily extended and maintained.
More articles
1 - Classes and objects
In object-oriented programming, classes and objects are the key concepts to understand how we model elements of reality and define their structure and behaviour within software. Let’s look in detail at the anatomy of a class, how to create objects from it to use their properties and methods, and other key details of their relationship.
Anatomy of a class
A class acts as a blueprint or mould to construct similar objects, defining their common characteristics and functionalities. It is similar to the blueprint used to construct houses in the same neighbourhood: they all share certain key attributes.
The typical components of a class are:
Attributes (properties): Variables that characterise the object. For example, for a Person
class, attributes like name
, age
, ID
, etc.
class Person:
id = ""
name = ""
age = 0
Methods: Functions that define behaviours. For example, a Person
can walk()
, talk()
, eat()
, etc. They access the attributes to implement said functionality.
Constructor: Special __init__()
method that executes when instantiating the class and allows initialising attributes.
Destructor: __del__()
method that executes when deleting the instance, freeing up resources. Optional in some languages.
Creating objects
From the class we generate objects, which are specific instances with their own defined attributes. Let’s say the House class is the blueprint, and a specific house on a particular street is the object.
In code, we create an object by invoking the class as if it were a method:
# Person class
class Person:
def __init__(self, n, a):
self.name = n
self.age = a
# Specific Person objects
john = Person("John", 30)
mary = Person("Mary", 35)
Each object shares the general structure and behaviour but can store different data.
Using properties and methods
We now have a Person
class and a john
object of type Person
. How do we interact with the object?
- Properties: It is possible to access the value of an object attribute using the object reference (
john
) and the attribute name.
john.name # "John"
john.age # 30
- Methods: Are invoked in the same way as accessing attributes but adding parentheses, and inside them, the arguments that are passed if it takes any.
# Person class
class Person:
def __init__(self, n, a):
self.name = n
self.age = a
def eat(self, food):
print(f"Eating {food}")
# Specific Person object
john = Person("John", 30)
john.eat("pizza") # Prints "Eating pizza"
The john object now has its own state (properties) and behaviour (methods).
Self vs This
An important detail in methods is how they access the object’s attributes and other methods. Here another difference between languages comes into play:
- Self: In Python, attributes and methods are accessed within the class by prepending
self
. This points to the instantiated object.
class Person:
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hello! I'm {self.name}")
john = Person("John")
john.greet()
# Prints "Hello! I'm John"
- This: In Java or C#,
this
is used instead of self. It fulfils the same functionality of pointing to the object’s members.
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public void greet() {
System.out.println("Hello! I'm " + this.name);
}
}
Person john = new Person("John");
john.greet();
// Prints "Hello! I'm John"
Conclusion
Classes and objects are the key concepts in OOP, allowing modelling real-world entities and generating modular, generic components of our system to construct more robust and easy to understand programmes.
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
2 - Class relations
One of the most powerful aspects of OOP is the ability to create relationships between classes, allowing for complex systems to be modeled in a way that closely mimics real-world interactions. Understanding these relationships is crucial for designing robust, maintainable, and scalable software systems. This guide aims to explore the various types of relationships between classes in OOP, including association, aggregation, composition, inheritance, and more. We’ll delve into the nuances of each relationship type, provide detailed examples using Python, and illustrate concepts with UML diagrams where appropriate.
In Object-Oriented Programming, classes don’t exist in isolation. They interact and relate to each other in various ways to model complex systems and relationships. Understanding these relationships is crucial for designing effective and maintainable object-oriented systems.
The main types of class relationships we’ll explore in depth are:
- Association (“uses-a”)
- Aggregation (weak “has-a” relationship)
- Composition (strong “has-a” relationship)
- Inheritance (“is-a” relationship)
- Realisation (Implementation)
- Dependency
Each of these relationships represents a different way that classes can be connected and interact with each other. They vary in terms of the strength of the coupling between classes, the lifecycle dependencies, and the nature of the relationship.
Before we dive into each type of relationship, let’s visualise them using a UML class diagram:
classDiagram
class ClassA
class ClassB
class ClassC
class ClassD
class ClassE
class ClassF
class InterfaceG
ClassA --> ClassB : Association
ClassC o-- ClassD : Aggregation
ClassE *-- ClassF : Composition
ClassB --|> ClassA : Inheritance
ClassE ..|> InterfaceG : Realisation
ClassA ..> ClassF : Dependency
This diagram provides a high-level overview of the different types of class relationships. In the following sections, we’ll explore each of these relationships in detail, providing explanations, examples, and more specific UML diagrams.
2.1 - Association
Association is the most basic and generic form of relationship between classes. It represents a connection between two classes where one class is aware of and can interact with another class. This relationship is often described as a “uses-a” relationship.
Key characteristics of association:
- It represents a loose coupling between classes.
- The associated classes can exist independently of each other.
- The lifetime of one class is not tied to the lifetime of the other.
- It can be unidirectional or bidirectional.
There are two main types of association:
- Unidirectional Association
- Bidirectional Association
Let’s explore each of these in more detail.
Unidirectional association
In a unidirectional association, one class knows about and can interact with another class, but not vice versa. This is a one-way relationship.
Here’s an example in Python:
class Customer:
def __init__(self, name):
self.name = name
class Order:
def __init__(self, order_number, customer):
self.order_number = order_number
self.customer = customer # This creates an association
def display_info(self):
return f"Order {self.order_number} placed by {self.customer.name}"
# Creating instances
customer = Customer("John Doe")
order = Order("12345", customer)
print(order.display_info()) # Output: Order 12345 placed by John Doe
In this example, the Order
class has a unidirectional association with the Customer
class. An Order
knows about its associated Customer
, but a Customer
doesn’t know about its Order
s.
Here’s a UML diagram representing this relationship:
classDiagram
class Customer {
+name: string
}
class Order {
+order_number: string
+customer: Customer
+display_info()
}
Order --> Customer : places
The arrow in the diagram points from Order
to Customer
, indicating that Order
knows about Customer
, but not the other way around.
Bidirectional association
In a bidirectional association, both classes are aware of each other and can interact with each other. This is a two-way relationship.
Here’s an example in Python:
class Student:
def __init__(self, name):
self.name = name
self.courses = []
def enroll(self, course):
self.courses.append(course)
course.add_student(self)
def display_courses(self):
return f"{self.name} is enrolled in: {', '.join(course.name for course in self.courses)}"
class Course:
def __init__(self, name):
self.name = name
self.students = []
def add_student(self, student):
self.students.append(student)
def display_students(self):
return f"{self.name} has students: {', '.join(student.name for student in self.students)}"
# Creating instances
student1 = Student("Alice")
student2 = Student("Bob")
math_course = Course("Mathematics")
physics_course = Course("Physics")
# Enrolling students in courses
student1.enroll(math_course)
student1.enroll(physics_course)
student2.enroll(math_course)
print(student1.display_courses())
print(math_course.display_students())
In this example, there’s a bidirectional association between Student
and Course
. A Student
knows about their Course
s, and a Course
knows about its Student
s.
Here’s a UML diagram representing this relationship:
classDiagram
class Student {
+name: string
+courses: list
+enroll(course)
+display_courses()
}
class Course {
+name: string
+students: list
+add_student(student)
+display_students()
}
Student "0..*" <--> "0..*" Course : enrolls in >
The double-headed arrow in the diagram indicates that both Student
and Course
are aware of each other. The “0..*” notation indicates that a Student
can be enrolled in zero or more Course
s, and a Course
can have zero or more Student
s.
Association is a flexible relationship that can represent many real-world connections between objects. It’s important to choose between unidirectional and bidirectional associations carefully, as bidirectional associations can introduce more complexity and potential for errors if not managed properly.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley.
- Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley.
- Phillips, D. (2018). Python 3 Object-Oriented Programming (3rd ed.). Packt Publishing.
- Lott, S. F. (2020). Object-Oriented Python: Master OOP by Building Games and GUIs. No Starch Press.
- Booch, G., Rumbaugh, J., & Jacobson, I. (2005). The Unified Modeling Language User Guide (2nd ed.). Addison-Wesley.
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
2.2 - Aggregation
Aggregation is a specialised form of association that represents a “whole-part” or “has-a” relationship between classes. In aggregation, one class (the whole) contains references to objects of another class (the part), but the part can exist independently of the whole.
Key characteristics of aggregation:
- It’s a stronger relationship than a simple association, but weaker than composition.
- The “part” object can exist independently of the “whole” object.
- Multiple “whole” objects can share the same “part” object.
- If the “whole” object is destroyed, the “part” object continues to exist.
Let’s look at an example to illustrate aggregation:
class Department:
def __init__(self, name):
self.name = name
self.employees = []
def add_employee(self, employee):
self.employees.append(employee)
def remove_employee(self, employee):
self.employees.remove(employee)
def list_employees(self):
return f"Department {self.name} has employees: {', '.join(emp.name for emp in self.employees)}"
class Employee:
def __init__(self, name, id):
self.name = name
self.id = id
def __str__(self):
return f"Employee(name={self.name}, id={self.id})"
# Creating instances
hr_dept = Department("Human Resources")
it_dept = Department("Information Technology")
emp1 = Employee("Alice", "E001")
emp2 = Employee("Bob", "E002")
emp3 = Employee("Charlie", "E003")
# Adding employees to departments
hr_dept.add_employee(emp1)
hr_dept.add_employee(emp2)
it_dept.add_employee(emp2) # Note: Bob works in both HR and IT
it_dept.add_employee(emp3)
print(hr_dept.list_employees())
print(it_dept.list_employees())
# If we remove the HR department, the employees still exist
del hr_dept
print(emp1) # Employee still exists
In this example, we have an aggregation relationship between Department
and Employee
. A Department
has Employee
s, but Employee
s can exist independently of any particular Department
. Also, an Employee
can belong to multiple Department
s (as we see with Bob).
Here’s a UML diagram representing this aggregation relationship:
classDiagram
class Department {
+name: string
+employees: list
+add_employee(employee)
+remove_employee(employee)
+list_employees()
}
class Employee {
+name: string
+id: string
+__str__()
}
Department o-- Employee : has
In this diagram, the open diamond on the Department
side of the relationship indicates aggregation. This shows that Department
is the “whole” and Employee
is the “part” in this relationship.
It’s important to note that while aggregation implies a whole-part relationship, the “part” (in this case, Employee
) can exist independently and can even be part of multiple “wholes” (multiple Department
s).
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley.
- Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley.
- Phillips, D. (2018). Python 3 Object-Oriented Programming (3rd ed.). Packt Publishing.
- Lott, S. F. (2020). Object-Oriented Python: Master OOP by Building Games and GUIs. No Starch Press.
- Booch, G., Rumbaugh, J., & Jacobson, I. (2005). The Unified Modeling Language User Guide (2nd ed.). Addison-Wesley.
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
2.3 - Composition
Composition is a stronger form of aggregation. It’s a “whole-part” relationship where the part cannot exist independently of the whole. In other words, the lifetime of the part is tied to the lifetime of the whole.
Key characteristics of composition:
- It represents a strong “has-a” relationship.
- The “part” object cannot exist independently of the “whole” object.
- When the “whole” object is destroyed, all its “part” objects are also destroyed.
- A “part” object belongs to only one “whole” object at a time.
Let’s look at an example to illustrate composition:
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return "Engine started"
class Car:
def __init__(self, make, model, horsepower):
self.make = make
self.model = model
self.engine = Engine(horsepower) # Composition: Car creates its own Engine
def start_car(self):
return f"{self.make} {self.model}: {self.engine.start()}"
def __del__(self):
print(f"{self.make} {self.model} is being destroyed, and so is its engine.")
# Creating a Car instance
my_car = Car("Toyota", "Corolla", 150)
print(my_car.start_car()) # Output: Toyota Corolla: Engine started
# When we delete the Car, its Engine is also deleted
del my_car # This will print: Toyota Corolla is being destroyed, and so is its engine.
In this example, we have a composition relationship between Car
and Engine
. A Car
has an Engine
, and the Engine
cannot exist independently of the Car
. When a Car
object is created, it creates its own Engine
. When the Car
object is destroyed, its Engine
is also destroyed.
Here’s a UML diagram representing this composition relationship:
classDiagram
class Car {
+make: string
+model: string
-engine: Engine
+start_car()
+__del__()
}
class Engine {
-horsepower: int
+start()
}
Car *-- Engine : has
In this diagram, the filled diamond on the Car
side of the relationship indicates composition. This shows that Car
is the “whole” and Engine
is the “part” in this relationship, and that the Engine
’s lifetime is tied to the Car
’s lifetime.
The key difference between aggregation and composition is the strength of the relationship and the lifecycle dependency. In aggregation, the “part” can exist independently of the “whole”, while in composition, the “part” cannot exist without the “whole”.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley.
- Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley.
- Phillips, D. (2018). Python 3 Object-Oriented Programming (3rd ed.). Packt Publishing.
- Lott, S. F. (2020). Object-Oriented Python: Master OOP by Building Games and GUIs. No Starch Press.
- Booch, G., Rumbaugh, J., & Jacobson, I. (2005). The Unified Modeling Language User Guide (2nd ed.). Addison-Wesley.
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
2.4 - Inheritance
Inheritance is a fundamental concept in OOP that allows a class (subclass or derived class) to inherit properties and methods from another class (superclass or base class). It represents an “is-a” relationship between classes.
Key characteristics of inheritance:
- It promotes code reuse and establishes a hierarchy between classes.
- The subclass inherits all public and protected members from the superclass.
- The subclass can add its own members and override inherited members.
- It supports the concept of polymorphism.
Let’s look at an example to illustrate inheritance:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
# Creating instances
dog = Dog("Buddy")
cat = Cat("Whiskers")
print(dog.speak()) # Output: Buddy says Woof!
print(cat.speak()) # Output: Whiskers says Meow!
# Demonstrating polymorphism
def animal_sound(animal):
print(animal.speak())
animal_sound(dog) # Output: Buddy says Woof!
animal_sound(cat) # Output: Whiskers says Meow!
In this example, we have a base class Animal
and two derived classes Dog
and Cat
. Both Dog
and Cat
inherit from Animal
and override the speak
method.
Here’s a UML diagram representing this inheritance relationship:
classDiagram
class Animal {
+name: string
+speak()
}
class Dog {
+speak()
}
class Cat {
+speak()
}
Animal <|-- Dog
Animal <|-- Cat
In this diagram, the arrows pointing from Dog
and Cat
to Animal
indicate inheritance. This shows that Dog
and Cat
are subclasses of Animal
.
Inheritance is a powerful feature of OOP, but it should be used judiciously. Overuse of inheritance can lead to complex class hierarchies that are difficult to understand and maintain. The principle of “composition over inheritance” suggests that it’s often better to use composition (has-a relationship) rather than inheritance (is-a relationship) when designing class relationships.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley.
- Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley.
- Phillips, D. (2018). Python 3 Object-Oriented Programming (3rd ed.). Packt Publishing.
- Lott, S. F. (2020). Object-Oriented Python: Master OOP by Building Games and GUIs. No Starch Press.
- Booch, G., Rumbaugh, J., & Jacobson, I. (2005). The Unified Modeling Language User Guide (2nd ed.). Addison-Wesley.
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
2.5 - Realisation (Implementation)
Realisation, also known as implementation, is a relationship between a class and an interface. It indicates that a class implements the behaviour specified by an interface.
Key characteristics of realisation:
- It represents a contract that the implementing class must fulfil.
- The class must provide implementations for all methods declared in the interface.
- It allows for polymorphism through interfaces.
Python doesn’t have a built-in interface concept like some other languages (e.g., Java), but we can simulate interfaces using abstract base classes. Here’s an example:
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def draw(self):
pass
class Circle(Drawable):
def draw(self):
return "Drawing a circle"
class Square(Drawable):
def draw(self):
return "Drawing a square"
def draw_shape(shape: Drawable):
print(shape.draw())
# Creating instances
circle = Circle()
square = Square()
# Using polymorphism through the interface
draw_shape(circle) # Output: Drawing a circle
draw_shape(square) # Output: Drawing a square
In this example, Drawable
is an abstract base class that acts like an interface. Both Circle
and Square
implement the Drawable
interface by providing their own implementation of the draw
method.
Here’s a UML diagram representing this realisation relationship:
classDiagram
class Drawable {
<<interface>>
+draw()
}
class Circle {
+draw()
}
class Square {
+draw()
}
Drawable <|.. Circle
Drawable <|.. Square
In this diagram, the dashed arrows pointing from Circle
and Square
to Drawable
indicate realisation. This shows that Circle
and Square
implement the Drawable
interface.
Realisation is a powerful concept that allows for designing loosely coupled systems. By programming to interfaces rather than concrete implementations, we can create more flexible and extensible software.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley.
- Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley.
- Phillips, D. (2018). Python 3 Object-Oriented Programming (3rd ed.). Packt Publishing.
- Lott, S. F. (2020). Object-Oriented Python: Master OOP by Building Games and GUIs. No Starch Press.
- Booch, G., Rumbaugh, J., & Jacobson, I. (2005). The Unified Modeling Language User Guide (2nd ed.). Addison-Wesley.
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
2.6 - Dependency
Dependency is the weakest form of relationship between classes. It exists when one class uses another class, typically as a method parameter, local variable, or return type.
Key characteristics of dependency:
- It represents a “uses” relationship between classes.
- It’s a weaker relationship compared to association, aggregation, or composition.
- Changes in the used class may affect the using class.
Here’s an example to illustrate dependency:
class Printer:
def print_document(self, document):
return f"Printing: {document.get_content()}"
class PDFDocument:
def get_content(self):
return "PDF content"
class WordDocument:
def get_content(self):
return "Word document content"
# Using the Printer
printer = Printer()
pdf = PDFDocument()
word = WordDocument()
print(printer.print_document(pdf)) # Output: Printing: PDF content
print(printer.print_document(word)) # Output: Printing: Word document content
In this example, the Printer
class has a dependency on both PDFDocument
and WordDocument
classes. The Printer
uses these classes in its print_document
method, but it doesn’t maintain a long-term relationship with them.
Here’s a UML diagram representing these dependency relationships:
classDiagram
class Printer {
+print_document(document)
}
class PDFDocument {
+get_content()
}
class WordDocument {
+get_content()
}
Printer ..> PDFDocument : uses
Printer ..> WordDocument : uses
In this diagram, the dashed arrows pointing from Printer
to PDFDocument
and WordDocument
indicate dependency. This shows that Printer
uses these classes, but doesn’t have a stronger relationship with them.
Dependency is often used to reduce coupling between classes. By depending on abstractions (like interfaces) rather than concrete classes, we can make our code more flexible and easier to change.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley.
- Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley.
- Phillips, D. (2018). Python 3 Object-Oriented Programming (3rd ed.). Packt Publishing.
- Lott, S. F. (2020). Object-Oriented Python: Master OOP by Building Games and GUIs. No Starch Press.
- Booch, G., Rumbaugh, J., & Jacobson, I. (2005). The Unified Modeling Language User Guide (2nd ed.). Addison-Wesley.
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
2.7 - Dependency
Understanding class relationships is crucial for effective object-oriented design and programming. We’ve explored various types of relationships including association, aggregation, composition, inheritance, realisation, and dependency. Each of these relationships serves a specific purpose and has its own strengths and weaknesses.
Comparing and contrasting relations
Now that we’ve explored the various types of class relations, let’s compare and contrast them to better understand when to use each:
Association vs Aggregation vs Composition
- Association is the most general relationship, representing any connection between classes.
- Aggregation is a specialised association representing a whole-part relationship where the part can exist independently.
- Composition is the strongest whole-part relationship where the part cannot exist independently of the whole.
Inheritance vs Composition
- Inheritance represents an “is-a” relationship (e.g., a Dog is an Animal).
- Composition represents a “has-a” relationship (e.g., a Car has an Engine).
- The principle of “composition over inheritance” suggests favouring composition for more flexible designs.
Realisation vs Inheritance
- Realisation is about implementing an interface, focusing on behaviour.
- Inheritance is about extending a class, inheriting both state and behaviour.
Dependency vs Association
- Dependency is a weaker, often temporary relationship (e.g., a method parameter).
- Association implies a more permanent relationship, often represented by a class attribute.
Here’s a comparison table summarising these relationships:
Relationship | Strength | Lifecycle Binding | “Is-a” or “Has-a” | Symbol in UML |
---|
Dependency | Weakest | None | Uses | - - - - > |
Association | Weak | Independent | Has-a (loose) | ———> |
Aggregation | Medium | Independent | Has-a | ◇———> |
Composition | Strong | Dependent | Has-a (strong) | ♦———> |
Inheritance | Strong | N/A | Is-a | ——— |
Realisation | Medium | N/A | Behaves-as | - - - |
Common pitfalls
While class relationships are powerful tools in OOP, they can also lead to common pitfalls if not used carefully. Here are some common issues and how to avoid them:
Overuse of inheritance
- Problem: Creating deep inheritance hierarchies that are hard to understand and maintain.
- Solution: Prefer composition over inheritance. Use inheritance only for genuine “is-a” relationships.
Tight coupling
- Problem: Creating strong dependencies between classes, making the system rigid and hard to change.
- Solution: Use interfaces and dependency injection to reduce coupling. Depend on abstractions rather than concrete classes.
God objects
- Problem: Creating classes that try to do too much, violating the Single Responsibility Principle.
- Solution: Break large classes into smaller, more focused classes. Use composition to bring functionality together.
Circular dependencies
- Problem: Creating mutual dependencies between classes, leading to complex and hard-to-maintain code.
- Solution: Refactor to remove circular dependencies. Consider using interfaces or introducing a new class to break the cycle.
Leaky abstractions
- Problem: Exposing implementation details through interfaces or base classes.
- Solution: Design interfaces and base classes carefully. Hide implementation details and expose only what’s necessary.
Inappropriate intimacy
- Problem: Classes that know too much about each other’s internal details.
- Solution: Encapsulate data and behaviour. Use public interfaces to interact between classes.
Brittle base classes
- Problem: Changes to base classes breaking derived classes in unexpected ways.
- Solution: Design base classes for extension. Document how derived classes should interact with base classes.
Diamond problem in multiple inheritance
- Problem: Ambiguity in method resolution when a class inherits from two classes with a common ancestor.
- Solution: Avoid multiple inheritance if possible. Use interfaces or mixins instead.
Overuse of getters and setters
- Problem: Breaking encapsulation by providing unrestricted access to internal state.
- Solution: Use meaningful methods that represent behaviors rather than exposing internal data directly.
Violation of Liskov Substitution Principle
- Problem: Derived classes that can’t be used interchangeably with their base classes.
- Solution: Ensure that derived classes truly represent specialisations of their base classes. Use composition if the “is-a” relationship doesn’t hold.
By being aware of these pitfalls and following best practices, you can create more robust and maintainable object-oriented designs.
Conclusion
Key takeaways:
- Association is a general relationship between classes.
- Aggregation represents a whole-part relationship where parts can exist independently.
- Composition is a stronger whole-part relationship where parts cannot exist independently.
- Inheritance represents an “is-a” relationship and promotes code reuse.
- Realisation is about implementing interfaces and focusing on behaviour.
- Dependency is a weak, often temporary relationship between classes.
Remember that good object-oriented design is not just about using these relationships, but about using them appropriately. Always consider the SOLID principles and the “composition over inheritance” guideline.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley.
- Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley.
- Phillips, D. (2018). Python 3 Object-Oriented Programming (3rd ed.). Packt Publishing.
- Lott, S. F. (2020). Object-Oriented Python: Master OOP by Building Games and GUIs. No Starch Press.
- Booch, G., Rumbaugh, J., & Jacobson, I. (2005). The Unified Modeling Language User Guide (2nd ed.). Addison-Wesley.
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
3 - The four pillars
At the heart of OOP lie four fundamental concepts: Encapsulation, Inheritance, Polymorphism, and Abstraction. These concepts, often referred to as the “four pillars” of OOP, form the foundation upon which complex software systems are built. In this guide, we will delve deep into each of these concepts, exploring their definitions, implementations, and practical applications. We’ll use Python, a language known for its clarity and versatility, to demonstrate these concepts in action. Whether you’re a beginner just starting your programming journey or a seasoned professional looking to refresh your knowledge, this article aims to provide valuable insights and a deeper understanding of OOP principles.
3.1 - Encapsulation
Encapsulation is often described as the first pillar of object-oriented programming. It is the mechanism of bundling the data (attributes) and the methods (functions) that operate on the data within a single unit or object. This concept is also often referred to as data hiding because the object’s internal representation is hidden from the outside world.
The importance of encapsulation lies in several key aspects:
- Data protection: By controlling access to object data through methods, we can ensure that the data remains consistent and valid.
- Modularity: Encapsulation allows objects to be self-contained, making it easier to understand and maintain code.
- Flexibility: The internal implementation can be changed without affecting other parts of the code that use the object.
- Reduced complexity: By hiding the details of internal workings, encapsulation reduces the complexity of the overall system from an external perspective.
Implementation in Python
Python provides several mechanisms to implement encapsulation. Let’s explore these with examples:
1. Using private attributes
In Python, we can create private attributes by prefixing the attribute name with double underscores (__
). This triggers name mangling, which makes the attribute harder to access from outside the class.
class BankAccount:
def __init__(self, account_number, balance):
self.__account_number = account_number # Private attribute
self.__balance = balance # Private attribute
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return True
return False
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return True
return False
def get_balance(self):
return self.__balance
# Usage
account = BankAccount("1234567890", 1000)
print(account.get_balance()) # Output: 1000
account.deposit(500)
print(account.get_balance()) # Output: 1500
account.withdraw(200)
print(account.get_balance()) # Output: 1300
# This will raise an AttributeError
# print(account.__balance)
In this example:
__account_number
and __balance
are private attributes.- We provide public methods (
deposit
, withdraw
, get_balance
) to interact with these private attributes. - Direct access to
__balance
from outside the class will raise an AttributeError
exception.
2. Using properties
Python’s @property
decorator allows us to define methods that can be accessed like attributes, providing a more Pythonic way of implementing getters and setters.
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value > 0:
self._radius = value
else:
raise ValueError("Radius must be positive")
@property
def area(self):
return 3.14159 * self._radius ** 2
# Usage
circle = Circle(5)
print(circle.radius) # Output: 5
print(circle.area) # Output: 78.53975
circle.radius = 7
print(circle.radius) # Output: 7
print(circle.area) # Output: 153.93791
# This will raise a ValueError
# circle.radius = -1
In this example:
_radius
is a protected attribute (single underscore is a convention for protected attributes in Python).- The
radius
property provides get and set access to _radius
with validation. - The
area
property is read-only and calculated on-the-fly.
Benefits and best practices
The benefits of encapsulation are numerous:
- Improved maintainability: Changes to the internal implementation don’t affect external code that uses the class.
- Enhanced security: Private attributes can’t be accidentally modified from outside the class.
- Flexibility in implementation: You can change how data is stored or calculated without changing the public interface.
- Better abstraction: Users of the class don’t need to know about its internal workings.
Best practices for encapsulation in Python include:
- Use private attributes (double underscore prefix) for data that should not be accessed directly from outside the class.
- Provide public methods or properties for controlled access to internal data.
- Use properties instead of get/set methods for a more Pythonic approach.
- Document the public interface clearly, including any side effects of methods.
Let’s look at a more complex example that demonstrates these practices:
class Employee:
def __init__(self, name, salary):
self.__name = name
self.__salary = salary
self.__projects = []
@property
def name(self):
return self.__name
@property
def salary(self):
return self.__salary
@salary.setter
def salary(self, value):
if value > 0:
self.__salary = value
else:
raise ValueError("Salary must be positive")
def add_project(self, project):
"""
Add a project to the employee's project list.
:param project: string representing the project name
"""
self.__projects.append(project)
def remove_project(self, project):
"""
Remove a project from the employee's project list.
:param project: string representing the project name
:return: True if project was removed, False if not found
"""
if project in self.__projects:
self.__projects.remove(project)
return True
return False
@property
def project_count(self):
return len(self.__projects)
def __str__(self):
return f"Employee: {self.__name}, Salary: ${self.__salary}, Projects: {self.project_count}"
# Usage
emp = Employee("John Doe", 50000)
print(emp.name) # Output: John Doe
print(emp.salary) # Output: 50000
emp.add_project("Project A")
emp.add_project("Project B")
print(emp.project_count) # Output: 2
emp.salary = 55000
print(emp) # Output: Employee: John Doe, Salary: $55000, Projects: 2
emp.remove_project("Project A")
print(emp.project_count) # Output: 1
# This will raise an AttributeError
# print(emp.__projects)
This example demonstrates:
- Private attributes (
__name
, __salary
, __projects
) - Properties for controlled access (
name
, salary
, project_count
) - Public methods for manipulating private data (
add_project
, remove_project
) - Clear documentation of method behaviour
- A custom
__str__
method for a nice string representation of the object
By following these practices, we create a class that is both flexible and robust, embodying the principle of encapsulation.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
3.2 - Inheritance
Inheritance is a fundamental concept in object-oriented programming that allows a new class to be based on an existing class. The new class, known as the derived or child class, inherits attributes and methods from the existing class, called the base or parent class. This mechanism promotes code reuse and establishes a relationship between classes.
Key aspects of inheritance include:
- Code reusability: Inheritance allows us to reuse code from existing classes, reducing redundancy and promoting efficient development.
- Hierarchical classification: It enables the creation of class hierarchies, representing relationships and commonalities among objects.
- Extensibility: New functionality can be added to existing classes without modifying them, following the open-closed principle.
- Polymorphism: Inheritance is a prerequisite for runtime polymorphism (which we’ll discuss in detail later).
Types of inheritance
There are several types of inheritance, though not all programming languages support all types. The main types are:
- Single inheritance: A derived class inherits from a single base class.
- Multiple inheritance: A derived class inherits from multiple base classes.
- Multilevel inheritance: A derived class inherits from another derived class.
- Hierarchical inheritance: Multiple derived classes inherit from a single base class.
- Hybrid inheritance: A combination of two or more types of inheritance.
Python supports all these types of inheritance. Let’s explore each with examples.
Single inheritance
Single inheritance is the simplest form of inheritance, where a class inherits from one base class.
class Animal:
def __init__(self, species):
self.species = species
def make_sound(self):
pass
class Dog(Animal):
def __init__(self, name):
super().__init__("Canine")
self.name = name
def make_sound(self):
return "Woof!"
# Usage
dog = Dog("Buddy")
print(f"{dog.name} is a {dog.species}") # Output: Buddy is a Canine
print(dog.make_sound()) # Output: Woof!
In this example:
Animal
is the base class with a generic make_sound
method.Dog
is derived from Animal
, inheriting its attributes and methods.Dog
overrides the make_sound
method with its own implementation.- We use
super().__init__()
to call the initialiser of the base class.
Multiple inheritance
Multiple inheritance allows a class to inherit from multiple base classes.
class Flyer:
def fly(self):
return "I can fly!"
class Swimmer:
def swim(self):
return "I can swim!"
class Duck(Animal, Flyer, Swimmer):
def __init__(self, name):
Animal.__init__(self, "Aves")
self.name = name
def make_sound(self):
return "Quack!"
# Usage
duck = Duck("Donald")
print(f"{duck.name} is a {duck.species}") # Output: Donald is a Aves
print(duck.make_sound()) # Output: Quack!
print(duck.fly()) # Output: I can fly!
print(duck.swim()) # Output: I can swim!
Here, Duck
inherits from Animal
, Flyer
, and Swimmer
, combining attributes and methods from all three.
Multilevel inheritance
In multilevel inheritance, a derived class inherits from another derived class.
class Mammal(Animal):
def __init__(self, species, is_warm_blooded=True):
super().__init__(species)
self.is_warm_blooded = is_warm_blooded
def give_birth(self):
return "Giving birth to live young"
class Cat(Mammal):
def __init__(self, name):
super().__init__("Feline")
self.name = name
def make_sound(self):
return "Meow!"
# Usage
cat = Cat("Whiskers")
print(f"{cat.name} is a {cat.species}") # Output: Whiskers is a Feline
print(cat.make_sound()) # Output: Meow!
print(cat.give_birth()) # Output: Giving birth to live young
print(f"Is warm-blooded: {cat.is_warm_blooded}") # Output: Is warm-blooded: True
In this example, Cat
inherits from Mammal
, which in turn inherits from Animal
, forming a multilevel inheritance chain.
Hierarchical inheritance
Hierarchical inheritance involves multiple derived classes inheriting from a single base class.
class Bird(Animal):
def __init__(self, species, can_fly=True):
super().__init__(species)
self.can_fly = can_fly
class Parrot(Bird):
def __init__(self, name):
super().__init__("Psittacine", can_fly=True)
self.name = name
def make_sound(self):
return "Squawk!"
class Penguin(Bird):
def __init__(self, name):
super().__init__("Spheniscidae", can_fly=False)
self.name = name
def make_sound(self):
return "Honk!"
# Usage
parrot = Parrot("Polly")
penguin = Penguin("Pingu")
print(f"{parrot.name} can fly: {parrot.can_fly}") # Output: Polly can fly: True
print(f"{penguin.name} can fly: {penguin.can_fly}") # Output: Pingu can fly: False
Here, both Parrot
and Penguin
inherit from Bird
, which demonstrates hierarchical inheritance.
Hybrid inheritance
Hybrid inheritance is a combination of multiple inheritance types. Let’s create a more complex example to illustrate this:
class Terrestrial:
def walk(self):
return "Walking on land"
class Aquatic:
def swim(self):
return "Swimming in water"
class Amphibian(Animal, Terrestrial, Aquatic):
def __init__(self, species):
Animal.__init__(self, species)
def adapt(self):
return "Can survive both on land and in water"
class Frog(Amphibian):
def __init__(self, name):
super().__init__("Anura")
self.name = name
def make_sound(self):
return "Ribbit!"
# Usage
frog = Frog("Kermit")
print(f"{frog.name} is a {frog.species}") # Output: Kermit is a Anura
print(frog.make_sound()) # Output: Ribbit!
print(frog.walk()) # Output: Walking on land
print(frog.swim()) # Output: Swimming in water
print(frog.adapt()) # Output: Can survive both on land and in water
This example demonstrates hybrid inheritance:
Frog
inherits from Amphibian
Amphibian
inherits from Animal
, Terrestrial
, and Aquatic
- This creates a combination of multilevel and multiple inheritance
Considerations
Inheritance offers several advantages. However, there are also important considerations:
- Complexity: Deep inheritance hierarchies can become difficult to understand and maintain.
- Tight coupling: Inheritance creates a tight coupling between base and derived classes.
- Fragile base class problem: Changes in the base class can unexpectedly affect derived classes.
- Diamond problem: In multiple inheritance, conflicts can arise if two base classes have methods with the same name.
To address these considerations:
- Prefer composition over inheritance when possible.
- Keep inheritance hierarchies shallow and focused.
- Use abstract base classes to define clear interfaces.
- Be cautious with multiple inheritance and resolve conflicts explicitly.
Let’s visualise the inheritance relationships we’ve discussed using an UML class diagram:
classDiagram
Animal <|-- Mammal
Animal <|-- Bird
Mammal <|-- Dog
Mammal <|-- Cat
Bird <|-- Parrot
Bird <|-- Penguin
Animal <|-- Amphibian
Terrestrial <|-- Amphibian
Aquatic <|-- Amphibian
Amphibian <|-- Frog
class Animal {
+species: str
+make_sound()
}
class Mammal {
+is_warm_blooded: bool
+give_birth()
}
class Bird {
+can_fly: bool
}
class Amphibian {
+adapt()
}
class Terrestrial {
+walk()
}
class Aquatic {
+swim()
}
This diagram illustrates the inheritance relationships between the classes we’ve discussed, showing both single and multiple inheritance.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
3.3 - Polymorphism
Polymorphism is a core concept in object-oriented programming that allows objects of different classes to be treated as objects of a common base class. The term “polymorphism” comes from Greek, meaning “many forms”. In OOP, it refers to the ability of a single interface to represent different underlying forms (data types or classes).
Polymorphism enables writing flexible and reusable code by allowing us to work with objects at a more abstract level, without needing to know their specific types.
There are two main types of polymorphism in object-oriented programming:
Compile-time polymorphism (Static polymorphism)
- Achieved through method overloading.
- Resolved at compile time.
Runtime polymorphism (Dynamic polymorphism)
- Achieved through method overriding.
- Resolved at runtime.
Python primarily supports runtime polymorphism, as it is a dynamically typed language. However, we can demonstrate concepts similar to compile-time polymorphism as well.
Let’s explore different aspects of polymorphism in Python:
Duck typing
Python uses duck typing, which is a form of polymorphism. The idea is: “If it walks like a duck and quacks like a duck, then it must be a duck.” In other words, Python cares more about the methods an object has than the type of the object itself.
class Duck:
def speak(self):
return "Quack quack!"
class Dog:
def speak(self):
return "Woof woof!"
class Cat:
def speak(self):
return "Meow meow!"
def animal_sound(animal):
return animal.speak()
# Usage
duck = Duck()
dog = Dog()
cat = Cat()
print(animal_sound(duck)) # Output: Quack quack!
print(animal_sound(dog)) # Output: Woof woof!
print(animal_sound(cat)) # Output: Meow meow!
In this example, animal_sound()
works with any object that has a speak()
method, regardless of its class.
Method overriding
Method overriding is a key aspect of runtime polymorphism. It occurs when a derived class defines a method with the same name as a method in its base class.
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
# Usage
shapes = [Rectangle(5, 4), Circle(3)]
for shape in shapes:
print(f"Area: {shape.area()}")
# Output:
# Area: 20
# Area: 28.27431
Here, Rectangle
and Circle
both override the area()
method of the Shape
class.
Operator overloading
Python allows operator overloading, which is a form of compile-time polymorphism. It allows the same operator to have different meanings depending on the operands.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
# Usage
v1 = Vector(2, 3)
v2 = Vector(3, 4)
v3 = v1 + v2
print(v3) # Output: Vector(5, 7)
Here, we’ve overloaded the +
operator for our Vector
class.
Abstract base classes
Python’s abc
module provides infrastructure for defining abstract base classes, which are a powerful way to define interfaces in Python.
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def make_sound(self):
pass
class Dog(Animal):
def make_sound(self):
return "Woof!"
class Cat(Animal):
def make_sound(self):
return "Meow!"
# Usage
def animal_sound(animal):
return animal.make_sound()
dog = Dog()
cat = Cat()
print(animal_sound(dog)) # Output: Woof!
print(animal_sound(cat)) # Output: Meow!
# This will raise a TypeError
# animal = Animal()
Abstract base classes cannot be instantiated and force derived classes to implement certain methods, ensuring a consistent interface.
Real-world Applications
Polymorphism is widely used in real-world applications:
- GUI frameworks: Different widgets (buttons, text boxes) can respond to common events (click, hover) in their own ways.
- Database interfaces: Different database systems can implement a common interface for querying, allowing applications to work with various databases without changing code.
- Plugin systems: Applications can work with plugins through a common interface, regardless of the specific implementation of each plugin.
- Game development: Different game entities can share common behaviors (move, collide) but implement them differently.
Here’s a simple example of a plugin system:
class Plugin(ABC):
@abstractmethod
def process(self, data):
pass
class UppercasePlugin(Plugin):
def process(self, data):
return data.upper()
class ReversePlugin(Plugin):
def process(self, data):
return data[::-1]
class Application:
def __init__(self):
self.plugins = []
def add_plugin(self, plugin):
self.plugins.append(plugin)
def process_data(self, data):
for plugin in self.plugins:
data = plugin.process(data)
return data
# Usage
app = Application()
app.add_plugin(UppercasePlugin())
app.add_plugin(ReversePlugin())
result = app.process_data("Hello, World!")
print(result) # Output: !DLROW ,OLLEH
This example demonstrates how polymorphism allows the Application
class to work with different plugins through a common interface.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
3.4 - Abstraction
Abstraction is the process of hiding the complex implementation details and showing only the necessary features of an object. It’s about creating a simplified view of an object that represents its essential characteristics without including background details or explanations.
Key aspects of abstraction include:
- Simplification: Abstraction reduces complexity by hiding unnecessary details.
- Focusing on essential features: It emphasises what an object does rather than how it does it.
- Separation of concerns: It allows separating the interface of a class from its implementation.
- Modularity: Abstraction promotes modular design by defining clear boundaries between components.
Abstract classes and interfaces
In many object-oriented languages, abstraction is implemented through abstract classes and interfaces. While Python doesn’t have a built-in interface concept, we can achieve similar functionality using abstract base classes. Python’s abc
module provides infrastructure for defining abstract base classes:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
# Usage
# shapes = [Shape()] # This would raise TypeError
shapes = [Rectangle(5, 4), Circle(3)]
for shape in shapes:
print(f"Area: {shape.area()}, Perimeter: {shape.perimeter()}")
# Output:
# Area: 20, Perimeter: 18
# Area: 28.27431, Perimeter: 18.84954
In this example:
Shape
is an abstract base class that defines the interface for all shapes.Rectangle
and Circle
are concrete classes that implement the Shape
interface.- We can’t instantiate
Shape
directly, but we can use it as a common type for all shapes.
Implementing abstraction in Python
While abstract base classes provide a formal way to define interfaces in Python, abstraction can also be achieved through convention and documentation. Here’s an example of abstraction without using ABC
:
class Database:
def connect(self):
raise NotImplementedError("Subclass must implement abstract method")
def execute(self, query):
raise NotImplementedError("Subclass must implement abstract method")
class MySQLDatabase(Database):
def connect(self):
print("Connecting to MySQL database...")
def execute(self, query):
print(f"Executing MySQL query: {query}")
class PostgreSQLDatabase(Database):
def connect(self):
print("Connecting to PostgreSQL database...")
def execute(self, query):
print(f"Executing PostgreSQL query: {query}")
def perform_database_operation(database):
database.connect()
database.execute("SELECT * FROM users")
# Usage
mysql_db = MySQLDatabase()
postgres_db = PostgreSQLDatabase()
perform_database_operation(mysql_db)
perform_database_operation(postgres_db)
# Output:
# Connecting to MySQL database...
# Executing MySQL query: SELECT * FROM users
# Connecting to PostgreSQL database...
# Executing PostgreSQL query: SELECT * FROM users
In this example:
Database
is an abstract base class (though not using ABC
) that defines the interface for all database types.MySQLDatabase
and PostgreSQLDatabase
are concrete implementations.perform_database_operation
works with any object that adheres to the Database
interface.
Design principles and patterns
Abstraction is a key component of several important design principles and patterns:
SOLID Principles:
- Single Responsibility Principle (SRP).
- Open/Closed Principle (OCP).
- Liskov Substitution Principle (LSP).
- Interface Segregation Principle (ISP).
- Dependency Inversion Principle (DIP).
Design Patterns:
- Factory method pattern.
- Abstract factory pattern.
- Strategy pattern.
- Template method pattern.
Let’s implement the Strategy Pattern as an example:
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data):
pass
class BubbleSort(SortStrategy):
def sort(self, data):
print("Performing bubble sort")
return sorted(data) # Using Python's built-in sort for simplicity
class QuickSort(SortStrategy):
def sort(self, data):
print("Performing quick sort")
return sorted(data) # Using Python's built-in sort for simplicity
class Sorter:
def __init__(self, strategy):
self.strategy = strategy
def sort(self, data):
return self.strategy.sort(data)
# Usage
data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
bubble_sorter = Sorter(BubbleSort())
print(bubble_sorter.sort(data))
quick_sorter = Sorter(QuickSort())
print(quick_sorter.sort(data))
# Output:
# Performing bubble sort
# [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
# Performing quick sort
# [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
This Strategy Pattern example demonstrates how abstraction allows us to define a family of algorithms, encapsulate each one, and make them interchangeable. The Sorter
class doesn’t need to know the details of how each sorting algorithm works; it just knows that it can call the sort
method on any SortStrategy
object.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!
3.5 - Conclusion
Object-Oriented Programming is a powerful paradigm that provides a way to structure code that closely mirrors real-world entities and their interactions. The four fundamental concepts we’ve explored (encapsulation, inheritance, polymorphism, and abstraction) work together to create flexible, maintainable, and reusable code.
- Encapsulation allows us to bundle data and methods together, hiding internal details and protecting data integrity.
- Inheritance enables code reuse and the creation of hierarchical relationships between classes.
- Polymorphism provides a way to use objects of different types through a common interface, enhancing flexibility and extensibility.
- Abstraction allows us to create simplified models of complex systems, focusing on essential features and hiding unnecessary details.
As you continue your journey in software development, you’ll find that mastering these concepts opens up new ways of thinking about and solving problems. Remember that OOP is not just about syntax or language features - it’s a mindset for modeling complex systems and managing complexity in software.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
Cheers for making it this far! I hope this journey through the programming universe has been as fascinating for you as it was for me to write down.
We’re keen to hear your thoughts, so don’t be shy – drop your comments, suggestions, and those bright ideas you’re bound to have.
Also, to delve deeper than these lines, take a stroll through the practical examples we’ve cooked up for you. You’ll find all the code and projects in our GitHub repository learn-software-engineering/examples-programming.
Thanks for being part of this learning community. Keep coding and exploring new territories in this captivating world of software!