0% found this document useful (0 votes)
6 views94 pages

Part 4 - Structured, Modular, Object-Oriented Programming

The document outlines the curriculum for early-stage training of doctoral students in programming fundamentals and techniques, focusing on structured, modular, and object-oriented programming. It emphasizes the evolution of programming paradigms to address the complexities of modern software systems and includes detailed sections on functions, modules, and packages in Python. The content is designed to enhance students' understanding of programming concepts and improve code organization, reusability, and maintainability.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views94 pages

Part 4 - Structured, Modular, Object-Oriented Programming

The document outlines the curriculum for early-stage training of doctoral students in programming fundamentals and techniques, focusing on structured, modular, and object-oriented programming. It emphasizes the evolution of programming paradigms to address the complexities of modern software systems and includes detailed sections on functions, modules, and packages in Python. The content is designed to enhance students' understanding of programming concepts and improve code organization, reusability, and maintainability.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 94

‫الجمهــوريـة الجزائريــة الديمقراطيـــة الشعبيـــة‬

ⵜⴰⴳⴷⵓⴷⴰ ⵜⴰⴷⵣⴰⵢⵔⵉⵜ ⵜⴰⵎⴰⴳⴷⴰⵢⵜ ⵜⴰⵖⴻⵔⴼⴰⵏⵜ


People's Democratic Republic of Algeria
‫وزارة التعليم العالي والبحث العلمي‬
ⴰⵖⵍⵉⴼ ⵏ ⵓ ⵙⴻⵍⵎⴻⴷ ⵓⵏⵏⵉⴳ ⴷ ⵓⵏⴰⴷⵉ ⵓⵙⵙⵏⴰⵏ
Ministry of Higher Education and Scientific Research
‫اللجنة الوطنية لإلشراف ومتابعة تنفيذ برنامج التكوين األولي لطلبة الدكتوراه الطور الثالث‬
National Steering and Monitoring Commission for the Implementation of the Early-stage Training Program for Third-Stage Doctoral Students
‫اللجنة الوطنية البيداغوجية لمادة أساسيات وتقنيات البرمجة‬
National Pedagogical Committee for Programming Fundamentals and Techniques

EARLY-STAGE TRAINING OF DOCTORAL STUDENTS


SUBJECT: PROGRAMMING FUNDAMENTALS & TECHNIQUES

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.

 Maintainability — Code is less modular, tends to become tightly coupled and


harder to change as the system grows.
 Scalability — Code is harder to scale as it often lacks clear abstractions for
managing large systems.
 Modern applications — from web services to mobile apps — demand architectures
that support frequent changes, multiple developers, and complex logic. These needs are
better addressed by procedural, modular, and object-oriented paradigms.
Structured Procedural Programming
Motivating Example
5

 Problems with Traditional Programming: students = [


{"name": "Ali", "score": 10},
 Code tends to be long and unstructured, especially {"name": "Ahmed", "score": 19},
{"name": "Kawther", "score": 15},
in large programs. {"name": "Amel", "score": 8}
]
 Hard to read and understand due to lack of clear
# Calculate average
organization. total = 0
for s in students:
 Difficult to debug or test parts of the program in total += s["score"]
average = total / len(students)
isolation.
# Find above-average students
 Low reusability – logic is often duplicated rather above_average = []
than reused. for s in students:
if s["score"] > average:
 Changes in one part of the code may
above_average.append(s)

unintentionally affect other parts. # Print results


print("Average Score:", average)
 Tends to use global variables, leading to tightly print("Students above average:")
for s in above_average:
coupled logic. print(s["name"], "-", s["score"])
Motivating Example
6

 Benefits of Structural Programming: def calculate_average(data_list, field):


 Code is organized into logical blocks total = sum(e[field] for e in data_list)
return total / len(data_list)
(functions).
def find_above_average(data_list, field, average):
 Improved readability through return [e for e in data_list if e[field] > average]

meaningful names and clear structure. def print_results(average, above_average):


print("Average :", average)
 Easier to debug and test individual print("Above average:")
for r in above_average:
functions in isolation. print(" ".join(f"{k}: {v}" for k, v in r.items()))

 Encourages code reuse by using the if __name__ == "__main__":


same function in multiple places. students = [
{"name": "Ali", "score": 10},
 Enhances maintainability – each
{"name": "Ahmed", "score": 19},
{"name": "Kawther", "score": 15},
function handles a specific task. ]
{"name": "Amel", "score": 8}

 Avoids redundant code by avg = calculate_average(students, "score")


ab_avg = find_above_average(students, "score", avg)
abstracting repeated logic. print_results(avg, ab_avg)
Definition and Usage of Functions (1/2)
7

 A function is a block of reusable code that performs a specific task.


 It helps break a large program into smaller, manageable parts.
 Functions improve modularity, readability, and reusability.
 In Python, functions are defined using the def keyword followed by the name of a
