0% found this document useful (0 votes)
23 views59 pages

SDA - Lec 13 N

The document outlines various design patterns in software architecture, categorized into creational, structural, and behavioral patterns. It provides detailed explanations and examples of patterns such as Factory Method, Singleton, Adapter, Decorator, Observer, and Strategy. Each pattern is illustrated with practical scenarios and implementation steps to enhance understanding of their applications in software design.

Uploaded by

bsef23m031
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)
23 views59 pages

SDA - Lec 13 N

The document outlines various design patterns in software architecture, categorized into creational, structural, and behavioral patterns. It provides detailed explanations and examples of patterns such as Factory Method, Singleton, Adapter, Decorator, Observer, and Strategy. Each pattern is illustrated with practical scenarios and implementation steps to enhance understanding of their applications in software design.

Uploaded by

bsef23m031
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/ 59

Software Design

and architecture

Fall 2024
Dr. Natalia Chaudhry
Creational patterns Structural patterns Behavioral patterns
Factory Adapter Chain of
Abstract factory Bridge responsibility
Builder Composite Command
Prototype Decorator Iterator
Singleton Façade Mediator
Flyweight Memento
Proxy Observer
State
Strategy
Template method
Visitor
A good link to refer: https://refactoring.guru/design-patterns
Creational patterns
✓ Factory
✓ Singleton
Factory Method is a creational design pattern that provides an
interface for creating objects in a superclass, but allows
subclasses to alter the type of objects that will be created.
• Imagine that you’re creating a logistics management
application. The first version of your app can only
handle transportation by trucks, so the bulk of your
code lives inside the Truck class.

• After a while, your app becomes pretty popular. Each


day you receive dozens of requests from sea
transportation companies to incorporate sea logistics
into the app.
• At present, most of your code is coupled to the Truck class.
Adding Ships into the app would require making changes to the
entire codebase. Moreover, if later you decide to add another
type of transportation to the app, you will probably need to
make all of these changes again.

• As a result, you will end up with pretty nasty code, riddled with
conditionals that switch the app’s behavior depending on the
class of transportation objects.
• The Factory Method pattern suggests that you replace direct
object construction calls (using the new operator) with calls to a
special factory method.
• the objects are still created via the new operator, but it’s being
called from within the factory method.
• Objects returned by a factory method are often referred to as
products.
• At first glance, this change may look pointless: we just moved the
constructor call from one part of the program to another.
• However, consider this: now you can override the factory method in a
subclass and change the class of products being created by the method.
both Truck and Ship classes should implement the Transport interface, which declares a
method called deliver. Each class implements this method differently: trucks deliver
cargo by land, ships deliver cargo by sea. The factory method in the RoadLogistics class
returns truck objects, whereas the factory method in the SeaLogistics class returns
ships.
Factory Method handles object creation, while polymorphism
focuses on behavior at runtime. Factory method delegates object
creation to subclasses while ensuring flexibility and reuse.

What’s new? It decouples object creation from the main logic,


enabling flexibility and scalability
Steps to Implement Factory Method:
1.Create an interface/abstract class for the product.
class Shape:
def draw(self):
pass

2. Define concrete implementations of the product.


class Circle(Shape):
def draw(self):
print("Drawing a Circle.")

class Square(Shape):
def draw(self):
print("Drawing a Square.")
3. Create a factory class/method that uses polymorphism to create and
return product objects.
class ShapeFactory:
def create_shape(self, shape_type):
if shape_type == "circle":
return Circle()
elif shape_type == "square":
return Square()
else:
raise ValueError("Unknown shape type")
Client code
factory = ShapeFactory()
shape1 = factory.create_shape("circle")
shape1.draw() # Output: Drawing a Circle.

shape2 = factory.create_shape("square")
shape2.draw() # Output: Drawing a Square.
• You avoid tight coupling between the creator and the
concrete products.
• Single Responsibility Principle. You can move the
product creation code into one place in the program,
making the code easier to support.
• Open/Closed Principle. You can introduce new types of
products into the program without breaking existing
client code.
The code may become more complicated since you need
to introduce a lot of new subclasses to implement the
pattern.

