Introduction
What are Design Patterns and why do we need them?
The Design Patterns course delves into the essential design patterns that every software engineer should be familiar with, emphasizing their critical role in software development.
Design patterns are notable for being well-tested, reusable, and optimal solutions to recurrent software engineering challenges, serving as reliable templates to address common problems.
The utility of design patterns extends beyond providing proven solutions — they also foster efficient communication among developers through establishing a common language or vocabulary regarding architectural solutions.
Despite developers working with varied programming languages, knowledge of a design pattern creates a shared understanding of its usage and implementation, underscoring its universal applicability across different programming languages.
Essentially, design patterns encapsulate generations of cumulative experience and knowledge from software engineers, offering a reservoir of tried-and-true strategies to inform and enhance current software design practices.
The Design Patterns Covered in This course
This course provides an in-depth study of seven key design patterns.
The methodology for each pattern:
- Understand the architecture of the design pattern.
- Implement the pattern using Flutter with Dart.
- Study its role in promoting good architecture.
- Application of these patterns will be showcased by simulating John Conway’s “Game of Life” for mobile devices.
The patterns are categorized into three families:
Creational Patterns: Focus on object creation methods. In this course, we’ll discuss:
- Singleton Pattern
- Factory Method Pattern
- Builder Pattern
Creational patterns offer reusable, flexible methods for object instantiation.
Structural Patterns: Aim to improve design by efficiently establishing relationships between entities. In this course, we’ll cover:
- Adapter Pattern
Structural patterns focus on efficient creation of complex object hierarchies.
Behavioral Patterns: Enhance object interaction and communication. The patterns in this category for our study are:
- Strategy Pattern
- Observer Pattern
- State Design Pattern
Behavioral patterns promote loosely coupled ways for objects to communicate and exchange information.
Mastery of these patterns allows for a nuanced understanding of coding, enabling one to identify them in various applications.
The WHY Of Software Architecture
Complex software systems often face issues such as:
- Extended timelines due to changing requirements.
- Coordination challenges among developers.
- Code redundancy and inadequate documentation.
- Maintenance difficulties and inflexibility in incorporating new features.
All these issues can stem from a lack of proper design and architecture.
A well-defined architecture is comparable to the blueprint of a skyscraper like the Empire State Building, which provides clarity and organization to everyone involved.
The purpose of a robust software architecture is to ensure predictability, coherence, and an organized construction process in software projects.
The typical software development cycle:
- Requirements Gathering: Documentation of UI, UX, and overarching project goals.
- Design/Architecture: Create a system design based on the documented requirements.
- Implementation: This involves coding and testing the solution to ensure it adheres to the design and meets requirements.
- The cycle can be iterative, meaning requirements can be gathered, followed by a phase of design and implementation, then revisited.
Whether the development cycle is linear or iterative, a sound design is imperative.
This course emphasizes the design aspect, exploring how design patterns promote exemplary software engineering.
Various documents and artifacts associated with a project include:
- Requirement documents.
- Design documents.
- UI/UX documents.
- Test suites and related documentation.
- Deployment documents.
- Audit logs and analytics.
- Most crucially, the source code — the actual product.
- Architecture documents and potential database diagrams.
In the context of software, while the tangible product is the source code, proper architecture and design patterns are fundamental. These patterns are essential for good architecture documentation.
To understand and document these design patterns, the course will utilize UML (Unified Modeling Language).
The course’s primary objective is to delve deep into the significance of design patterns in ensuring effective software architecture.
Why use UML?
UML stands for Unified Modeling Language.
It’s used in the course because of its capability to clearly document well-defined structures like design patterns.
Advantages of UML:
- Provides a visual representation, making complex patterns easier to comprehend.
- Though maintaining UML diagrams can be challenging during evolving requirements, they are beneficial for illustrative purposes in a structured learning environment.
For the course’s objectives, only two types of UML diagrams are employed:
Class Diagrams: These outline the structural framework of a design pattern.
- Example given demonstrates the relationships among components such as director, builder, concrete builder, and product.
- These relationships help elucidate how components interact, highlighting inheritance, aggregation, and product creation.
Sequence Diagrams: These depict the interaction sequence among main objects and classes over time.
- The example showcases how a client creates a concrete builder, initializes a director with it, and uses the director to construct a product. The director coordinates tasks to ensure the final object is organized.
- This particular example was from the builder design pattern which is utilized to construct intricate hierarchies of objects.
UML excels at the abstraction level required for design patterns.
Each pattern can be viewed as an isolated block of interaction, allowing for in-depth, independent study.
The study approach is similar to the “divide and conquer” method, focusing on mastering one pattern thoroughly before progressing to the next.
Organized vs. Unorganized code
A simple grid pattern is visually represented by neatly arranged rectangles, showing an organized structure.
In contrast, overlapping blobs of varying sizes indicate less organization and more chaos.
Drawing a parallel to coding practices: the grid pattern reflects a well-structured, divide and conquer coding approach with good encapsulation.
The blobs symbolize code that is bulky, with multiple overlaps, making it less structured.
When examining communication patterns, it’s more straightforward in a well-organized approach, like the grid.
In a chaotic setup, it’s tougher to identify any discernible pattern.
This course aims to guide learners towards an organized and structured coding methodology (akin to the left side) for better, defined patterns in coding architecture.
UML Refresher
UML (Unified Modeling Language) is a vital tool for visualizing structural and behavioral relationships between objects in designs.
Structural Relationships:
It reveals how objects fit together, their relationships, and their construction.
Class diagrams are instrumental for this, showcasing object-oriented relationships. Within class diagrams:
Attributes and operations (methods) are captured.
Visibility can be defined (e.g., public using “+” or private using “-”).
Class relationships in the diagram can illustrate:
- Generalizations: Such as inheritance where descendant classes inherit from a parent or abstract class.
- Associations: Like aggregations, where one class contains or is made up of other classes.
- Dependencies: Indicates a usage relationship between classes, but not necessarily a strong connection. For example, one class might use another for a specific function without having it as a component.
Behavioral Relationships:
Focus on how objects interact and communicate.
Sequence diagrams highlight object-oriented behavior, indicating message exchanges over time. This visual representation includes interaction details such as actors, operations, and method calls with relevant data.
What makes a Great Architecture
Hallmarks of Good Architecture
The Law of Demeter is a guideline that recommends objects to have limited knowledge of other objects.
- Each unit (or object) should be aware only of units that are closely related to itself, essentially its “friends”.
- Direct interaction should be confined to these friends, avoiding any communication with “strangers” or distant units.
- For instance, with three classes A, B, and C: If A can communicate with B, and B with C, A should not directly communicate with C even though there’s an indirect connection. B is A’s friend, while C is a stranger to A.
- This approach encourages a design where an object knows or assumes as little as possible about other objects, focusing mainly on its immediate neighbors.
- The Law of Demeter is also referred to as the Principle of Least Knowledge.
The introduction to the Law of Demeter acts as a precursor to SOLID principles, foundational principles in software engineering and architecture, which will be the focus of the upcoming lecture.
S.O.L.I.D Design Principles
Single Responsibility Principle (SRP)
Each class should have only one reason to change.
A class should focus on a single task such as logging, parsing, formatting, etc.
For example, a class might focus exclusively on persistence or on validating pre and post conditions.
Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
Instead of altering existing code, create new classes that extend the original.
For example, rather than modifying a basic calculator class to add scientific functions, create a new ‘scientific calculator’ class that extends the original.
Liskov Substitution Principle (LSP)
Objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program.
For instance, if there are different shape subclasses like triangle, circle, and rectangle under a shape superclass, the specific type of shape shouldn’t matter when calculating the area.
Interface Segregation Principle (ISP)
Clients shouldn’t be forced to depend on interfaces they don’t use.
Avoid “fat” interfaces that have methods irrelevant for some implementers.
For instance, instead of having a logging interface with methods irrelevant for file logging, have specific interfaces for each kind of logging.
Dependency Inversion Principle (DIP)
Depend on abstractions, not concretions.
Higher-level modules should not depend on lower-level modules.
Both should depend on abstractions.
For example, a book service class should depend on an abstract number generator rather than specific ISBN generator class.
Singleton Design Pattern
Understanding Singleton Pattern
- The Singleton pattern is a creational design pattern ensuring only one instance of a class exists, providing easy global access to it.
- Singleton’s essence is about controlling instance creation, not dictating its functionality, leaving room for creativity.
- The pattern is crucial for scenarios requiring serialized access, especially with thread safety concerns. Access to the singleton should be one-at-a-time.
- Practical applications for Singletons include loggers, caching, thread pools, database connections, and configuration access. All clients should be able to access the same instance to ensure consistency.
- Singleton ensures that various code pieces accessing it refer to the same instance.
- Singleton is a Gang of Four pattern, designed to ensure specific classes only have one instance. It collaborates with other patterns like Abstract Factory, Builder, Prototype, Facade, and State.
- Use Singleton when controlling access to shared resources. However, use it judiciously to avoid overgeneralized global access. Overuse can obscure dependencies and reduce code readability.
- Ensure that using Singleton doesn’t breach the Single Responsibility Principle. If it tackles too many tasks simultaneously, consider refactoring.
- When designing a Singleton, consider lazy construction, creating the instance only upon the first need. In situations requiring immediate availability, opt for eager loading.
- For multithreaded environments, ensuring thread safety is paramount. However, in Dart and Flutter, thread safety concerns are minimal due to Dart’s use of isolates.
- The motivation behind using Singleton is its ability to manage single instances effectively, catering to specific scenarios that need consistent and controlled access.
Factory Method Design Pattern
Understanding Factory Method Pattern
- The factory method pattern is a creational design pattern that provides a way to create objects.
- Typically, objects are created using constructors, but this ties code directly to the specific class type it wants to instantiate, creating a strong coupling.
- The direct use of constructors has an architectural disadvantage, especially in scenarios where the exact types of objects may vary or evolve.
- Factory method abstracts the creation logic of class instances, hiding the instantiation logic from the client. Instead of using a specific constructor, objects are created via a factory method.
- Two main aspects of the factory method pattern: Objects are created by the factory method, not a constructor. Creation is based on an abstraction, not a concrete type.
- This pattern allows the caller to refer to the newly created object via a common interface, promoting a loose coupling between the caller and the object.
- For instance, in a game with various bullet types, bullets can be created based on the abstract ‘bullet’ class rather than specific bullet types. This allows easy addition of new bullet types without changing the core logic.
- With the factory method, creation logic is encapsulated in the factory, so the caller only interacts with a common interface and doesn’t concern itself with the creation details.
- An added benefit is the potential for object caching within the factory, enhancing efficiency without the caller’s awareness.
Builder Design Pattern
Understanding Builder Pattern
The builder design pattern is a creational pattern used to handle the construction of complex objects, which are composed of multiple parts with intricate relationships.
Unlike simple objects, complex ones may have a hierarchy and are not just a collection of loosely related parts.
The builder pattern addresses the issue of unwieldy and inflexible constructors by separating the construction of a complex object from its representation, allowing the same construction process to produce different representations.
For example, in a video game where one needs to build a house with various features like walls, windows, floors, and a garage, a single constructor with numerous parameters would be cumbersome and difficult to maintain.
Instead, the builder pattern abstracts the creation process, providing an object that assembles the complex object part by part, offering both encapsulation and flexibility.
It allows skipping unnecessary construction steps, hence offering great flexibility to build various configurations of the object.
Furthermore, builders can have variants to create slightly different versions of an object, utilizing a director controller to delegate construction to the specific type of builder needed, such as different document types in a document reader scenario.
All builders follow the same steps, but each builds the object in a slightly different way.
The builder pattern is useful when dealing with complex objects with many constructors, composite tree-like structures, or when multiple representations of the object are needed while using the same construction process. It should be avoided for simple objects where a factory method might suffice.
Pros of the builder pattern include clear separation between construction and representation, fine control over the construction process, and support for changes in an object’s internal representation.
The cons are an increase in the number of classes due to different concrete builders and potential complexity in dependency injection.
Design considerations for the builder pattern involve encapsulating the creation and assembly of parts in a builder object, using a director class to delegate creation, and ensuring synergy among the family of objects being modeled, so different representations can be built using the same steps.
Adapter Design Pattern
Understanding Adapter Pattern
In today’s learning session, we delved into the adapter pattern, which is a structural design pattern employed to resolve interface incompatibility issues.
We explored two distinct scenarios: one involving data incompatibility between an investment data provider outputting XML and a charting library requiring JSON, and another addressing interface incompatibility between different versions of a rectangle object with varied constructor parameters.
The adapter pattern provides a solution by creating a wrapper or adapter that mediates between the client’s expectations and the service’s offerings, transforming inputs and outputs as necessary to enable seamless interaction.
This pattern aligns with the principles of solid design, allowing for code reuse without the need to rewrite existing codebases, thus adhering to the single responsibility and open-closed principles.
The adapter’s utility is apparent when existing interfaces require integration without direct compatibility, streamlining the interaction between legacy systems and newer implementations.
However, its application might not be ideal for time-sensitive scenarios where performance overhead from additional abstraction layers is a concern.
We concluded by contrasting the adapter pattern with similar Gang of Four patterns like the bridge, state, strategy, proxy, and decorator patterns, highlighting the adapter’s unique role in interface conversion.
By implementing the adapter pattern, developers can enhance the maintainability and extensibility of their systems, although this may come at the cost of increased complexity due to additional interfaces and classes.
Strategy Design Pattern
Understanding Strategy Pattern
Today’s session covered the strategy design pattern, which is favored for handling a variety of data extraction protocols within a web bot, such as XML, SQL, and CSV files.
The problem with having a scraper handle all the logic for each data type is that it can lead to code bloat, lack of reusability, and violation of the SOLID principles, particularly the open-closed principle.
The solution involves abstracting each protocol into separate classes with a common interface and having the scraper delegate to these protocol-specific handlers.
This allows for adherence to the strategy pattern’s principles by deferring algorithm selection to runtime, exposing a consistent interface to clients, and enabling seamless substitution and loose coupling through composition.
The strategy pattern’s essence is demonstrated via UML, showcasing the execution method and how context (our scraper) holds a reference to the strategy interface, allowing dynamic changing of strategies at runtime.
This design pattern ensures flexibility and dynamic adaptability, and is considered a pinnacle of the Gang of Four design patterns.
Observer Design Pattern
Understanding Observer Pattern
The observer pattern is a vital behavioral design pattern that serves as an event notification mechanism among objects, akin to the relationship between a publisher and a subscriber.
The core idea is that subscribers can register with a publisher to receive updates.
This pattern includes a subscription process where subscribers voluntarily add or remove themselves from the publisher’s notification list.
When the publisher’s state changes, it notifies all its subscribers about this change.
The pattern is comprised of subjects (publishers) and observers (subscribers), where the subject keeps track of its observers and notifies them of any state changes, often used in scenarios with non-predictable or random data arrival like HTTP requests, user inputs, and distributed systems such as databases and blockchains.
It’s notably a fundamental component of the MVC architectural pattern and is extensively employed in frameworks like Flutter.
State Design Pattern
Understanding State Pattern
Today’s discussion focused on the state design pattern and its application in representing state actions and transitions within a logical workflow.
The example used was a toaster’s operation, outlining various states like idle, bread loaded, toasting, and bread ejected.
The states represent different conditions of the toaster, while the actions are operations such as loading bread or starting toasting, which induce transitions — the changes from one state to another.
The concept was further clarified using a finite state machine and a state diagram that illustrated the toaster’s states, possible actions, and resulting transitions.
The state pattern is a behavioral design pattern allowing an object to change its behavior based on its internal state, similar to a finite state machine with a finite number of states and discrete transitions.
This pattern is akin to the strategy pattern where a strategy can be switched via method invocations.
An abstract UML diagram for the state pattern was discussed, showcasing elements such as context (e.g., the toaster), a state interface defining possible states, and concrete state classes that embody specific states and transitions.
The context holds a state, and the state can reference the context, allowing transitions between states.
The concrete state classes can perform actions and decide the transitions when those actions are invoked.
A sequence diagram demonstrated how a client interacts with the context to perform actions, and how states manage transitions.
This pattern ensures that the states are aware of the transitions, unlike in other patterns where the context may manage the transitions.