Design Patterns Exam
Design Patterns Exam
Definition: The Abstract Factory Design Pattern is a creational pattern that provides an interface
for creating families of related or dependent objects without specifying their concrete classes. It
abstracts the process of object creation.
Intent: The intent of the Abstract Factory pattern is to create an interface for creating families of
related objects, where each family shares a common theme. It allows you to ensure that the created
objects are compatible and follow a certain consistency.
Motivation: Suppose you are building a user interface framework. You have different UI elements
like buttons, text boxes, and checkboxes. Depending on the platform (e.g., Windows, macOS,
Linux), you need to create different sets of these elements. The Abstract Factory pattern helps you
create platform-specific UI element families without coupling your code to specific
implementations.
- You need to ensure that the created objects are compatible with each other.
- You need to support multiple families of objects with the ability to switch between them.
Scenario: Let's say you're developing a video game that runs on multiple platforms (Windows,
PlayStation, Xbox). You need to create different rendering components for each platform,
including shaders, models, and textures. The Abstract Factory pattern can help you define abstract
rendering factories for each platform and then create platform-specific implementations.
2. Prototype Design Pattern
Definition: The Prototype Design Pattern is a creational pattern that allows you to create new
objects by copying an existing object, known as the prototype. It is used when the cost of creating
an object is more expensive or complex than copying it.
Intent: The intent of the Prototype pattern is to create new objects by copying an existing object,
and preserving its state. It provides an alternative to creating objects through constructors.
Motivation: Imagine a scenario where you have a complex object that requires a time-consuming
setup or initialization. Instead of recreating this object from scratch, you can clone an existing
instance, saving time and resources.
- You need to create new objects that are similar to existing ones, with minor differences.
- You want to isolate the object creation logic from the client.
Scenario Suppose you are developing a graphics application that allows users to create and
manipulate geometric shapes. Instead of creating a new shape object every time a user draws a
shape, you can use the Prototype pattern to clone an existing shape.
3. Adapter Design Pattern:
Definition: The Adapter Design Pattern is a structural pattern that allows objects with incompatible
interfaces to work together. It acts as a bridge between two interfaces, making them compatible
without changing their source code.
Intent: The intent of the Adapter pattern is to allow objects with different interfaces to collaborate
effectively. It ensures that classes with incompatible interfaces can work together by providing a
wrapper (the adapter) that converts one interface into another.
Motivation: Consider a scenario where you have an existing class or component (e.g., a legacy
library) with an interface that doesn't match the interface expected by a client. Instead of modifying
the existing code, you can use an adapter to make them work together seamlessly.
- You need to make existing classes work with others without modifying their source code.
- You want to create a reusable component that connects various incompatible interfaces.
Scenario: Imagine you have a music player application that can play MP3 files. You want to add
support for a new audio format called "WAV." However, the existing player class only understands
MP3. You can create an adapter to make the new WAV player compatible with the existing code.
4. Decorator Design Pattern:
Definition: The Decorator Design Pattern is a structural pattern that allows you to add behavior to
objects dynamically without altering their original class structure. It involves creating a set of
decorator classes that are used to wrap concrete components.
Intent: The intent of the Decorator pattern is to extend the functionality of objects in a flexible
and reusable way, often referred to as "wrapping." It allows you to add or modify behavior at
runtime without changing the core code.
Motivation: In software development, you often encounter situations where you need to add
features or responsibilities to objects. Rather than creating a multitude of subclasses for each
combination of features, decorators offer a more flexible solution.
Scenario: Suppose you're building a coffee ordering system. You have a base Coffee class, and
you want to allow customers to add extra ingredients such as milk, sugar, or caramel to their coffee
orders without creating a separate class for each combination. Here's a simplified example in
Python:
5. Façade Design Pattern:
Definition: The Façade Design Pattern is a structural pattern that provides a simplified interface
to a complex system of classes, libraries, or components. It acts as a unified interface to a set of
interfaces in a subsystem, making it easier to use.
Intent: The intent of the Façade pattern is to provide a high-level, simplified interface to a complex
system, making it more accessible and easier to use. It abstracts the complexities of the subsystem
from the client code.
Motivation: In software development, you often encounter systems with intricate dependencies
and multiple classes. The Façade pattern helps encapsulate these complexities behind a single entry
point, simplifying interactions and reducing the coupling between clients and the subsystem.
- You need to decouple client code from the inner workings of the system.
Scenario: Consider a multimedia framework that involves various components like audio, video,
and codecs. Using the Façade pattern, you can create a simplified interface for playing multimedia
files without exposing the internal complexities.
6. Flyweight Design Pattern:
Definition: The Flyweight Design Pattern is a structural pattern that minimizes memory usage or
computational expenses by sharing as much as possible with related objects. It is particularly useful
when you need to create a large number of similar objects.
Intent: The intent of the Flyweight pattern is to reduce memory usage or improve performance by
sharing common portions of objects among multiple objects. It focuses on minimizing the
overhead of large numbers of fine-grained objects.
Motivation: In scenarios where you need to manage a large number of objects with shared
characteristics, the Flyweight pattern can help reduce memory consumption and improve
efficiency by reusing shared parts of objects.
- You have a large number of similar objects that can share common properties.
- The objects can be divided into intrinsic (shared) and extrinsic (non-shared) parts.
Scenario: Suppose you are developing a text editor where each character in a document is an
object. To save memory and reduce object creation overhead, you can use the Flyweight pattern to
represent characters with shared properties (e.g., font style) as flyweights.
7. Proxy Design Pattern:
Definition: The Proxy Design Pattern is a structural pattern that provides a surrogate or
placeholder for another object to control access to it. It acts as an intermediary, allowing you to
add additional functionality while controlling access to the real object.
Intent: The intent of the Proxy pattern is to provide a way to control access to an object, add
behavior to it, or delay its creation and initialization until it's actually needed. It can be used for
various purposes, such as lazy loading, access control, logging, and caching.
Motivation: There are several scenarios where you might need to use a Proxy:
- Lazy Loading: To defer the creation and initialization of a resource-intensive object until it's
required to save resources and improve performance.
- You need to control access to an object, especially for lazy loading, access control, or caching.
Scenario: Let's consider a scenario where you're building a web page that displays high-resolution
images. Loading these images can be slow and resource-intensive. You can use a proxy to
implement lazy loading, where the image is only loaded when it's actually viewed by the user.
8. Chain of Responsibility Design Pattern:
Definition: The Chain of Responsibility Design Pattern is a behavioral pattern that lets you pass
requests along a chain of handlers. Each handler decides either to process the request or to pass it
to the next handler in the chain.
Intent: The intent of the Chain of Responsibility pattern is to decouple senders and receivers of
requests. It allows multiple objects to handle a request without the sender needing to know which
object will ultimately process it.
Motivation: In scenarios where you have a series of processing steps for requests, and you want
to avoid coupling the sender to specific handlers, you can use the Chain of Responsibility pattern.
It promotes flexibility and extensibility.
- You need to decouple the sender from the receiver, and you're not sure which object will handle
the request.
Scenario: Consider an order processing system where orders go through various validation steps,
such as checking customer information, inventory availability, and payment processing. Each
validation step is handled by a separate object in the chain. If one step fails, the order is rejected.
9. Command Design Pattern:
Definition: The Command Design Pattern is a behavioral pattern that encapsulates a request as an
object, thereby allowing you to parameterize clients with queues, requests, and operations. It also
allows you to support undoable operations.
Intent: The intent of the Command pattern is to encapsulate a request as an object, allowing for
the decoupling of senders (objects that issue requests) from receivers (objects that perform the
actual work). It also enables the support of various types of requests, queuing, and logging of
requests.
Motivation: In software development, you often encounter scenarios where you want to
parameterize objects with operations, delay the execution of a request, or support undo operations.
The Command pattern provides a structured way to achieve these goals by encapsulating requests
in objects.
- You need to support undoable operations by storing the state required to revert a command's
effect.
Scenario: Consider a remote control for various electronic devices such as TVs, music players,
and lights. Each button on the remote control should trigger a specific action, but the actions may
vary depending on the device. The Command pattern can be used to encapsulate these actions and
make the remote control flexible and extensible.
10. Strategy Design Pattern:
Definition: The Strategy Design Pattern is a behavioral pattern that defines a family of algorithms,
encapsulates each of them, and makes them interchangeable. It allows the client to choose an
appropriate algorithm to use at runtime.
Intent: The intent of the Strategy pattern is to define a set of algorithms, encapsulate each of them,
and make them interchangeable. This enables the client to vary the behavior of an object by
selecting an appropriate strategy without altering its structure.
Motivation: In software development, you often encounter situations where an object needs to
perform a specific task, but the exact implementation can vary. Rather than hardcoding the
algorithm directly into the object, the Strategy pattern allows you to separate the algorithm from
the context and choose the appropriate strategy dynamically.
- You have a family of related algorithms and need to make them interchangeable.
- You want to allow clients to select an algorithm from a family of algorithms at runtime.
- You need to avoid code duplication in situations where similar logic appears in multiple places.
Scenario: Consider a scenario where you are building a navigation application. The application
needs to calculate the fastest route from one location to another, and the calculation can vary based
on user preferences (e.g., shortest route, fastest route, avoid toll roads). Using the Strategy pattern,
you can encapsulate the routing algorithms and allow users to select their preferred strategy.
11. Memento Design Pattern:
Definition: The Memento Design Pattern is a behavioral pattern that allows you to capture and
externalize an object's internal state so that the object can be restored to that state later. It
involves three key components: the Originator (the object whose state needs to be saved), the
Memento (the snapshot of the state), and the Caretaker (the object that keeps track of and
manages multiple snapshots).
Intent: The intent of the Memento pattern is to provide the ability to capture an object's internal
state without exposing its structure, and to allow external objects to restore the object to a
previous state. This pattern is useful for implementing undo/redo functionality and managing the
history of changes in an object.
Motivation: When you need to implement features like undo/redo functionality in an application
or maintain a history of changes in an object's state, the Memento pattern allows you to capture
and manage the state snapshots effectively.
- You need to capture and restore an object's internal state without exposing its details.
Scenario: Consider a text editor application where users can edit documents. To implement an
undo feature, you can use the Memento pattern to capture and restore the document's state.
12. State Design Pattern:
Definition: The State Design Pattern is a behavioral pattern that allows an object to alter its
behavior when its internal state changes. The pattern encapsulates each state as a separate class
and delegates state-specific behavior to these classes.
Intent: The intent of the State pattern is to allow an object to change its behavior dynamically
when its internal state changes. It promotes a cleaner separation of concerns by representing
different states as separate classes, making it easier to add new states without modifying the
context class.
Motivation: When an object exhibits different behaviors based on its internal state and these
behaviors are complex or numerous, the State pattern provides a structured way to manage these
behaviors and transitions between states.
- You have multiple conditional statements that depend on the object's state.
- You want to avoid long switch/case or if/else blocks for state-based behavior.
Scenario: Consider an order processing system where the order can be in different states (e.g.,
pending, processing, shipped, canceled). Using the State pattern, you can represent each order state
as a separate class and simplify state transitions.
The SOLID principles, DRY (Don't Repeat Yourself), the Law of Demeter, and Tell, Don't Ask
are fundamental concepts in software design and development that help create maintainable,
flexible, and readable code. Let's discuss each of these principles in detail:
1. SOLID Principles:
SOLID is an acronym that represents five principles of object-oriented design, which aim to
improve the quality and maintainability of software systems:
Single Responsibility Principle (SRP): A class should have only one reason to change. In other
words, a class should have one primary responsibility or job. This principle helps in maintaining
a clear and focused codebase.
Open/Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be
open for extension but closed for modification. This encourages the use of inheritance and
interfaces to extend behavior rather than altering existing code.
Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types
without affecting the correctness of the program. In essence, derived classes should adhere to the
contract established by their base class.
Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces
they do not use. This principle promotes creating smaller, more focused interfaces to avoid "fat"
interfaces that require clients to implement unnecessary methods.
Dependency Inversion Principle (DIP): High-level modules should not depend on low-level
modules; both should depend on abstractions. Abstractions should not depend on details; details
should depend on abstractions. This principle encourages the use of dependency injection to
decouple components and make the system more flexible and maintainable.
2. DRY (Don't Repeat Yourself):
DRY is a principle that emphasizes the importance of avoiding code duplication in software
development. When you repeat code, it becomes harder to maintain because changes must be
made in multiple places, increasing the risk of introducing inconsistencies and errors. Instead,
DRY suggests that you should extract common functionality into reusable components,
functions, or classes. This promotes modularity, code reusability, and easier maintenance.
3. Law of Demeter:
The Law of Demeter, also known as the "Principle of Least Knowledge," states that a module
should not have knowledge of the inner workings or structure of other modules. In simpler terms,
an object should only interact with its immediate collaborators and should not reach into the
internal details of other objects. This principle reduces coupling between components, making
the system more resilient to changes and easier to understand.
"Tell, Don't Ask" is a design principle that encourages you to encapsulate behavior within
objects rather than exposing their internal state for external manipulation. Instead of asking an
object for its data and then making decisions based on that data, you should tell the object what
you want it to do, allowing it to manage its own state and behavior. This promotes encapsulation,
reduces the risk of unintended side effects, and leads to more maintainable and flexible code.
High Cohesion
Definition: High cohesion refers to the degree to which the responsibilities of a module or
component are closely related and focused on a single, well-defined purpose or task. In other
words, a module with high cohesion should have a clear and specific role within the software
system.
Characteristics
- Modules with high cohesion tend to be more self-contained and have a clear, single-minded
purpose.
- They are easier to understand, test, and maintain because their functionality is well-defined
and limited in scope.
- Changes or updates to a module with high cohesion are less likely to have ripple effects on
other parts of the system.
- High cohesion promotes code reusability because well-defined modules can be easily
extracted and reused in different parts of the application.
Example: A class that handles user authentication and authorization has high cohesion because
its sole responsibility is managing user access control.
Low Coupling:
Characteristics:
- Modules with low coupling are more independent, making them easier to develop, test, and
maintain in isolation.
- Changes to one module with low coupling are less likely to impact other modules because
they have limited knowledge of each other's internal details.
- Low coupling promotes flexibility and adaptability since you can replace or modify one
module without affecting the entire system.
- It also encourages code reusability because loosely coupled modules can be more easily
integrated into other systems.
Example: A module responsible for sending email notifications should have low coupling with
the rest of the application. It should be able to send emails without needing detailed knowledge
of the application's other functionalities.
In summary, high cohesion and low coupling are design principles that complement each other.
High cohesion ensures that modules have well-defined and focused responsibilities, while low
coupling reduces the interdependencies between modules, making the system more modular,
maintainable, and adaptable to changes. When applied together, these principles contribute to the
development of robust and scalable software systems.