The best case scenario is when you’re introducing the


pattern into an existing hierarchy of creator classes.
Creational patterns
✓ Factory
✓ Abstract factory
✓ Builder
✓ Prototype
✓ Singleton
Singleton is a creational design pattern that lets you ensure that a class
has only one instance, while providing a global access point to this
instance.

<<already covered>>
Creational patterns Structural patterns Behavioral patterns
Factory Adapter Chain of
Abstract factory Bridge responsibility
Builder Composite Command
Prototype Decorator Iterator
Singleton Façade Mediator
Flyweight Memento
Proxy Observer
State
Strategy
Template method
Visitor
A good link to refer: https://refactoring.guru/design-patterns
Structural patterns
✓ Adapter
✓ Decorator
Adapter is a structural design pattern that allows objects with
incompatible interfaces to collaborate.

<<already covered>>
Structural patterns
✓ Adapter
✓ Decorator
Decorator is a structural design pattern that lets you attach
new behaviors to objects by placing these objects inside
special wrapper objects that contain the behaviors.
Imagine that you’re working on a notification library which lets other
programs notify their users about important events.

The initial version of the library was based on the Notifier class that had
only a few fields, a constructor and a single send method.

The method could accept a message argument from a client and send the
message to a list of emails that were passed to the notifier via its
constructor.

A third-party app which acted as a client was supposed to create and


configure the notifier object once, and then use it each time something
important happened.
At some point, you realize that users of the library expect more
than just email notifications.

Many of them would like to receive an SMS about critical issues.


Others would like to be notified on Facebook and, of course, the
corporate users would love to get Slack notifications.
How hard can that be? You extended the Notifier class and put the
additional notification methods into new subclasses. Now the client was
supposed to instantiate the desired notification class and use it for all
further notifications.

But then someone reasonably asked you, “Why can’t you use several
notification types at once? If your house is on fire, you’d probably want to
be informed through every channel.”

You tried to address that problem by creating special subclasses which


combined several notification methods within one class. However, it
quickly became apparent that this approach would bloat the code
immensely, not only the library code but the client code as well.
• Extending a class is the first thing that comes to mind when you
need to alter an object’s behavior. However, inheritance has
several serious caveats that you need to be aware of.
• Inheritance is static. You can’t alter the behavior of an existing
object at runtime. You can only replace the whole object with
another one that’s created from a different subclass.
• Subclasses can have just one parent class. In most languages,
inheritance doesn’t let a class inherit behaviors of multiple classes
at the same time.
• One of the ways to overcome these is by using Aggregation or
Composition instead of Inheritance.
• Both of the alternatives work almost the same way: one
object has a reference to another and delegates it some work,
whereas with inheritance, the object itself is able to do that
work, inheriting the behavior from its superclass.
• With this new approach you can easily substitute the linked
“helper” object with another, changing the behavior of the
container at runtime.
• An object can use the behavior of various classes, having
references to multiple objects and delegating them all kinds of
work.
Various notification methods become decorators.
Steps to Implement:
1.Define a component interface that declares a method(s) that can be
extended.
2.Implement a concrete component that implements the component
interface.
3.Create an abstract decorator that implements the same interface and has a
reference to a component.
4.Create concrete decorators that extend the abstract decorator and add
functionality.
A Simple Coffee Example
coffee shop where we can decorate a basic coffee with different add-ons (like
milk or sugar).
# Step 1: Component interface
class Coffee: # Step 3: Abstract Decorator
def cost(self): class CoffeeDecorator(Coffee):
pass def __init__(self, coffee):
self._coffee = coffee
# Step 2: ConcreteComponent
class SimpleCoffee(Coffee): def cost(self):
def cost(self): return self._coffee.cost()
return 5 # Basic cost of a simple coffee
# Step 4: Concrete Decorators
class MilkDecorator(CoffeeDecorator):
def cost(self):
return self._coffee.cost() + 2 # Adding milk cost
class SugarDecorator(CoffeeDecorator):
def cost(self):
return self._coffee.cost() + 1 # Adding sugar cost

# Using the decorators