function, a pair of parenthesis (with or without parameters), a colon “:”, and an
function’s logic: def function_name(parameters):
# block of code
# optional return statement
 You can use the return keyword to send a result back to the caller.
 Examples:
def calculate_average(data_list, field):
def print_line():
total = 0
print("-" * 30)
for e in data_list:
def add(a, b): total += e[field]
return a + b return total / len(data_list)
Definition and Usage of Functions (2/2)
8

 Functions are not executed automatically—they only run when called.


 A function is executed by writing its name followed by parentheses containing the
required parameter values.
 Calling a Function Without a Return Value: the function performs an action but does not return
a result to the caller. It is called simply to execute a task, such as printing or updating something.
function_name(parameters)
 Calling a Function With a Return Value: the function returns a value using the return statement.
This value can be stored in a variable or used in an expression.
value = function_name(parameters)

 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

 Parameters with default values must come after non-default ones.


def calculate_total(price, quantity, discount=0):
total = price * quantity
return total - (total * (discount / 100))

# Case 1: No discount - Output: Total (no discount): 300.0 DZD


print(f"Total (no discount): {calculate_total(100, 3)} DZD")

# 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.

 The called function f2 executes and optionally returns a result to f1.

 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}")

checkout(100) # Output: Total with tax: 115.0


 It allows for breaking down complex tasks into smaller, manageable parts (smaller
functions). Each function can focus on a specific responsibility, making code easier to
read, test, and reuse.
Recursive Functions
11

 A recursive function is a function that calls itself to solve a problem. Recursion is


particularly useful when a problem can be divided into smaller subproblems of the
same type. It should include:
 Base Case: A condition that stops calling itself (prevents infinite recursion). Without it,
Python raises a RecursionError (stack overflow).
 Recursive Case: The function calls itself with a modified input to progress toward the
base case.
def recursive_function(params): def factorial(n):
if base_condition: # base case if n == 0: # base case
return result return 1
else: # recursive case else: # recursive case
return recursive_function(smaller_params) return n * factorial(n - 1)

 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

 While functions help break large code into smaller and


manageable pieces, proper code organization is still
essential for more scalability, reusability, and
Library
maintainability.
 Python offers several ways to structure programs code using Package
higher-level organizational units which help manage and
organize larger codebases logically and efficiently: Module

 Module: A single Python file that contains code (functions,


variables, etc.).
Function
 Package: A collection of related modules stored in a
directory.
 Library: A collection of packages and modules that
provide specialized functionality for a particular domain.
Modules in Python (1/4)
14

 A module in Python is simply a file that # calc.py


def add(a, b):
contains Python code — typically functions, return a + b
and variables. It allows: def subtract(a, b):
 Breaking a program into smaller, organized
return a - b

files. def multiply(a, b):


return a * b
 Organizing related functions together.
def divide(a, b):
 Reusing code across multiple programs. if b == 0:
