Part 4 - Structured, Modular, Object-Oriented Programming
Part 4 - Structured, Modular, Object-Oriented Programming
PART IV:
STRUCTURED, MODULAR & OBJECT-ORIENTED PROGRAMMING
Academic Year Prof. Abdelhakim HANNOUSSE — hannousse.abdelhakim@univ-guelma.dz
2024/2025 Computer Science Department, 8 May 1945 University of Guelma, Algeria
Outline
Structured Procedural Programming ◼ Instance Attributes and Methods
Motivating Example ◼ Static Attributes and Methods
Definition and Usage of Functions ◼ Access Modifiers
Default parameter values ◼ Relationships between Classes
Inter-function calling ◼ Association
◼ Aggregation
Recursive functions
◼ Composition
Modular Programming
◼ Inheritance (Simple, Multiple)
Modules — Definition and Usage in Python
◼ Abstract and Inner Classes
Packages — Definition and Usage in Python
Introduction to Design Patterns
Libraries — Definition and Usage in Python
SOLID Principles
Object-Oriented Programming Paradigm (OOP)
What are Design Patterns?
Development process with OOP
Origin and Bible of Design Patterns
Basic Concepts
Categories of Design Patterns
◼ Classes and Constructors
Useful Design Patterns
◼ Objects
Introduction
3
As software systems have grown in complexity, the need for better ways to design,
write, and maintain code has led to the evolution of several programming paradigms.
While traditional structured programming was a major leap forward from unstructured
code, it has shown several limitations:
Reusability — Code reuse is limited and duplication is common.
Examples:
print_line()
result = add(5, 3)
average = calculate_average(students, "score")
Default Parameter Values
9
Default parameters allow Python functions to be called with fewer arguments than they
define by assigning default values to some parameters.
def function_name(param1, param2=default_value):
# function body
# Case 2: With discount - Output: Total (with 15% discount): 255.0 DZD
print(f"Total (with 15% discount): {calculate_total(100, 3, 15)} DZD")
Inter-function Calling
10
Inter-function calling refers to the process where one function calls another, including
built-in functions, to perform a subtask.
A function f1 calls another function f2 to delegate a specific task.
After the call, f1 can: use the result from f2, call another function, perform further
operations, or complete its execution. # f2 function
def checkout(price):
# f1 function tax = calculate_tax(price) # f2 calls f1
def calculate_tax(price): total = price + tax
return price * 0.15 print(f"Total with tax: {total}")
Python has a default recursion limit of ~1000 calls to prevent stack overflow. This limit
can be increased using sys.setrecursionlimit(limit_value), but it should be done
cautiously.
Modular Programming
Modules, Packages, Libraires
Code Organization — Modules, Packages, Libraries
13
Built-in modules come bundled with Python, without external downloads or installations.
They provide a wide range of ready-to-use functions that help performing common
programming tasks more efficiently.
Examples of built-in modules are:
math : provides a collection of mathematical functions and constants.
import math
print(math.sqrt(64)) # Output: 8.0
print(math.pi) # Output: 3.141592653589793
import random
print(random.randint(1, 10)) # Output: e.g., 2
Modules in Python (3/4)
16
The __init__.py file can simply be left empty — this is enough to tell Python that
the folder is a package.
from package_example import calc
print(calc.add(3, 5)) # Output: 8
print(calc.power(2, 4)) # Output: 16
The __init__.py file also allows controlling the package interface by:
Making specific modules or functions directly accessible from the package level.
from .calc import add, power from package_example import add, power
print(add(3, 5)) # Output: 8
print(power(2, 4)) # Output: 16
Packages in Python (3/3)
20
Exposing only the essential functions to users for a cleaner, more organized API.
# __init__.py
Python’s libraries provide a rich set of built-in packages and modules that offer
powerful, ready-to-use functionality for a wide range of programming tasks — all
without requiring any additional installation.
SciPy : provides efficient tools for scientific and technical computing, including
modules for optimization, integration, interpolation, linear algebra, and statistics.
matplotlib : a powerful library for creating static, animated, and interactive
visualizations.
Tkinter : useful for creating graphical user interfaces (GUIs) in Python applications.
beautifulsoup4 : a library used for parsing HTML and XML and extracting useful
data from web pages.
Scikit-learn : a comprehensive machine learning library with a variety of
algorithms for classification, regression, clustering, etc.
Libraries in Python (3/5) : SciPy
23
print("Area under the curve:", result) # Output : Area under the curve: 9.000000000000002
print("Estimated error:", error) # Output : Estimated error: 9.992007221626411e-14
Libraries in Python (4/5) : matplotlib
24
# Sample data
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
sales = [2500, 2700, 3000, 3200, 3100, 3300]
import tkinter as tk
# Create the main application window
root = tk.Tk()
root.title("Greeting App")
root.geometry("300x150")
# Function to be called when button is clicked
def greet_user():
name = name_entry.get()
greeting_label.config(text=f"Hello, {name}!")
# Create a label, entry box, and button
name_label = tk.Label(root, text="Enter your name:")
name_label.pack(pady=5)
name_entry = tk.Entry(root)
name_entry.pack(pady=5)
greet_button = tk.Button(root, text="Validate", command=greet_user)
greet_button.pack(pady=5)
greeting_label = tk.Label(root, text="")
greeting_label.pack(pady=10)
# Start the event loop
root.mainloop()
Object-Oriented Programming Paradigm
Object-oriented paradigm
27
Objects collaborate and interact to achieve the overall functionality of the system.
Data structures and methods are no longer treated as separate entities, as they are in
procedural programming—in OOP, they are combined in a single unit named object.
The data required by an object is contained within the object itself (encapsulation).
Access to the data is controlled, promoting data hiding and safer interaction.
Unit Testing: Perform unit tests on each class to ensure that methods function correctly
and meet the specifications.
Integration Testing: Verify the consistency and interaction between the different
classes and objects when combined.
Process Iteration: Repeat the iterative development process based on feedback from
tests and evaluations, adjusting the design as needed.
Documentation: Document the classes, methods, and overall system architecture for
clear understanding and future maintenance.
Optimization: Optimize the code and structures to enhance performance and the
efficiency of the final program.
Finalization: Complete the final program, ensuring its stability, reliability, and
alignment with the initial requirements.
Basic Concepts — Classes
30
Concrete class
icon
Class name
Method access
modifier icon method_name(parameters) : Type
Methods
Basic Concepts — Classes in Python
32
class Car:
def __init__(self, plate, brand, model, power, year):
self.license_plate = plate
def brake(self, decrement):
self.brand = brand
if self.state == "Running" and self.speed > 0:
self.model = model
self.speed -= decrement
self.horsepower = power
if self.speed < 0:
self.yearOfManufacture = year
self.speed = 0
self.speed = 0
def get_speed(self):
self.state = "Stopped"
return self.speed
def get_license_plate(self):
def start(self):
return self.licensePlate
if self.state == "Stopped":
def get_brand(self):
self.state = "Running“
return self.brand
def accelerate(self, increment):
def get_model(self):
if self.state == "Running":
return self.model
self.speed += increment
def get_year_of_manufacture(self):
def stop(self):
return self.yearOfManufacture
if self.state == "Running":
def get_state(self):
self.state = "Stopped"
return self.state
self.speed = 0
Basic Concepts — Class Constructors
33
The constructor is a special method named __init__, defined within a class, and
is commonly used to initialize the object's attributes assigning them initial values.
class Car:
def __init__(self, plate, brand, model, power, year):
self.license_plate = plate
self.brand = brand
self.model = model
self.horsepower = power
self.year_of_manufacture = year
self.speed = 0
self.state = "Stopped"
It is automatically called when a new object is created from the class and invoked
only once, at the time of object instantiation.
If no constructor is defined, Python provides a default constructor that takes no
arguments (except self). However, it is recommended to define an explicit
constructor to ensure meaningful and controlled initialization of object attributes.
Basic Concepts — Objects (1/2)
34
Each object has its own distinct state (i.e., its attribute values), but all objects of the
same class share the same structure and behavior defined by the class.
Each object has a unique identity that distinguishes it from other objects, even if they
belong to the same class. This identity allows referencing and tracking an object in the
program. In Python, you can get the identity using the id() function:
object_name = class_name()
print(id(object_name)) # Output: Unique memory address (identity)
Basic Concepts — Objects (2/2)
35
The internal details of an object are hidden from the outside. The object's data is
typically accessed through methods, enforcing encapsulation.
This ensures that the object’s attributes are not directly modified or accessed without
going through defined methods (getter/setter).
Each object can be developed, tested, and maintained independently of others.
When an object is no longer referenced anywhere, the memory space it occupies
can and must be freed.
Python uses automatic garbage collection to destroy objects that are no longer
referenced. Python tracks how many references (variables, data structures) point to
an object. When the reference count reaches zero, the object is no longer needed
and becomes eligible for destruction.
Basic Concepts — Instance Attributes and Methods
36
class Car:
Class attributes/Static attributes, are wheels = 4 # Class attribute
shared by all instances of a class ➤ If a registration_fee = 500 # Class attribute
def __init__(self, brand, model, year):
class attribute is modified by one object, self.brand = brand
the change will be reflected across all self.model = model
self.year = year
other instances. They are defined inside @staticmethod
the class body but outside of any instance def honk():
method, and can be accessed using either print("Beep beep!")
# Example usage:
the class name or an instance. car1 = Car("Geely", "Coolray", 2024)
Static methods are defined using car2 = Car("Fiat", "500X", 2025)
print(Car.wheels) # Output: 4
@staticmethod decorator and do not print(car1.registration_fee) # Output: 500
receive self as a parameter. They Car.registration_fee = 700
print(car1.registration_fee) # Output: 700
behave like regular functions but print(car2.registration_fee) # Output: 700
grouped in class for organization. Car.honk() # Output: Beep beep!
car1.honk() # Output: Beep beep!
Basic Concepts — Access Modifiers
38
class Teacher:
Association def __init__(self, name):
self.name = name
self.students = []
class Student:
def __init__(self, name):
self.name = name
# Example usage:
teacher = Teacher("Prof. Hannousse")
Student1, student2 = Student("Ali"), Student("Kawther")
teacher.supervise(student1)
teacher.supervise(student2)
print("Teacher:", teacher.name)
print("Supervised students:")
for s in teacher.students:
print("-", s.name)
Basic Concepts —
Relationships between classes in UML & Python (2/7)
42
class Teacher:
Aggregation def __init__(self, name):
self.name = name
class Department:
def __init__(self, name):
self.name = name
self.teachers = []
def add_teacher(self, teacher):
if isinstance(teacher, Teacher):
self.teachers.append(teacher)
# Example usage:
t1 = Teacher("Prof. Hannousse")
t2 = Teacher("Prof. Gadhri")
cs_dept = Department("Computer Science")
cs_dept.add_teacher(t1)
cs_dept.add_teacher(t2)
print("Department:", cs_dept.name)
print("Teachers in department:")
for teacher in cs_dept.teachers:
print("-", teacher.name)
Basic Concepts —
Relationships between classes in UML & Python (3/7)
43
class Department:
Composition def __init__(self, name):
self.name = name
class University:
def __init__(self, name):
self.name = name
self.departments = []
# Example usage:
univ = University("Annaba University")
univ.add_department("Computer Science")
univ.add_department("Mathematics")
print("University:", univ.name)
print("Departments:")
for dept in univ.departments:
print("-", dept.name)
Basic Concepts —
Relationships between classes in UML & Python (4/7)
44
class Teacher:
Inheritance (Simple Inheritance) def __init__(self, name, subject):
self.name = name
self.subject = subject
class TemporaryTeacher(Teacher):
def __init__(self, name, subject, duration):
super().__init__(name, subject)
self.contract_duration = duration
class PermanentTeacher(Teacher):
def __init__(self, name, subject, salary, benefits):
super().__init__(name, subject)
self.salary = salary
self.benefits = benefits
# Example usage:
teach = Teacher("Dr. Khoualdia", "Physics")
temp = TemporaryTeacher("Mr. Merdaci", "Chemistry", "6 M")
perm = PermanentTeacher("Ms. Amir", "Biology", 55000,
["Health Insurance", "Pension"])
Basic Concepts —
Relationships between classes in UML & Python (5/7)
45
class Phone:
def power_on(self):
print("Phone is powering on...")
# Example usage:
device = SmartPhone()
device.power_on() # Output: Phone is powering on ...
Camera is powering on ...
Basic Concepts — Abstract Classes
48
An abstract class serves as a blueprint for other classes by declaring abstract methods
= methods without a body (no implementation) that must be implemented by derived
concrete classes.
An abstract class is useful for:
Providing a basic structure: They define a common structure and shared behavior
for a group of related classes, promoting code reuse and consistency.
Enforcing implementation: Abstract methods act as placeholders that ensure
subclasses provide specific functionality, enforcing a contract for inheritance.
Preventing direct instantiation: An abstract class cannot be instantiated directly.
This ensures only fully implemented subclasses are used to create objects.
An abstract class can also contain class and instance attributes, concrete methods, a
constructor, and inherit methods from other super-classes concrete or abstract.
Basic Concepts — Abstract Classes in Python
49
An abstract class must inherit from the ABC class defined in the abc module. This marks
it as abstract and allows abstract methods.
Abstract classes can include concrete methods, constructors, and attributes to provide
shared functionality.
A subclass must implement all abstract methods, if any, to become concrete. Otherwise,
it remains abstract and can't be instantiated.
Any attempt to create an instance of a class with unimplemented abstract methods
raises a TypeError.
Class and static abstract methods can be defined using @abstractclassmethod and
@abstractstaticmethod or by combining @classmethod and @abstractmethod
decorators to enforce these in subclasses.
Basic Concepts —
Abstract Classes in UML & Python (1/2)
50
def get_brand(self):
return self.__brand
def get_model(self):
return self.__model
@abstractmethod
Abstract def start(self):
methods pass
@abstractmethod
def stop(self):
pass
Basic Concepts —
Abstract Classes in UML & Python (2/2)
51
class Truck(Vehicle):
def __init__(self, brand, model, cargo_capacity):
super().__init__(brand, model)
self.cargo_capacity = cargo_capacity
def start(self):
print(f"{self.__brand} {self.__model} is starting.")
def stop(self):
print(f"{self.__brand} {self.__model} is stopping.")
# Example usage:
truck = Truck("Volvo", "FH16", 25.0)
truck.start() # Output: Volvo FH16 is starting.
truck.stop() # Output: Volvo FH16 is stopping.
Basic Concepts — Inner Classes (1/2)
52
An inner class is a class defined within (inside) another class. It is also sometimes
referred to as a nested class.
class OuterClass:
class InnerClass:
pass
Inner classes are beneficial when they have no use outside of the enclosing class ➤
restricting their access, thereby enhancing their encapsulation.
Inner classes do not have direct access to the outer class’s instance attributes or
methods unless explicitly passed.
They simplify the structure of the code by grouping related functionalities within
the enclosing class.
Inner class object creation is made through the outer class:
outer = OuterClass()
inner = OuterClass.InnerClass()
Basic Concepts — Inner Classes (2/2)
53
Prefer many specific interfaces over from abc import ABC, abstractmethod
a single general-purpose one. class Worker(ABC): class Human(Worker):
@abstractmethod def work(self):
Eliminates Unused Dependencies def work(self): print("Human working")
– No more forcing classes to pass
@abstractmethod
def eat(self):
print("Human eating")
implement irrelevant methods def eat(self): def sleep(self):
pass print("Human sleeping")
Reduces Code Bloat – Smaller @abstractmethod
interfaces = leaner classes. def sleep(self):
pass
Maintainability – Changes to one
interface don’t affect unrelated class Robot(Worker):
def work(self):
classes. print("Robot working")
Flexibility – Classes can mix and def eat(self): # Robots don't eat!
raise NotImplementedError("Robots don't eat!")
match interfaces like building def sleep(self): # Robots don't sleep!
blocks. raise NotImplementedError("Robots don't sleep!")
ISP: Interface Segregation Principle (2/2)
63
Prefer many specific interfaces over from abc import ABC, abstractmethod
a single general-purpose one. class Workable(ABC): class Robot(Workable):
@abstractmethod def work(self):
Eliminates Unused Dependencies def work(self): print("Robot working")
– No more forcing classes to pass
class Eatable(ABC):
implement irrelevant methods @abstractmethod
def eat(self):
Reduces Code Bloat – Smaller pass
interfaces = leaner classes. class Sleepable(ABC):
@abstractmethod
Maintainability – Changes to one def sleep(self):
interface don’t affect unrelated pass
class Human(Workable, Eatable, Sleepable):
classes. def work(self):
Flexibility – Classes can mix and print("Human working")
def eat(self):
match interfaces like building print("Human eating")
blocks. def sleep(self):
print("Human sleeping")
DIP: Dependency Inversion Principle (1/2)
64
Objective: Ensures that a class has only one instance and provides a global access
point to that instance.
Example use case: The system requires a single window manager, a single print
spooler, a single access point to a database engine, etc.
https://refactoring.guru/design-patterns
Singleton Pattern (2/2)
71
Structure:
class Singleton:
__instance = None
Objective: Allow a class to dynamically choose how to solve a problem from a family
of possible solutions.
Example use case: You want to have different variants of an algorithm inside an
object (e.g., sorting algorithms), and be able to switch from one algorithm to another
during runtime.
https://refactoring.guru/design-patterns
Strategy Pattern (2/2)
73
class ConcreteStrategyA(Strategy):
def execute(self, data):
return data * 2
class ConcreteStrategyB(Strategy):
def execute(self, data):
return data + 5
class Client:
def __init__(self, strategy: Strategy):
if __name__ == "__main__": self.__strategy = strategy
client = Client (ConcreteStrategyA()) def set_strategy(self, strategy: Strategy):
print(client.execute_strategy(10)) # Output: 20 self.__strategy = strategy
client.set_strategy(ConcreteStrategyB()) def execute_strategy(self, data):
print(client.execute_strategy(10)) # Output: 15 return self.__strategy.execute(data)
Chain of Responsibility Pattern (1/3)
74
Structure:
Chain of Responsibility Pattern (3/3)
76
Objective: Defines a 1-to-N dependency relationship between objects such that if one
object changes its state, all dependent objects are notified and automatically updated.
Example use case: Administrators need to be notified of every change in the
database.
https://refactoring.guru/design-patterns
Observer Pattern (2/3)
78
Structure:
Observer Pattern (3/3)
79
https://refactoring.guru/design-patterns
Adapter Pattern (2/3)
81
class Target(ABC):
@abstractmethod
def request(self, data):
pass
class Adaptee:
def specific_request(self, data):
return data[::-1]
class Adapter(Target):
def __init__(self, adaptee: Adaptee):
self._adaptee = adaptee
def request(self, data):
return self._adaptee.specific_request(data)
class Client:
def __init__(self, target: Target):
self.__target = target
def client_code(self, data):
print(self.__target.request(data))
Adapter Pattern (3/3)
82
# Output:
# Client: I can work fine with the CompatibleClass object:
# Special behavior of the Compatible Object.
# Client: The Adaptee class has a different interface
# Client: I can only work with it via the Adapter:
# Special behavior of the Adaptee.
Composite Pattern (1/3)
83
https://refactoring.guru/design-patterns
Composite Pattern (2/3)
84
Structure:
Composite Pattern (3/3)
85
Structure:
Proxy Pattern (3/3)
88
class Proxy(Subject):
def __init__(self, real_subject: RealSubject):
self.__real_subject = real_subject
def request(self):
print("Proxy: Checking access prior request.")
print("Proxy: Access granted.")
self.__real_subject.request()
print("Proxy: Logging the time of request.") # Output:
# Client: Executing with a real subject:
class Client: # RealSubject: Handling request.
# Client: Executing with a proxy:
def __init__(self, subject: Subject):
# Proxy: Checking access prior request.
self._subject = subject # Proxy: Access granted.
def client_code(self): # RealSubject: Handling request.
self._subject.request() # Proxy: Logging the time of request.
Decorator Pattern (1/3)
89
https://refactoring.guru/design-patterns
Decorator Pattern (2/3)
90
Structure:
Decorator Pattern (3/3)
91
class ConcreteDecoratorA(Decorator):
def operation(self):
return f"DecoratorA({self._component.operation()})"
# Output:
# Client: Basic component:
class ConcreteDecoratorB(Decorator): # ConcreteComponent
def operation(self): # Client: Decorated component:
return f"DecoratorB({self._component.operation()})" # DecoratorB(DecoratorA(ConcreteComponent))
Conclusion
92