simple_coffee = SimpleCoffee()
print(f"Simple Coffee Cost: ${simple_coffee.cost()}")

milk_coffee = MilkDecorator(simple_coffee)
print(f"Milk Coffee Cost: ${milk_coffee.cost()}")

sugar_milk_coffee = SugarDecorator(milk_coffee)
print(f"Sugar and Milk Coffee Cost:
${sugar_milk_coffee.cost()}")
• You can extend an object’s behavior without making a new
subclass.
• You can add or remove responsibilities from an object at
runtime.
• You can combine several behaviors by wrapping an object into
multiple decorators.
• Single Responsibility Principle. You can divide a monolithic class
that implements many possible variants of behavior into several
smaller classes.
• It’s hard to remove a specific wrapper from the
wrappers stack.
• It’s hard to implement a decorator in such a way that
its behavior doesn’t depend on the order in the
decorators stack.
• The initial configuration code of layers might look
pretty ugly.
Creational patterns Structural patterns Behavioral patterns
Factory Adapter Chain of responsibility
Abstract factory Bridge Command
Builder Composite Iterator
Prototype Decorator Mediator
Singleton Façade Memento
Flyweight Observer
Proxy State
Strategy
Template method
Visitor

A good link to refer: https://refactoring.guru/design-patterns


Behavioral patterns
✓ Observer
✓ Strategy
Observer is a behavioral design pattern that lets you
define a subscription mechanism to notify multiple
objects about any events that happen to the object
they’re observing.
Imagine that you have two types of objects: a Customer and a
Store. The customer is very interested in a particular brand of
product (say, it’s a new model of the iPhone) which should
become available in the store very soon.

The customer could visit the store every day and check product
availability. But while the product is still en route, most of these
trips would be pointless.
On the other hand, the store could send tons of emails
(which might be considered spam) to all customers each
time a new product becomes available.

This would save some customers from endless trips to the


store. At the same time, it’d upset other customers who
aren’t interested in new products.
The object that has some interesting state is often called subject,
but since it’s also going to notify other objects about the changes
to its state, we’ll call it publisher.

All other objects that want to track changes to the publisher’s state
are called subscribers.

The Observer pattern suggests that you add a subscription


mechanism to the publisher class so individual objects can
subscribe to or unsubscribe from a stream of events coming from
that publisher.
Following things will be needed:
1) an array field for storing a list of references to subscriber objects and
2) several public methods which allow adding subscribers to and removing
them from that list.
Steps to Implement:
1.Subject Interface: Define methods for adding, removing, and notifying
observers.
2.Observer Interface: Define a method for receiving updates.
3.Concrete Subject: Implements the subject interface and maintains
observer state.
4.Concrete Observers: Implement the observer interface and react to
updates.
A WeatherStation notifies multiple displays (observers)
when the temperature changes.
Observer Interface: Subject Interface:
class Subject:
class Observer:
def add_observer(self, observer):
def update(self, temperature):
pass
pass
def remove_observer(self, observer):
pass