raise ValueError(“Divizion by zero.")
 Any .py file is a module. return a / b

 A module can be imported and used in def power(a, b):


return a ** b
another Python file using the import statement.
# another python file
 Functions and variables defined in the module import calc
are accessed by prefixing them with the print(calc.add(3, 5)) # Output: 8
module name, using dot “.” notation. print(calc.power(2, 4)) # Output: 16
Modules in Python (2/4)
15

 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

 random : implements pseudo-random number generator functions.

import random
print(random.randint(1, 10)) # Output: e.g., 2
Modules in Python (3/4)
16

 datetime : provides a collection of functions manipulating dates and times.


import datetime
print(datetime.date.today()) # Output: 2025-04-30
print(datetime.timedelta(weeks=1, days=3, hours=1)) # Output: 10 days, 1:00:00
 os : includes functions enabling interactions with the operating systems.
import os
print(os.getcwd()) # Output: C:\...
directory = "FTP_Course"
parent_dir = os.getcwd()
path = os.path.join(parent_dir, directory) # /home/User/Documents/FTP_Course
os.mkdir(path) # Creates the directory in path
 sys : provides functions to access system-specific parameters.
import sys
print(sys.version) # Output: 3.13.0 ...
print(sys.platform) # Output: win32
sys.exit() # Exits the program
Modules in Python (4/4)
17

 statistics : includes functions that calculate basic statistical measures.


import statistics
data = [10, 20, 20, 30, 40, 40, 50]
print(statistics.mean(data)) # Output : 30
print(statistics.median(data)) # Output : 30
print(statistics.mode(data)) # Output : 20

 re : implements functions supporting regular expressions for advanced text


searching.
import re
def is_valid_email(email):
pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
return bool(re.match(pattern, email))

print(is_valid_email("user@example.com")) # Output: True


print(is_valid_email("user@com")) # Output: False
Packages in Python (1/3)
18

 A package is a directory that contains a collection of related Python modules.


 Packages help organize code by dividing projects into folders and subfolders based
on specific features (e.g., database, tools), making the codebase easier to manage.
 Packages group related functionality, like having a math_tools package with
geometry.py, algebra.py, and statistics.py.
 Packages act as namespaces, allowing the same module name in different packages
without conflicts. For creating a package:
1. Create a directory or folder that will hold the package files.
2. Add an __init__.py file inside the created folder. It is used to tell Python that the
folder should be treated as a package.
3. Add modules (.py files containing functions) to the created folder.
4. Use the functions included in the package modules by importing the package and
calling the appropriate functions.
Packages in Python (2/3)
19

 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.

# __init__.py # another Python file

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

from .calc import add


__all__ = ['add']

# Output: another Python file

from package_example import add, power


print(add(3, 5)) # Output: 8
print(power(2, 4)) # Cannot import name ‘power’ from package_example
Libraries in Python (1/5)
21

 A library is a collection of related packages and modules that provide specialized


functionality.
 Setup.py : a script used by tools like setuptools to
distribute the library. It includes metadata such as the
library name, version, author, description, and what modules
to include.
 REDME.md : a markdown file that gives an overview of the
library: what it does, how to install and use it. This is the first
thing users see when they view it on GitHub or PyPI.
 requirements.txt : lists the external packages the
library depends on to run properly. It is used by developers
and tools to automatically install all the required packages.
 LICENSE : specifies the legal terms under which the library
code can be used, modified, and shared.
Libraries in Python (2/5)
22

 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

from scipy.optimize import fsolve


# Define the equation as a function
def equation(x):
return x**3 - 4*x + 1 𝑥 3 + 4𝑥 + 1 = 0
# Solve the equation near x = 2
solution = fsolve(equation, 2)
print("Solution:", solution[0]) # Output : Solution: 1.8608058531117035
from scipy.integrate import quad

# Define the function to integrate


def f(x): 𝑏
return x**2
න 𝑥 2 𝑑𝑥
# Compute the definite integral from 0 to 3 𝑎
result, error = quad(f, 0, 3)

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

import matplotlib.pyplot as plt

# Sample data
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
sales = [2500, 2700, 3000, 3200, 3100, 3300]

# Create the plot


plt.plot(months, sales, marker='o',
color='blue',
linestyle='-')

# Add title and labels


plt.title('Monthly Sales Report')
plt.xlabel('Month')
plt.ylabel('Sales (Million DZD)')
plt.grid(True)

# Show the plot


plt.show()
Libraries in Python (5/5) : Tkinter
25

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

 Object-oriented design involves thinking in terms of objects and their interactions.


 A system is broken down into a set of objects with clearly defined roles.

 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.

 Object-oriented design relies on an isomorphic correspondence between the real


world and the digital world.
 This isomorphism ensures consistency between the reality of a system and how it is
modeled in software, thereby facilitating code comprehension and maintenance.
 Principles such as encapsulation, inheritance, and polymorphism are used to capture
similarities and relationships between real-world entities in software design.
Development process with the OOP (1/2)
28

 Requirements Analysis: Understand the system requirements and identify the


various functionalities needed.
 Object Identification: Identify and define the system's objects, which represent
entities or concepts related to the required functionalities.
 Interface Method Definition: Develop the interface methods for the objects,
describing the actions each object can perform.
 Class Design: Declare the classes necessary to implement the objects and their
methods. This step involves defining the attributes and behaviors of each class.
 Method Development: Implement the methods defined in each class, ensuring they
adhere to the interface specifications.
 Class Integration: Combine the various developed classes to form functional
subsystems or modules.
Development process with the OOP (2/2)
29

 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

 A class is a blueprint or template for creating objects.


 A class provides an abstraction by defining a generic model for objects. It allows
developers to focus on relevant aspects without concerning themselves with complex
details.
 A class is an entity that groups data (attributes) and operations/functions (methods)
associated with a set or group of objects sharing the same structure.
 Attributes, also called properties, represent the characteristics or data that each
object of the class will possess. These attributes describe the state of the object.
 Methods are functions associated with the class that specify the behavior of its
objects. They define the actions that objects can perform.
Basic Concepts — Classes in UML
31

Concrete class
icon
Class name

Attribute access Attributes


attribute_name : Type
modifier icon

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

 An object is an instance of a class, which defines the structure and behavior.


 Creating an object from a class is called instantiation. In Python, this happens simply
by calling the class name like a function:
Object_name = class_name(parameters)
 Example:
car1 = Car(“01234-125-23", “Geely", "Coolray", 172, 2025)

 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

 Instance Attributes are defined using class Car:


def __init__(self, brand, year, fuel):
self inside the __init__() self.brand = brand # Instance attribute
constructor and belong only to the self.year = year # Instance attribute
self.fuel = fuel # Instance attribute
specific object (instance) where each
instance can have different values for def refuel(self, amount): # Instance method
these attributes. self.fuel += amount

 Instance methods are defined with def get_brand(self): # Instance method


return self.brand
self as the first parameter, and can
access or modify instance attributes, def get_year(self): # Instance method
and must be called on an object. return self.year

Although it is passed automatically def get_fuel(self): # Instance method


by Python when calling an instance return self.fuel
# Example usage:
method on an object, self must be car1 = Car("Volkswagen", 2023, 20)
declared in the method definition. car2 = Car("Geely", 2025, 35)
Basic Concepts — Static Attributes and Methods
37

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

 Access modifiers in Python are followed by naming conventions to indicate the


intended level of visibility for class members (methods and attributes).
 There are three main access levels commonly used in Python:
 public: members declared without underscores are considered as public and
accessible from any class. They have the widest scope - ( : attributes, : methods)
 protected: members prefixed with a single underscore (_) are treated as protected
and they are accessible within the same class and its subclasses - ( ).
 private: members prefixed with double underscores (__) are accessible only within
the same class. They have the most restrictive scope - ( : attributes, : methods).
class Car: # Example usage:
def __init__(self): car = Car()
self.speed = 0 # public print("Speed:", car.speed) # Speed: 0
self._state = "Stopped" # protected print("State:", car._state) # State: Stopped
self.__vin = "123ABC" # private print("VIN:", car.__vin) # AttributeError
Basic Concepts — Relationships between classes (1/2)
39

 In object-oriented programming, an object communicates with other objects to use their


associated functionalities and the services provided by these objects.
 There are different relationships between objects in a system, and these relationships
are generalized and expressed in the form of links between classes:
 Association: Represents a connection between two classes, indicating that an object
of one class requires the execution of methods from an object of another class. It
does not imply ownership or a shared lifetime between the objects.
◼ Example: a teacher teaches a list of students ➤ An association between the two
classes Teacher and Student.
 Aggregation: A specific form of association that indicates a part-whole relationship
between two classes of objects, but the objects can have independent lifetimes.
◼ Example: a Department class can be in aggregation with the Teacher class,
indicating that the department includes several teachers.
Basic Concepts — Relationships between classes (2/2)
40

 Composition: A stronger form of aggregation that indicates that one object is


responsible for the lifecycle of other objects.
◼ Example: a University class can be in composition with the Department class,
indicating that the department is an essential part of the university.
 Inheritance: Represents an "is-a" relationship, indicating that one class inherits the
characteristics (attributes and methods) of another class.
◼ In Python, one child class can inherit from one or multiple parent classes.
◼ Polymorphism, enabled by inheritance, allows objects of different classes to be
treated through a common interface, typically via a shared base class.
◼ Subclasses can provide their own specific implementations of methods inherited
from their super-classes — a concept known as method overriding.
◼ Example: a PermanentTeacher class can inherit from the Teacher class, because a
permanent teacher is a specific type of teacher.
Basic Concepts —
Relationships between classes in UML & Python (1/7)
41

class Teacher:
 Association def __init__(self, name):
self.name = name
self.students = []

def supervise(self, student):


self.students.append(student)

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 = []

def add_department(self, department_name):


department = Department(department_name)
self.departments.append(department)

# 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

 Inheritance (Multiple Inheritance): One


child class inherits from one or multiple
parents, enabling the combination of
behaviors from multiple classes.
 Example: A SmartPhone class can
inherit from both Camera and Phone
class Camera:
classes. def take_photo(self):
 Python uses a Method Resolution Order print("Taking a photo...")

(MRO) algorithm to determine the order class Phone:


in which classes are searched for a def make_call(self, number):
print(f"Calling {number}...")
method.
 A class always appears before its class SmartPhone(Camera, Phone):
def browse_internet(self):
parents. print("Browsing the internet...")
Basic Concepts —
Relationships between classes in UML & Python (6/7)
46

 If a class inherits from multiple class Camera:


parents, their order in the class def power_on(self):
definition matters. Left-to-Right print("Camera is powering on...")

order in the class definition class Phone:


determines priority. def power_on(self):
print("Phone is powering on...")
 The MRO of a class is obtained by
merging MROs of their parents class SmartPhone(Camera, Phone):
pass
while preserving their order.
# Example usage:
device = SmartPhone()
device.power_on() # Output: Camera is powering on ...
print(SmartPhone.__mro__) # Output:
# (<class '__main__.SmartPhone’>,
# <class '__main__.Camera’>,
# <class '__main__.Phone’>,
# <class 'object'>)
Basic Concepts —
Relationships between classes in UML & Python (7/7)
47

 The conflict can also be resolved class Camera:


by overriding conflicting methods def power_on(self):
in the class. print("Camera is powering on...")

class Phone:
def power_on(self):
print("Phone is powering on...")

class SmartPhone(Camera, Phone):


def power_on(self):
Phone.power_on(self)
Camera.power_on(self)

# 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

from abc import ABC, abstractmethod


Abstract class Vehicle(ABC):
class def __init__(self, brand, model):
self.__brand = brand
self.__model = model

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

 To restrict direct instantiation of an class Outer:


def __init__(self, name):
inner class and enforce creation self.name = name
through the outer class: self._inner_object

1. Prefix the inner class name with an def create_inner(self, data):


inner = self._Inner(data, _from_outer=True)
underscore to mark it as internal. self._inner_object = inner
2. Provide a factory method in the return inner

outer class to control instance def get_inners(self):


creation. return self._inner_objects

3. Add a hidden flag (e.g., class _Inner:


def __init__(self, data, _from_outer=False):
_from_outer=False) to the inner if not _from_outer:
class constructor and raise an raise ValueError(“Error message.")
self.data = data
exception if it's not set, preventing
unauthorized use. def display(self):
return f"Inner data: {self.data}"
Introduction to Design Patterns
SOLID Principles
55

 SOLID is an acronym that represents five key principles of object-oriented design.


These principles help developers create systems that are easier to understand and
maintain, more resilient to changes, and less prone to bugs:
 S – Single Responsibility Principle (SRP): A class should focus on doing one thing
and do it well, rather than handling multiple unrelated tasks (have only one job).
 O – Open/Closed Principle (OCP): New functionality can be added without
changing existing code.
 L – Liskov Substitution Principle (LSP): If class B is a subtype of class A, then objects
of A should be replaceable with objects of B without breaking the program.
 I – Interface Segregation Principle (ISP): Clients should not be forced to depend on
interfaces they do not use.
 D – Dependency Inversion Principle (DIP): Rely on abstractions, not concrete
implementations.
SRP: Single Responsibility Principle (1/2)
56

 A class should have only one


reason to change, meaning it class Order:
should have only one job or def __init__(self, order_id, customer, items):
self.order_id = order_id
responsibility. self.customer = customer
 Maintainability – Changes to one self.items = items
responsibility don’t risk breaking def calculate_total(self):
unrelated functionalities. total = 0;
for item in self.items:
 Readability – Smaller, focused total += item['price’] * item['quantity']
classes are easier to understand. return total
 Reusability – Single-purpose def process_payment(self, payment_method):
classes can be reused in different # Payment gateway logic here...
contexts.
def save_to_database(self):
 Testability – Testing a class with a # Database logic here...
single responsibility is simpler.
SRP: Single Responsibility Principle (2/2)
57

 A class should have only one class Order:


reason to change, meaning it def __init__(self, order_id, customer, items):
self.order_id = order_id
should have only one job or self.customer = customer
responsibility. self.items = items
 Maintainability – Changes to one
def calculate_total(self):
responsibility don’t risk breaking total = 0;
unrelated functionalities. for item in self.items:
total += item['price’] * item['quantity']
 Readability – Smaller, focused return total
classes are easier to understand.
class PaymentProcessor:
 Reusability – Single-purpose def process_payment(self, order, payment_method):
classes can be reused in different # Payment gateway logic...
contexts. class OrderRepository:
 Testability – Testing a class with a def save_to_database(self, order):
# Database logic...
single responsibility is simpler.
OCP: Open/Closed Principle (1/2)
58

 Software entities (classes, modules,


functions) should be open for extension
but closed for modification.
 Fewer bugs – Since old code remains class PaymentProcessor:
unchanged, there’s less chance of def process_payment(self,
amount,
breaking existing functionality. payment_type):
 Modularity & Maintainability –
if payment_type == "credit_card":
# Credit-card payment logic here ...
Encourages small, reusable elif payment_type == "paypal":
components (like plugins) instead of # Paypal payment logic here ...
elif payment_type == "crypto":
monolithic code. # Bitcoin payment logic here ...
 Testability – Existing logic stays
stable, so tests don’t need frequent
updates.
OCP: Open/Closed Principle (2/2)
59

 Software entities (classes, modules, from abc import ABC, abstractmethod


functions) should be open for extension
class PaymentMethod(ABC):
but closed for modification. @abstractmethod
 Fewer bugs – Since old code remains def process(self, amount: float):
pass
unchanged, there’s less chance of
breaking existing functionality. class CreditCardPayment(PaymentMethod):
def process(self, amount):
 Modularity & Maintainability –
# CreditCard payment logic here ...
Encourages small, reusable
components (like plugins) instead of class PayPalPayment(PaymentMethod):
def process(self, amount):
monolithic code. # PayPal payment logic here ...
 Testability – Existing logic stays
class CryptoPayment(PaymentMethod):
stable, so tests don’t need frequent def process(self, amount):
updates. # Bitcoin payment logic here ...
LSP: Liskov Substitution Principle (1/2)
60

 Subclasses must behave


consistently with their base
classes. class Vehicle:
def __init__(self, fuel_level):
 Prevents Surprise Failures – self.fuel_level = fuel_level
Subclasses should work def refuel(self, amount):
anywhere the parent class """Add fuel to the vehicle"""
works. self.fuel_level += amount
print(f"Added {amount} units of fuel.")
 Maintains Logical Consistency
class ElectricVehicle(Vehicle):
– Inheritance should represent def refuel(self, amount):
true "is-a" relationships. raise NotImplementedError("EVs charge, not refuel!")

 Enables Safe Polymorphism – def fuel_up_vehicle(vehicle: Vehicle, amount):


Functions accepting base types vehicle.refuel(amount)
should work seamlessly with
subtypes.
LSP: Liskov Substitution Principle (2/2)
61

 Subclasses must behave from abc import ABC, abstractmethod


class FuelVehicle(ABC):
consistently with their base @abstractmethod
classes. def refuel(self, amount):
pass
 Prevents Surprise Failures – class ChargeableVehicle(ABC):
@abstractmethod
Subclasses should work def charge(self, minutes):
anywhere the parent class pass
works. class GasolineCar(FuelVehicle):
def refuel(self, amount):
 Maintains Logical Consistency print(f"Pumping {amount} liters of gasoline")
class ElectricCar(ChargeableVehicle):
– Inheritance should represent def charge(self, minutes):
true "is-a" relationships. print(f"Charging for {minutes} minutes")

 Enables Safe Polymorphism – def operate_vehicle(vehicle):


Functions accepting base types if isinstance(vehicle, FuelVehicle):
vehicle.refuel(20)
should work seamlessly with elif isinstance(vehicle, ChargeableVehicle):
subtypes. vehicle.charge(30)
ISP: Interface Segregation Principle (1/2)
62

 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

 High-level modules should not depend on


low-level modules. Both should depend on
abstractions.
class MySQLConnection:
 Decouples High-Level & Low-Level Code def connect(self):
print("Connecting to MySQL")
– High-level modules (business logic) no
longer depend on low-level modules (like class App:
databases, APIs). Both depend on def __init__(self):
self.db = MySQLConnection()
abstractions (interfaces), making the
system more modular.
 Reusability – The same high-level module
can work with multiple implementations.
 Maintainability – Low-level changes
don’t propagate to high-level code.
DIP: Dependency Inversion Principle (2/2)
65

 High-level modules should not depend on class Database(ABC):


@abstractmethod
low-level modules. Both should depend on def connect(self):
abstractions. pass
 Decouples High-Level & Low-Level Code class MySQLConnection(Database):
– High-level modules (business logic) no def connect(self):
longer depend on low-level modules (like print("Connecting to MySQL")

databases, APIs). Both depend on class PostgreSQLConnection(Database):


abstractions (interfaces), making the def connect(self):
print("Connecting to PostgreSQL")
system more modular.
 Reusability – The same high-level module class App:
def __init__(self, db: Database):
can work with multiple implementations.
self.db = db
 Maintainability – Low-level changes
def start(self):
don’t propagate to high-level code.
self.db.connect()
What are Design Patterns ?
66

 Design patterns are reusable, generic solutions to recurring problems encountered in


software design. These patterns offer proven solutions to common problems while
promoting:
 Reusability: Design patterns provide ready-to-use solutions for common problems.
They allow developers to reuse tested designs rather than creating solutions from
scratch.
 Maintainability: By using design patterns, developers can design more modular and
maintainable systems.
 Communication: By using well-known pattern names, team members can
communicate effectively about the software design.
Design Patterns vs. Data Structures
67

Data Structures Design Patterns


Allow storing, organizing, and They provide proven patterns for structuring,
manipulating data efficiently. organizing, and interacting with code.
Often involving more specific They focus on high-level concepts and
implementation details. relationships between objects in systems.
They offer solutions to calculation They offer solutions to architectural problems:
problems: sorting, searching, etc. how to swap sorting algorithms without
disrupting the rest of the code.
They focus on efficiency in terms of time They focus on reusability and maintainability of
and space. the code.
Origin and Bible of Design Patterns
68

 The history of design patterns dates back to the work


of architect Christopher Alexander in the 1960s.
Christopher Alexander was an architect who
developed principles of architectural and urban
design. His ideas were published in 1977 in a book
titled A Pattern Language.
 In 1994, four authors—Erich Gamma, Richard Helm,
Ralph Johnson, and John Vlissides, often referred to as
the "Gang of Four" (GoF)—published a book titled
Design Patterns: Elements of Reusable Object-
Oriented Software, which introduced the concept of
design patterns in software development.
 In this book, 23 design patterns were described.
Categories of Design Patterns
69

 Design patterns are classified into three basic categories:


 Creational Patterns: These design patterns focus on how to create objects in a
system. They provide flexible and reusable mechanisms for instantiating objects
while isolating the details of their creation, composition, and representation.
◼ Example: Singleton
 Behavioral Patterns: These design patterns focus on how objects interact and
distribute responsibility among them.
◼ Example: Strategy, Chain of Responsibility, Observer
 Structural Patterns: These design patterns focus on how to compose classes or
objects to form more complex structures.
◼ Example: Adapter, Composite, Proxy, Decorator
Singleton Pattern (1/2)
70

 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

def __new__(cls, *args, **kwargs):


if not cls.__instance:
cls.__instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls.__instance

# Test the Singleton


singleton1 = Singleton()
singleton2 = Singleton()

print(singleton1 is singleton2) # Output: True (both are the same instance)


Strategy Pattern (1/2)
72

 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

from abc import ABC, abstractmethod


 Structure: class Strategy(ABC):
@abstractmethod
def execute(self, data):
pass

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

 Objective: Facilitates the flow of requests within


a chain of handlers.
 Each handler in the chain gets a chance to
process the request. A handler can either
handle it fully, partially, or pass it along.
 In many real-world implementations, only one
handler processes the request and then the
chain ends. But the pattern also allow for
multiple handlers to process the same request if
desired.
 Use case example: Data in a program need to
be processed by multiple objects, each applying
https://refactoring.guru/design-patterns
its own logic before passing it to the next (image
filtering, text processing).
Chain of Responsibility Pattern (2/3)
75

 Structure:
Chain of Responsibility Pattern (3/3)
76

from abc import ABC, abstractmethod class ConcreteHandlerC(Handler):


class Handler(ABC): def handle(self, request):
def __init__(self): if request.endswith("B"):
self._next_handler = None request += "C"
def set_next(self, handler): if self._next_handler:
self.__next_handler = handler return self._next_handler.handle(request)
return handler # Allow chaining return request
@abstractmethod
def handle(self, request): class Client:
pass def execute(self, request):
handler_a = ConcreteHandlerA()
class ConcreteHandlerA(Handler): handler_b = ConcreteHandlerB()
def handle(self, request): handler_c = ConcreteHandlerC()
if request == "R": handler_a.set_next(handler_b).set_next(handler_c)
request += "A" self.__handler = handler_a
if self._next_handler: return self.__handler.handle(request)
return self._next_handler.handle(request)
return request # Example usage:
if __name__ == "__main__":
class ConcreteHandlerB(Handler): client = Client()
def handle(self, request): # Test requests
if request.endswith("A") : print(client.execute("R")) # Output: RABC
request += "B" print(client.execute("A")) # Output: ABC
if self._next_handler: print(client.execute("B")) # Output: BC
return self._next_handler.handle(request) print(client.execute("C")) # Output: C
return request print(client.execute("O")) # Output: O
Observer Pattern (1/3)
77

 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

from abc import ABC, abstractmethod class ConcreteObserverA(Observer):


class Observer(ABC): def update(self, data):
@abstractmethod print(f"ObserverA received: {data}")
def update(self, data):
pass class ConcreteObserverB(Observer):
def update(self, data):
class Subject(ABC): print(f"ObserverB received: {data}")
def __init__(self):
self._observers = [] # Example Usage
def attach(self, observer: Observer): if __name__ == "__main__":
self._observers.append(observer) subject = ConcreteSubject()
def detach(self, observer: Observer):
self._observers.remove(observer) observer_a = ConcreteObserverA()
def notify(self, data): observer_b = ConcreteObserverB()
for observer in self._observers: subject.attach(observer_a)
observer.update(data) subject.attach(observer_b)
subject.set_state("Update 1")
class ConcreteSubject(Subject): subject.detach(observer_a)
def __init__(self): subject.set_state("Update 2")
super().__init__()
self.__state = None # Output:
def set_state(self, state): # ObserverA received: Update 1
self.__state = state # ObserverB received: Update 1
self.notify(state) # ObserverB received: Update 2
Adapter Pattern (1/3)
80

 Objective: Allows collaboration between objects with incompatible interfaces.


 Use case example: Useful in situations where an existing class provides a service, but
there is an incompatibility between the offered interface and the one the client would
like to have.

https://refactoring.guru/design-patterns
Adapter Pattern (2/3)
81

from abc import ABC, abstractmethod


 Structure: class CompatibeClass:
def request(self, data):
return data[::-1]

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

 Structure: # Example usage:


if __name__ == "__main__":
print("Client: I can work fine with the CompatibleClass object:")
compatible = CompatibeClass()
client = Client(compatible)
client.client_code(".tcejbO elbitapmoC eht fo roivaheb laicepS")

print("Client: The Adaptee class has a different interface:")


adaptee = Adaptee()

print("Client: I can only work with it via the Adapter:")


adapter = Adapter(adaptee)
client = Client(adapter)
client.client_code(".eetpadA eht fo roivaheb laicepS")

# 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

 Objective: Compose objects into tree structures to represent part-whole hierarchies.


 Use case example: The program processes simple as well as complex elements
uniformly (e.g., handling files and folders in a file system).

https://refactoring.guru/design-patterns
Composite Pattern (2/3)
84

 Structure:
Composite Pattern (3/3)
85

from abc import ABC, abstractmethod class Client:


class Component(ABC): def __init__(self, component: Component):
self.__component = component
def __init__(self, name):
def client_code(self):
self.name = name self.__component.operation()
@abstractmethod
def operation(self, indent=0): # --- Client Code ---
pass if __name__ == "__main__":
leaf1 = Leaf("Leaf 1")
leaf2 = Leaf("Leaf 2")
class Leaf(Component):
leaf3 = Leaf("Leaf 3")
def operation(self, indent=0): composite1 = Composite("Composite 1")
print(' ' * indent + f"- {self.name}") composite2 = Composite("Composite 2")

class Composite(Component): composite1.add(leaf1)


def __init__(self, name): composite1.add(leaf2)
composite2.add(leaf3)
super().__init__(name)
composite2.add(composite1)
self.__children = []
def add(self, component: Component): client = Client(composite2)
self.__children.append(component) client.client_code()
def remove(self, component: Component):
self.__children.remove(component) # Output
# + Composite 2
def operation(self, indent=0):
# - Leaf 3
print(' ' * indent + f"+ {self.name}") # + Composite 1
for child in self.__children: # - Leaf 1
child.operation(indent + 2) # - Leaf 2
Proxy Pattern (1/3)
86

 Objective: Allows the use of a substitute for an object.


It gives control over the original object, enabling
manipulation before or after the request reaches it.
 Use case examples:
 Delay the memory allocation of the object's
resources until they are actually used.
 Store the result of time-consuming operations to
share it with different user objects (acts as a
cache).
 Control access to an object or add additional
functionality around an existing object without https://refactoring.guru/design-patterns
modifying its source code (e.g., protecting access
to the object from malicious entities).
Proxy Pattern (2/3)
87

 Structure:
Proxy Pattern (3/3)
88

from abc import ABC, abstractmethod # -- Tests --


class Subject(ABC): if __name__ == "__main__":
print("Client: Executing with a real subject:")
@abstractmethod
real = RealSubject()
def request(self): client = Client(real)
pass client.client_code()

class RealSubject(Subject): print("Client: Executing with a proxy:")


def request(self): proxy = Proxy(real)
client = Client(proxy)
print("RealSubject: Handling request.")
client.client_code()

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

 Objective: To flexibly attach additional responsibilities to an object.


 Use case example: Extending the functionality of objects, adding behaviors to existing
objects.

https://refactoring.guru/design-patterns
Decorator Pattern (2/3)
90

 Structure:
Decorator Pattern (3/3)
91

from abc import ABC, abstractmethod class Client:


class Component(ABC): def __init__(self, component: Component):
self._component = component
@abstractmethod
def operation(self) -> str: def client_code(self):
pass return self._component.operation()

class ConcreteComponent(Component): # Example usage:


def operation(self): if __name__ == "__main__":
simple = ConcreteComponent()
return "ConcreteComponent"
client = Client(simple)
print("Client: Basic component:",client.client_code())
class Decorator(Component, ABC):
def __init__(self, component: Component): decorated1 = ConcreteDecoratorA(simple)
self._component = component decorated2 = ConcreteDecoratorB(decorated1)
@abstractmethod
client = Client(decorated2)
def operation(self):
print("Client: Decorated component:",client.client_code())
pass

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

 Structured procedural Programming taught how to build programs as a collection


procedures or functions, each ensuring a specific task in the overall system functionality.
 Modular Programming emphasized breaking large programs into independent,
reusable modules, providing better organization, enhanced maintainability and
collaboration across teams.
 Object-Oriented Programming introduced a powerful model based on objects, classes,
inheritance, and encapsulation, allowing to model real-world systems in a natural way.
 Code reuse enabled by modular and object-oriented paradigms leads to more
efficient development and easier maintenance.
 Well-structured programs are easier to debug, extend, and collaborate on.
 Choosing the right paradigm — or combining them effectively — depends on the
problem domain and project requirements.
Further Reading & Resources
93

 Jeff Maynard, Modular programming, Auerbach Publishers, 1972


 Erik Westra, Modular Programming with Python, Packt Publishing, 2016
 Martin Abadi and Luca Cardelli, A Theory of Objects (Monographs in Computer Science), Springer, Corrected
edition, 1996
 Brett McLaughlin, Gary Pollice and David West, Head First Object-Oriented Analysis and Design: A Brain Friendly
Guide to OOA&D, O'Reilly Media, 1st edition, 2007
 Steven F. Lott and Dusty Phillips, Python Object-Oriented Programming: Build robust and maintainable object-
oriented Python applications and libraries, Packt Publishing, 4th edition, 2021
 Mark Lutz, Learning Python: Powerful Object-Oriented Programming, O'Reilly Media; 6th edition, 2025
 Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides and Grady Booch, Design Patterns: Elements of
Reusable Object-Oriented Software, Addison-Wesley Professional, 1st edition, 1994
 Eric Freeman and Elisabeth Robson, Head First Design Patterns: Building Extensible and Maintainable Object-
Oriented Software, O'Reilly Media, 2nd edition, 2021
 Kamon Ayeva and Sakis Kasampalis, Mastering Python Design Patterns: Craft essential Python patterns by following
core design principles, Packt Publishing, 3rd edition, 2024
 Refactoring.Guru, Design Patterns, https://refactoring.guru/design-patterns, [Last visited: May 2025].
The End.

You might also like