Design Patterns in C# Sample
Design Patterns in C# Sample
Patterns in C#
By
Danny Adams
Copyright © 2024 by Danny Adams. All rights reserved. No part of this
book may be reproduced in any form or by any electronic or mechanical
means, including information storage and retrieval systems, without written
permission from the author. The only exception is for reviewers, who may
quote short excerpts in a review.
● Classes
● Creating objects from classes
● Access modifiers (public, private, protected)
● Class properties/fields/data/state
● Class methods
So, you just need to understand the very basics of OOP to find this book valuable. Any other
OOP concepts – such as abstract classes, polymorphism, encapsulation, composition – will be
fully explained in this book. You will also learn the very important SOLID principles.
OOP principles and the five SOLID principles are crucial to understand before learning any
design patterns – which is why I have dedicated the first few chapters of this book to those
topics, before we start learning any design patterns. This means that developers of all levels
can benefit from this book.
All examples are in C#, so it would help if you understood the basic syntax of C#. I don’t explain
basic syntax, as there are plenty of free and great videos on YouTube to get you started with C#
in little time.
My aim for this book was to keep it succinct, and not for it to become some slab of a textbook –
so that you can actually finish it!
Feel free to skip chapters. E.g. If you already understand the OOP principles, skip them.
Why Design Patterns?
Design patterns are essential in software development for several reasons:
Overall, design patterns facilitate the creation of high-quality, maintainable software systems by
providing reusable solutions to common design problems and promoting best practices in
software development.
(By the way, don’t worry if you don’t quite understand all of the above points; these points will
become clearer as we implement and discuss each of the design patterns, SOLID principles
and OOP principles. For example, many of you right now won’t understand the difference
between extending a codebase vs modifying a codebase. For now, relax – all will be revealed!)
Creational Patterns
1. Abstract Factory
2. Builder
3. Factory Method
4. Prototype
5. Singleton
Structural Patterns
1. Adapter
2. Bridge
3. Composite
4. Decorator
5. Facade
6. Flyweight
7. Proxy
Behavioral Patterns
1. Chain of Responsibility
2. Command
3. Interpreter
4. Iterator
5. Mediator
6. Memento
7. Observer
8. State
9. Strategy
10. Template Method
11. Visitor
On completion of this book, you will understand all of the 23 GoF design patterns, where to (and
where not) apply them, all five SOLID principles, and some advanced OOP concepts. You will
have all of the tools that you need to become a great object-oriented software developer.
About Me
I am currently a freelance software developer and technical writer. I build fullstack web
applications, Shopify apps, mobile apps, and WordPress plugins and themes. My current
techstack usually consists of React on the frontend, Laravel or .Net on the backend, and a
PostgreSQL database.
I am also a technical writer and enjoy writing tech blog posts, books, videos and courses. I
sporadically create content for FreeCodeCamp’s blog and YouTube channel.
Twitter: https://x.com/DoableDanny
Gumroad: https://doabledanny.gumroad.com/
FreeCodeCamp: https://www.freecodecamp.org/news/author/danny-adams/
I created a simple C# console app to run the examples in this book, and see their outputs in the
terminal.
All code examples in this book use VS Code’s “Solarized Light” theme in size 14 font.
Github Repo
All code examples are included in this repo:
https://github.com/DoableDanny/Design-Patterns-in-C-Sharp
namespace MyApp
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
Or, the same thing with top-level statements, requires no boilerplate:
Console.WriteLine("Hello, World!");
In this book, I’ll be creating examples with the explicit program class and main method, as well
as with top-level statements.
● Encapsulation
● Abstraction
● Inheritance
● Polymorphism
● Coupling
● Composition
● Composition vs Inheritance
Encapsulation
Encapsulation is a fundamental principle of object-oriented programming that involves bundling
the data (“attributes” or “fields”) and methods (behaviors) that operate on the data into a single
unit, called a class. Encapsulation helps in hiding the internal implementation details of a class
by only exposing the necessary functionalities to the outside world.
Users of this class now have free reign to assign balance to whatever value that they want:
class Program
{
static void Main(string[] args)
{
BadBankAccount badAccount = new BadBankAccount();
badAccount.balance = -1; // Oh dear -- balance should not be allowed to
be negative
}
}
this.balance += amount;
}
this.balance -= amount;
}
}
// Program.cs
account.Deposit(500.00m);
Console.WriteLine("Balance after deposit: " + account.GetBalance()); //
1500.00
account.Withdraw(200.00m);
Console.WriteLine("Balance after withdrawal: " + account.GetBalance()); //
1300.00
In this example:
● The BankAccount class encapsulates the account data (balance) and related
methods (Deposit() and Withdraw()) into a single unit.
● The data members (balance) are marked as private, encapsulating them within the
class and preventing direct access from outside the class.
● “Getter” methods (GetBalance()) are used to provide controlled access to the private
data members.
● Methods (Deposit() and Withdraw()) are used to manipulate balance, ensuring
that operations are performed safely and according to the business rules.
● The Main() method demonstrates how to create an instance of the BankAccount
class and interact with its properties and methods, without needing to know the internal
implementation details.
Above, the user of the BankAccount class (i.e. you, other developers, classes) can’t directly
access the balance field directly, as it is marked private. This data is encapsulated within
the class. Methods dictate the rules for how this data can be accessed and modified, ensuring
that our program’s correct rules and logic can’t be violated by users, or consumers, of the
BankAccount class – for example, it’s no longer possible to withdraw more money than is in
the account.
Encapsulation of the logic inside of the methods in BankAccount also means that users don’t
need to worry about the implementation details when interacting with a BankAccount object.
For example, the user doesn’t have to worry about the logic involved in withdrawing money –
they can just call account.Withdraw(200.00m). The implementation details are hidden and
encapsulated. And if the user tries to do something stupid, like deposit a negative amount, the
program will throw an error, and the user will be notified.
Encapsulation of logic within methods in the BankAccount class allows users to interact with a
BankAccount object without needing to know or understand the internal implementation details
of how withdrawals, deposits, or other operations are carried out. Users of the BankAccount
class can interact with it using simple, intuitive methods, like Withdraw() and Deposit(),
without needing to understand the complex logic behind these operations.
Encapsulation abstracts away the complexity of the implementation details, allowing users to
focus on the higher-level functionality provided by the BankAccount class. Users only need to
know the public interface of the BankAccount class (i.e., its public methods or properties) to
use it effectively, while the internal implementation details remain hidden.
In summary, encapsulation allows for a clear separation between the public interface and the
internal implementation of a class, providing users with a simplified and intuitive way to interact
with objects while hiding the complexity of how those interactions are handled internally.
Abstraction
Reduce complexity by hiding unnecessary details. E.g. when pressing a button on a tv remote,
we don't have to worry about, or interact directly with, the internal circuit board – those details
are abstracted away.
Example of abstraction:
class EmailService
{
public void sendEmail()
{
System.Console.WriteLine("Sending email...");
}
// ALL THE BELOW METHODS ARE PRIVATE -- THEY ARE NOT EXPOSED TO OTHER
CLASSES. OTHER CLASSES JUST WANT TO SEND EMAILS, NO NEED FOR THEM TO SEE
ALL THE COMPLEX DETAILS OF CONNECTING TO MAIL SERVER, AUTHENTICATING,
DISCONNECTING.
Without abstraction, the user would have more decisions to make, is exposed to more
information and complexity than is necessary to perform a task, and has to write more complex
code. If the above private methods were changed to public:
The methods become available via the EmailService public API. The user needs to know
more information and understand the internal logic involved in sending an email; their code
would end up looking like this:
Importantly, by using encapsulation, if any of those private methods are changed, e.g. they take
another parameter, then only the EmailService class has to change; classes using the
EmailService don't have to change. We can change the implementation details of
EmailService without it affecting other classes in our app.
Inheritance
Inheritance is a fundamental concept in object-oriented programming (OOP) that involves
creating new classes (subclasses or derived classes) based on existing classes (superclasses
or base classes). Subclasses inherit properties and behaviors from their superclasses and can
also add new features or override existing ones. Inheritance is often described in terms of an
"is-a" relationship.
A simple example, demonstrating inheritance and the “is-a” relationship: a Car is-a Vehicle, and
a Bike is-a Vehicle:
Now, all specific vehicles – such as cars, bikes, planes – can inherit common vehicle behavior,
and also include fields and methods specific to that particular type of vehicle:
Polymorphism
The word polymorphism is derived from Greek, and means "having multiple forms":
Poly = many
Morph = forms
Let’s say that we want to create a list of vehicles, then loop through it and perform an inspection
on each vehicle:
Notice the ugly code inside the foreach loop! Because vehicles is a list of any type of object
(Object), we have to figure out what type of object we are dealing with in each loop, then cast
it to the appropriate object type before we can access any information on the object.
This code will continue to get uglier as we add more vehicle types. For example, if we extended
our codebase to include a new Plane class, then we’d need to modify existing code – we’d
have to add another conditional check in the foreach loop for planes.
Introducing: Polymorphism…
Cars and motorcycles are both vehicles. They both share some common properties and
methods. So, let’s create a parent class that contains these shared properties and methods:
// Program.cs
In this example:
This demonstrates how polymorphism enables code to be written in a more generic and flexible
manner, allowing for easy extension and maintenance as new types of vehicles are added to the
system.
For example, if we wanted to add another vehicle, we don’t have to modify the code used to
inspect vehicles (“the client code”); we can just extend our code base, without modifying existing
code:
// Program.cs
The code to perform the vehicle inspections doesn’t have to change to account for a plane.
Everything still works, without having to modify our inspection logic.
We will discuss Extension vs Modification in more detail during the SOLID principles section of
the book. Hold tight for now!
Coupling
In object-oriented programming (OOP), coupling refers to the degree of dependency between
different classes or modules within a system. High coupling means that classes are tightly
interconnected, making it difficult to modify or maintain them independently. Low coupling, on
the other hand, indicates loose connections between classes, allowing for greater flexibility and
ease of modification.
If classes are tightly coupled, then modifying one class could break the other, which could break
our program.
Suppose we have two classes, Order and EmailSender, where the Order class is
responsible for placing an order on some eCommerce store, and the EmailSender class is
responsible for sending emails. In the bad example, the Order class directly creates an
instance of EmailSender to send an email after placing the order.
// Program.cs
In this example, the Order class is tightly coupled to the EmailSender class because it
directly creates an instance of EmailSender. This makes the Order class dependent on the
implementation details of EmailSender, and any changes to the EmailSender class may
require modifications to the Order class.
To reduce coupling, we can introduce an abstraction (e.g., an interface) between the Order
class and the EmailSender class. This allows the Order class to interact with the
EmailSender class through the abstraction, making it easier to replace or modify the
implementation of EmailSender without affecting the Order class.
The user can now easily switch between different notification services:
In this improved example, the Order class depends on the INotificationService interface
instead of the concrete EmailSender class. This decouples the Order class from the specific
implementation of the notification service, allowing different implementations (e.g.,
EmailSender, SMSNotifier, etc.) to be easily substituted without modifying the Order class.
This reduces coupling and improves the flexibility and maintainability of the codebase.
Composition
Composition involves creating complex objects by combining simpler objects or components. In
composition, objects are assembled together to form larger structures, with each component
object maintaining its own state and behavior. Composition is often described in terms of a
"has-a" relationship.
Example:
Consider a Car class that is composed of various components such as Engine, Wheels,
Chassis, and Seats. Each component is a separate class responsible for its own functionality.
The Car class contains instances of these component classes and delegates tasks to them.
class Program
{
static void Main(string[] args)
{
Car car = new Car();
car.StartCar();
}
}
In this example, the Car class uses composition to assemble its components – the Car class is
composed of an Engine, Wheels, a Chassis and Seats. Each component (e.g., Engine,
Wheels) is a separate class responsible for its own functionality. The Car class contains
instances of these component classes and delegates tasks to them (i.e. calls their methods
within its own methods).
Composition vs Inheritance
● When you need more flexibility in constructing objects by assembling smaller, reusable
components.
● When there is no clear "is-a" relationship between classes, and a "has-a" relationship is
more appropriate.
● When you want to avoid the limitations of inheritance, such as tight coupling and the
fragile base class problem – which we will look into shortly.
● When there is a clear "is-a" relationship between classes, and subclass objects can be
treated as instances of their superclass.
● When you want to promote code reuse by inheriting properties and behaviors from
existing classes.
Both composition and inheritance can be used to leverage polymorphism to allow objects to be
treated uniformly via their interface or parent class.
Let’s now look at the Fragile Base Class Problem to show you why you should generally use
composition over inheritance…
1. Inheritance Coupling: Inheritance creates a strong coupling between the base class
(superclass) and derived classes (subclasses). Any changes made to the base class can
potentially affect the behavior of all derived classes.
2. Ripple Effect: Modifying the implementation details, adding new methods, or changing
the behavior of a base class can have a ripple effect on all derived classes. This can
lead to unintended consequences and require extensive regression testing to ensure the
correctness of the entire hierarchy.
3. Limited Extensibility: The Fragile Base Class Problem limits the extensibility of
software systems, as modifications to the base class can become increasingly risky and
costly over time. Developers may avoid making necessary changes due to the fear of
breaking existing functionality.
4. Brittle Software: The Fragile Base Class Problem contributes to the brittleness of
software systems, where seemingly minor changes to one part of the codebase can
cause unexpected failures in other areas.
5. Mitigation Strategies: To mitigate the Fragile Base Class Problem, software developers
can use SOLID principles such as the Open/Closed Principle (OCP) and Dependency
Inversion Principle (DIP), as well as prefer Composition over Inheritance. These
approaches promote loose coupling, encapsulation, and modular design, reducing the
impact of changes in base classes.
In summary, the Fragile Base Class Problem highlights the challenges associated with
maintaining inheritance hierarchies in object-oriented software development. It underscores the
importance of designing software systems with extensibility and maintainability in mind, while
also considering alternative approaches to inheritance when appropriate.
Generally, it’s often recommended to use composition over inheritance, but there are
cases where inheritance makes more sense. Composition results in less coupling and
more flexibility. It is also easier to build classes out of various components than it is to
try to find commonality between them and build a family tree.
OK, you now know some very important OOP principles. Next, we’ll look at a way to model our
software systems in a graphical way…
Representing Classes
Dog class:
Value after “:” is the type. If there is no colon after the method, then void is the return type.
If the access modifier is omitted, in this book it should be assumed that fields are private and
methods are public.
Inheritance Relationship
Represented by an arrow. The Dog class inherits from, or extends, the Animal class:
or:
public class Dog : Animal
{
// ...
}
Composition Relationship
Represented by an arrow with a filled diamond.
Or
Association Relationship
Represented by an arrow:
Or
- Association: A Person has a Car, but is not composed of Car. A person holds a
reference to Car so it can interact with it, but a Person can exist without a Car.
- Composition: when a child object wouldn’t be able to exist without its parent object, e.g.
a hotel is composed of its rooms, and HotelBathroom cannot exist without Hotel
(destroy the hotel, you destroy the hotel bathroom – it can’t exist by itself). Another
example: if a Customer is destroyed, their ShoppingCart and Orders are lost too –
therefore Customer is composed of ShoppingCart and Orders. And if Orders are
lost, OrderDetails and ShippingInfo are lost – so Orders are composed of
ShippingInfo and OrderDetails.
Dependency Relationship
Represented by a dashed arrow:
Or
Above, Document is not a field in this class, but is used somewhere in the class – in this case
it's a parameter, but it could also be a local variable defined in the render() method. So,
somewhere in the Dog class, we have a reference, or dependency, to the Document class.
They were introduced by a guy called Robert C. Martin, also known as "Uncle Bob", in the early
2000s.
By following these principles, developers can create software designs that are easier to
understand, maintain, and extend, leading to higher-quality software that is more robust and
adaptable to change.
This principle encourages you to create classes that are more focussed and perform one single
well-defined task, rather than multiple tasks. Breaking up classes into smaller, more focused
units makes code easier to understand, maintain, and test.
In this example, the User class manages user data (username, email), and contains logic for
registering a user. This violates the SRP because the class has more than one reason to
change. It could change due to:
- Modifications in user data management – e.g. adding more fields, such as firstName,
gender, hobbies.
- Modifications to the logic of registering a user, e.g. we may choose to fetch a user from
the database by their username rather than their email.
To adhere to the Single Responsibility Principle, we should separate these responsibilities into
separate classes.
In the refactored code, the User class is responsible solely for representing user data. The
UserService class now handles user registration, separating concerns related to user data
management from user registration logic. The UserService class is responsible only for the
business logic of registering a user. This separation of responsibilities adheres to the Single
Responsibility Principle, making the code easier to understand, maintain, and extend.
This principle promotes the idea that existing code should be able to be extended with new
functionality without modifying its source code. It encourages the use of abstraction and
polymorphism to achieve this goal, allowing for code to be easily extended through inheritance
or composition.
Let's consider an example of a Shape class hierarchy that calculates the area of different
geometric shapes. Initially, this violates the Open/Closed Principle (OCP) because adding a new
shape requires modifying the existing code:
In this example, the Shape class has a method, CalculateArea(), that calculates the area
based on the type of shape. Adding a new shape, such as a triangle, would require modifying
the existing Shape class, violating the OCP.
To adhere to the Open/Closed Principle, we should design the system in a way that allows for
extension without modification. Let's refactor the code using inheritance and polymorphism:
public abstract class Shape
{
public abstract double CalculateArea();
}
In this refactored code, we define an abstract Shape class with an abstract CalculateArea()
method. Concrete shape classes (Circle and Rectangle) inherit from the Shape class and
provide their own implementations of CalculateArea(). Adding a new shape, such as a
triangle, would involve creating a new class – extending the codebase – that inherits from
Shape and implements CalculateArea(), without modifying existing code. This adheres to
the OCP by allowing for extension without modification.
Being able to add functionality without modifying existing code means that we don’t have to
worry as much about breaking existing working code and introducing bugs. Following the OCP
encourages us to design our software so that we add new features only by adding new code.
This helps us to build loosely-coupled, maintainable software.
Liskov Substitution Principle (LSP)
“Objects of a superclass should be replaceable with objects of its subclass without affecting the
correctness of the program.”
This principle ensures that inheritance hierarchies are well-designed and that subclasses
adhere to the contracts defined by their superclasses.
Violations of the LSP can lead to unexpected behavior or errors when substituting objects,
making code harder to reason about and maintain.
Let's consider an example involving a Rectangle class and a Square class, which inherit from
a common Shape class. Initially, we'll violate the LSP by not adhering to the behavior expected
from these classes. Then, we'll fix it to ensure that the principle is respected.
// Program.cs
Calculated area = 50
Perfect!
Now, in our program, our Square class inherits from, or extends, the Rectangle class,
because, mathematically, a square is just a special type of rectangle, where its height equals its
width. Because of this, we decided that Square should extend Rectangle – it’s like saying “a
square is a (special type of) rectangle”.
But look what happens if we substitute the Rectangle class for the Square class:
Calculated area = 25
Oh dear, LSP has been violated: we replaced the object of a superclass (Rectangle) with an
object of its subclass (Square), and it affected the correctness of our program. By modeling
Square as a subclass of Rectangle, and allowing width and height to be independently
set, we violate the LSP. When setting the width and height of a Square, it should retain its
squareness, but our implementation allows for inconsistency.
// Program.cs
In this corrected example, we redesign the Square class to directly set the side length. Now, a
Square is correctly modeled as a subclass of Shape, and it adheres to the Liskov Substitution
Principle.
How does this satisfy LSP? Well, we have a superclass, Shape, and subclasses Rectangle
and Square. Both Rectangle and Square maintain the correct expected behavior of a Shape
(in our case, providing an area), and they should both behave appropriately when interacting
with other parts of the program that expect shapes.
This principle encourages the creation of fine-grained interfaces that contain only the methods
required by the clients that use them. It helps prevent the creation of "fat" interfaces that force
clients to implement unnecessary methods, leading to cleaner and more maintainable code.
Let's consider an example involving 2D and 3D shapes, initially violating the ISP, and then we'll
fix it.
Violating ISP:
In this example, we have an IShape interface representing both 2D and 3D shapes. However,
the Volume() method is problematic for 2D shapes, like Circle and Rectangle, because
they don't have volume. This violates the ISP because clients (classes using the IShape
interface) may be forced to depend on methods they do not need.
Usually, if I try to call a method on an object that doesn’t exist, VS Code will tell me that I’m
making a mistake. But above, when I call circle.Volume(), VS code is like “no problem”.
And VS code is correct, because the IShape interface forces Circle to implement a
Volume() method, even though circles don’t have volume. It’s easy to see how violating ISP
can introduce bugs into a program – above, everything looks fine, until we run the program and
an exception gets thrown.
Fixing ISP
public interface IShape2D
{
double Area();
}
In the fixed example, we've segregated the IShape interface into two smaller, more focused
interfaces: IShape2D and IShape3D. Each shape class now implements only the interface that
is relevant to its functionality. This adheres to the Interface Segregation Principle by ensuring
that clients are not forced to depend on methods they do not use. Clients can now depend only
on the interfaces they need, promoting better code reuse and flexibility.
Dependency Inversion is the strategy of depending upon interfaces or abstract classes rather
than upon concrete classes. This principle promotes decoupling between modules and
promotes the use of interfaces or abstract classes to define dependencies, allowing for more
flexible and testable code.
Let's start with an example violating the DIP and then correct it.
In this example:
- The Car class directly creates an instance of the Engine class, leading to a tight
coupling between Car and Engine.
- If the Engine class changes, it may affect the Car class, violating the Dependency
Inversion Principle.
Fixing DIP:
To adhere to the Dependency Inversion Principle, we introduce an abstraction (interface)
between Car and Engine, allowing Car to depend on an abstraction instead of a concrete
implementation.
But what do you mean by “high level” and “low level” classes?
High-Level Class:
The high-level class is typically the one that represents the main functionality or business logic
of the application. It orchestrates the interaction between various components and is often more
abstract in nature.
In this example, the Car class can be considered the high-level class. It represents the main
functionality related to starting the car and driving it. The Car class is concerned with the overall
behavior of the car, such as controlling its movement.
Low-Level Class:
The low-level class is usually one that provides specific functionality or services that are used by
the high-level class. It typically deals with implementation details and is more concrete in nature.
In this example, the Engine class can be considered the low-level class. It provides the specific
functionality related to starting the engine. The Engine class encapsulates the details of how
the engine operates, such as ignition and combustion.
In summary:
The Car class is the high-level class, representing the main functionality of the application
related to the car's behavior.
The Engine class is the low-level class, providing specific functionality related to the operation
of the engine, which is used by the Car class.
OK – you now understand the very important SOLID principles. You are now ready to learn…
These patterns help in making the design more flexible, extensible, and maintainable by
promoting better communication and separation of concerns between objects and classes in the
system. Each pattern addresses specific design issues and provides a standardized solution to
common problems encountered in software development.
Memento Pattern
The Memento Pattern is used to restore an object to a previous state.
A common use case for the Memento Pattern is implementing an undo feature. For example,
most text editors, such as Microsoft Word, have undo features where you can undo things by
pressing Ctrl + Z on Windows, or Cmd + Z on Mac.
A simple way to implement this text editor in code would be to create a single Editor class and
have a field for title and content, and also have a field that stores each of the previous
values for each field in some list:
Problem: every time we add a new field, e.g. author, date, isPublished, we have to keep
storing lists of prev states (all the changes) for each field. Also, how would we implement the
undo feature? If the user changed the title, then changed the content, then pressed undo, the
current implementation has no knowledge of what the user last did – did they change the title or
the content?
How about this: instead of having multiple fields in this Editor class, we create a separate
class to store the state of our editor at a given time:
(Note the composition relationship: Editor is composed of, or has a field of, the EditorState
class).
This is a good solution as we can undo multiple times and we don't pollute the Editor class
with too many fields.
However, this solution is violating the Single Responsibility Principle, as our Editor class
currently has multiple responsibilities:
1. State management
2. Providing the features that we need from an editor
We should take all the state management stuff out of Editor and put it somewhere else:
The createState() method returns an EditorState object, hence the dotted line arrow
(dependency relationship). History has a field with a list of EditorStates, hence the diamond
arrow (composition relationship).
This is the Memento pattern. Here are the abstract names that each class would be in the
memento pattern:
These abstract names for the classes in the Memento Pattern come from the original Gang of
Four (GoF) book. Note that our solution differs slightly from the above pattern, as our Caretaker
class, History, also has a field that stores a reference to the Editor, so that the History
class can restore the Editor's state when the user clicks undo.
// Originator
public class Editor
{
public string Title { get; set; }
public string Content { get; set; }
// Memento
public class EditorState
{
// Editor state data:
// `readonly` so once created we cannot change it, adding robustness to
our code.
private readonly string _title;
private readonly string _content;
// Caretaker
public class History
{
private List<EditorState> _states = new List<EditorState>();
private Editor _editor;
_editor.Restore(prevState);
}
class Program
{
static void Main(string[] args)
{
Editor editor = new Editor();
History history = new History(editor);
history.Backup();
editor.Title = "Test";
history.Backup();
editor.Content = "Hello there, my name is Dan.";
history.Backup();
editor.Title = "The Life of a Developer: My Memoirs";
history.Undo();
Console.WriteLine("Title: " + editor.Title); // Title: Test
Console.WriteLine("Content: " + editor.Content); // Content: Hello
there, my name is Dan.
history.ShowHistory();
// History: Here's the list of mementos:
// 11/04/2024 12:11:18 / ()
// 11/04/2024 12:11:18 / (Test)
history.Undo();
Console.WriteLine("Title: " + editor.Title); // Title: Test
Console.WriteLine("Content: " + editor.Content); // Content:
history.Undo();
Console.WriteLine("Title: " + editor.Title); // Title:
Console.WriteLine("Content: " + editor.Content); // Content:
}
}
+ You can simplify the originator’s code by letting the caretaker maintain the history of the
originator’s state, satisfying the Single Responsibility Principle.
- The app might consume a lot of RAM if lots of mementos are created. E.g., if we have a
class that is heavy on memory, such as a Video class, then creating lots of snapshots of
videos will consume lots of memory.