def notify_observers(self):
pass
Concrete Subject (WeatherStation) Concrete Observers:
class WeatherStation(Subject): class PhoneDisplay(Observer):
def __init__(self): def update(self, temperature):
self.observers = [] print(f"Phone Display: Temperature updated to
self.temperature = 0 {temperature}°C")

def add_observer(self, observer): class WindowDisplay(Observer):


self.observers.append(observer) def update(self, temperature):
print(f"Window Display: Temperature updated to
def remove_observer(self, observer): {temperature}°C")
self.observers.remove(observer)

def set_temperature(self, temperature):


self.temperature = temperature
self.notify_observers()

def notify_observers(self):
for observer in self.observers:
observer.update(self.temperature)
Client code
# Create the subject (WeatherStation)
weather_station = WeatherStation()

# Create observers
phone_display = PhoneDisplay()
window_display = WindowDisplay()

# Add observers to the subject


weather_station.add_observer(phone_display)
weather_station.add_observer(window_display)

# Change temperature
weather_station.set_temperature(25) # Both observers are
notified
weather_station.set_temperature(30) # Both observers are
notified

# Remove one observer and change temperature


weather_station.remove_observer(phone_display)
weather_station.set_temperature(20) # Only WindowDisplay is
notified
•Subject (WeatherStation): Manages state and notifies observers.
•Observers (PhoneDisplay, WindowDisplay): React to changes in the
subject.
•Loose Coupling: The subject doesn't know the implementation details of
the observers.
• Open/Closed Principle. You can introduce new subscriber classes
without having to change the publisher’s code (and vice versa if
there’s a publisher interface).
• You can establish relations between objects at runtime.
Subscribers are notified in random order
Behavioral patterns
✓ Strategy
Strategy is a behavioral design pattern that enables selecting an
algorithm's behavior at runtime. It allows you to define a family
of algorithms, encapsulate them in separate classes, and make
them interchangeable without altering the client code.
Imagine an application that sorts data differently based on the input size or
type (e.g., QuickSort, MergeSort, or BubbleSort).
class Sorter:
def sort(self, data, algorithm):
if algorithm == "quick":
print("Sorting with QuickSort")
elif algorithm == "merge":
print("Sorting with MergeSort")
elif algorithm == "bubble":
print("Sorting with BubbleSort")
else:
print("Invalid sorting algorithm")
Adding a new algorithm means modifying the sort method, violating the Open/Closed
Principle. The class becomes cluttered with if-else statements.
• The Strategy pattern suggests that you take a class that does something
specific in a lot of different ways and extract all of these algorithms into
separate classes called strategies.
• The original class, called context, must have a field for storing a reference to
one of the strategies. The context delegates the work to a linked strategy
object instead of executing it on its own.
• The context isn’t responsible for selecting an appropriate algorithm for the
job. Instead, the client passes the desired strategy to the context. In fact,
the context doesn’t know much about strategies. It works with all strategies
through the same generic interface, which only exposes a single method for
triggering the algorithm encapsulated within the selected strategy.
• This way the context becomes independent of concrete strategies, so you
can add new algorithms or modify existing ones without changing the code
of the context or other strategies
A shopping cart applies different discounts: No Discount, Seasonal Discount,
or Promotional Discount.
1. Define Strategy Interface 2. Create Concrete Strategies
class DiscountStrategy: class NoDiscount(DiscountStrategy):
def calculate_discount(self, amount): def calculate_discount(self, amount):
pass return amount # No discount applied

class SeasonalDiscount(DiscountStrategy):
def calculate_discount(self, amount):
return amount * 0.9 # 10% discount

class PromotionalDiscount(DiscountStrategy):
def calculate_discount(self, amount):
return amount * 0.8 # 20% discount
3. Context Class
class ShoppingCart:
def __init__(self, discount_strategy):
self.discount_strategy = discount_strategy

def set_discount_strategy(self, discount_strategy):


self.discount_strategy = discount_strategy

def calculate_total(self, amount):


return self.discount_strategy.calculate_discount(amount)
Client code
# No Discount
cart = ShoppingCart(NoDiscount())
print("Total with No Discount:", cart.calculate_total(100)) # 100

# Apply Seasonal Discount


cart.set_discount_strategy(SeasonalDiscount())
print("Total with Seasonal Discount:", cart.calculate_total(100)) # 90

# Apply Promotional Discount


cart.set_discount_strategy(PromotionalDiscount())
print("Total with Promotional Discount:", cart.calculate_total(100)) # 80
• You can swap algorithms used inside an object at runtime.
• You can isolate the implementation details of an algorithm from
the code that uses it.
• You can replace inheritance with composition.
• Open/Closed Principle. You can introduce new strategies
without having to change the context.
• If you only have a couple of algorithms and they rarely change,
there’s no real reason to overcomplicate the program with
new classes and interfaces that come along with the pattern.
• Clients must be aware of the differences between strategies to
be able to select a proper one.
• A lot of modern programming languages have functional type
support that lets you implement different versions of an
algorithm inside a set of anonymous functions. Then you could
use these functions exactly as you’d have used the strategy
objects, but without bloating your code with extra classes and
interfaces.
That’s it

You might also like