Decorator: Difference between revisions
No edit summary |
No edit summary |
||
| (One intermediate revision by the same user not shown) | |||
| Line 25: | Line 25: | ||
Let's assume we have an entity "Data": | Let's assume we have an entity "Data": | ||
=== Data and Repository === | |||
<pre> | <pre> | ||
namespace DecoratorExample; | namespace DecoratorExample; | ||
public class Data | public class Data { | ||
{ | |||
public Guid Id { get; private set; } | public Guid Id { get; private set; } | ||
public string Name { get; private set; } | public string Name { get; private set; } | ||
| Line 57: | Line 57: | ||
// Skip the following Repository implementation if you like | // Skip the following Repository implementation if you like | ||
public class Repository : IRepository | public class Repository : IRepository { | ||
{ | |||
private string connectionString; | private string connectionString; | ||
public Repository( string dbFilePath ) | public Repository( string dbFilePath ) { | ||
connectionString = $"Data Source={dbFilePath};Version=3;"; | connectionString = $"Data Source={dbFilePath};Version=3;"; | ||
CreateMyTable(); // Ensure the table exists when the repository is instantiated. | CreateMyTable(); // Ensure the table exists when the repository is instantiated. | ||
| Line 71: | Line 69: | ||
/// </summary> | /// </summary> | ||
/// <param name="myData"></param> | /// <param name="myData"></param> | ||
public void InsertData( Data myData ) | public void InsertData( Data myData ) { | ||
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) { | |||
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) | |||
connection.Open(); | connection.Open(); | ||
using( SQLiteCommand command = new SQLiteCommand( connection ) ) | using( SQLiteCommand command = new SQLiteCommand( connection ) ) { | ||
command.CommandText = "INSERT INTO MyTable (Id, Name) VALUES (@id, @name)"; | command.CommandText = "INSERT INTO MyTable (Id, Name) VALUES (@id, @name)"; | ||
command.Parameters.AddWithValue( "@id", myData.Id.ToString() ); // Convert Guid to string | command.Parameters.AddWithValue( "@id", myData.Id.ToString() ); // Convert Guid to string | ||
| Line 91: | Line 86: | ||
/// </summary> | /// </summary> | ||
/// <returns></returns> | /// <returns></returns> | ||
public List<Data> GetAllData() | public List<Data> GetAllData() { | ||
List<Data> data = new List<Data>(); | List<Data> data = new List<Data>(); | ||
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) | using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) { | ||
connection.Open(); | connection.Open(); | ||
using( SQLiteCommand command = new SQLiteCommand( "SELECT Id, Name FROM MyTable", connection ) ) | using( SQLiteCommand command = new SQLiteCommand( "SELECT Id, Name FROM MyTable", connection ) ) { | ||
using( SQLiteDataReader reader = command.ExecuteReader() ) { | |||
using( SQLiteDataReader reader = command.ExecuteReader() ) | |||
while( reader.Read() ) | while( reader.Read() ) | ||
{ | { | ||
| Line 112: | Line 103: | ||
} | } | ||
} | } | ||
return data; | return data; | ||
} | } | ||
| Line 120: | Line 110: | ||
/// </summary> | /// </summary> | ||
/// <param name="myData"></param> | /// <param name="myData"></param> | ||
public void UpdateData( Data myData ) | public void UpdateData( Data myData ) { | ||
// Update database | // Update database | ||
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) | using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) { | ||
connection.Open(); | connection.Open(); | ||
using( SQLiteCommand command = new SQLiteCommand( connection ) ) | using( SQLiteCommand command = new SQLiteCommand( connection ) ) { | ||
command.CommandText = "UPDATE MyTable SET Name = @name WHERE Id = @id"; | command.CommandText = "UPDATE MyTable SET Name = @name WHERE Id = @id"; | ||
command.Parameters.AddWithValue( "@name", myData.Name ); | command.Parameters.AddWithValue( "@name", myData.Name ); | ||
| Line 141: | Line 128: | ||
/// </summary> | /// </summary> | ||
/// <param name="id"></param> | /// <param name="id"></param> | ||
public void DeleteData( Guid id ) | public void DeleteData( Guid id ) { | ||
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) { | |||
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) | |||
connection.Open(); | connection.Open(); | ||
using( SQLiteCommand command = new SQLiteCommand( connection ) ) | using( SQLiteCommand command = new SQLiteCommand( connection ) ) { | ||
command.CommandText = "DELETE FROM MyTable WHERE Id = @id"; | command.CommandText = "DELETE FROM MyTable WHERE Id = @id"; | ||
command.Parameters.AddWithValue( "@id", id.ToString() ); // Convert Guid to string | command.Parameters.AddWithValue( "@id", id.ToString() ); // Convert Guid to string | ||
| Line 156: | Line 140: | ||
} | } | ||
private void CreateMyTable() | private void CreateMyTable() { | ||
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) { | |||
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) | |||
connection.Open(); | connection.Open(); | ||
using( SQLiteCommand command = new SQLiteCommand( connection ) ) | using( SQLiteCommand command = new SQLiteCommand( connection ) ) { | ||
command.CommandText = "CREATE TABLE IF NOT EXISTS MyTable (Id TEXT PRIMARY KEY, Name TEXT)"; | command.CommandText = "CREATE TABLE IF NOT EXISTS MyTable (Id TEXT PRIMARY KEY, Name TEXT)"; | ||
command.ExecuteNonQuery(); | command.ExecuteNonQuery(); | ||
| Line 170: | Line 151: | ||
} | } | ||
private Guid ParseGuid( object value ) | private Guid ParseGuid( object value ) { | ||
if( value == DBNull.Value || value == null ) | if( value == DBNull.Value || value == null ) { | ||
return Guid.Empty; // Return a default value when the value is null | return Guid.Empty; // Return a default value when the value is null | ||
} | } else { | ||
return Guid.Parse( value.ToString() ); | return Guid.Parse( value.ToString() ); | ||
} | } | ||
} | } | ||
private string ParseString( object value ) | private string ParseString( object value ) { | ||
if( value == DBNull.Value || value == null ) { | |||
if( value == DBNull.Value || value == null ) | |||
return string.Empty; // Return an empty string when the value is null | return string.Empty; // Return an empty string when the value is null | ||
} | } else { | ||
return value.ToString(); | return value.ToString(); | ||
} | } | ||
| Line 212: | Line 185: | ||
List<Data> allData = repository.GetAllData(); | List<Data> allData = repository.GetAllData(); | ||
Console.WriteLine( "MAIN: All Data:" ); | Console.WriteLine( "MAIN: All Data:" ); | ||
foreach( var data in allData ) | foreach( var data in allData ) { | ||
Console.WriteLine( $"MAIN: ID: {data.Id}, Name: {data.Name}" ); | Console.WriteLine( $"MAIN: ID: {data.Id}, Name: {data.Name}" ); | ||
} | } | ||
| Line 232: | Line 204: | ||
// Check if the data was deleted | // Check if the data was deleted | ||
var deletedData = repository.GetAllData().Find( d => d.Id == newId ); | var deletedData = repository.GetAllData().Find( d => d.Id == newId ); | ||
if( deletedData == null ) | if( deletedData == null ) { | ||
Console.WriteLine( "MAIN: Data with ID not found. It has been deleted." ); | Console.WriteLine( "MAIN: Data with ID not found. It has been deleted." ); | ||
} | } else { | ||
Console.WriteLine( "MAIN: Data with ID still exists. Deletion failed." ); | Console.WriteLine( "MAIN: Data with ID still exists. Deletion failed." ); | ||
} | } | ||
| Line 245: | Line 214: | ||
[[File:Decorator inserted data.jpg|none|left|alt=Decorator inserted data console output]] | [[File:Decorator inserted data.jpg|none|left|alt=Decorator inserted data console output]] | ||
== Adding Functionality == | === Adding Functionality === | ||
The concrete repository implementation is also not that relevant right now; the important question is '''"How to add functionality?"''' Let's add some logging so that each time Data is updated, we would like to log that. We could add the logging to the repository method: | The concrete repository implementation is also not that relevant right now; the important question is '''"How to add functionality?"''' Let's add some logging so that each time Data is updated, we would like to log that. We could add the logging to the repository method: | ||
| Line 255: | Line 224: | ||
Another way to solve this is the usage of the decorator pattern. Please be aware, this is an example for understanding, not may be not "real world practically". | Another way to solve this is the usage of the decorator pattern. Please be aware, this is an example for understanding, not may be not "real world practically". | ||
== Decorator Class == | === Decorator Class === | ||
<pre> | <pre> | ||
namespace DecoratorExample; | namespace DecoratorExample; | ||
public class LoggingRepositoryDecorator : IRepository | public class LoggingRepositoryDecorator : IRepository { | ||
{ | |||
private readonly IRepository _repository; | private readonly IRepository _repository; | ||
public LoggingRepositoryDecorator( IRepository repository ) | public LoggingRepositoryDecorator( IRepository repository ) { | ||
_repository = repository; | _repository = repository; | ||
} | } | ||
public void InsertData( Data myData ) | public void InsertData( Data myData ) { | ||
Console.WriteLine( "LOGGING: Adding data: " + myData.Name ); | Console.WriteLine( "LOGGING: Adding data: " + myData.Name ); | ||
_repository.InsertData( myData ); | _repository.InsertData( myData ); | ||
} | } | ||
public List<Data> GetAllData() | public List<Data> GetAllData() { | ||
Console.WriteLine( "LOGGING: Getting all data" ); | Console.WriteLine( "LOGGING: Getting all data" ); | ||
return _repository.GetAllData(); | return _repository.GetAllData(); | ||
} | } | ||
public void UpdateData( Data myData ) | public void UpdateData( Data myData ) { | ||
Console.WriteLine( "LOGGING: Updating data: " + myData.Name ); | Console.WriteLine( "LOGGING: Updating data: " + myData.Name ); | ||
_repository.UpdateData( myData ); | _repository.UpdateData( myData ); | ||
} | } | ||
public void DeleteData( Guid id ) | public void DeleteData( Guid id ) { | ||
Console.WriteLine( "LOGGING: Deleting data by ID: " + id ); | Console.WriteLine( "LOGGING: Deleting data by ID: " + id ); | ||
_repository.DeleteData( id ); | _repository.DeleteData( id ); | ||
| Line 306: | Line 269: | ||
[[File:Decorator logging output.jpg|none|alt=Screenshot of Console of Decorator logging output]] | [[File:Decorator logging output.jpg|none|alt=Screenshot of Console of Decorator logging output]] | ||
== Class Diagram == | |||
General implementation: | General implementation: | ||
| Line 375: | Line 338: | ||
In general Decorator pattern makes usage of a "Decorator" abstract class or interface - since I skipped that our implementation is limited, but still represents the decorator pattern. | In general Decorator pattern makes usage of a "Decorator" abstract class or interface - since I skipped that our implementation is limited, but still represents the decorator pattern. | ||
== Practical Usage in C# == | |||
* '''Materialized Views in Data Access''': Utilize design patterns like the Abstract Factory to manage the creation of materialized views in applications, which can significantly improve query performance and application efficiency in data-intensive scenarios. | |||
* '''Service Communication''': Implement adapter or facade patterns to standardize communication between different services, especially when dealing with HTTP REST-based APIs or service bus architectures. This ensures a consistent and reliable data exchange process. | |||
* '''Dependency Injection''': Leverage design patterns to manage dependency injection, allowing for more flexible and testable code. Use extension methods to add additional functionality to injected services without altering their original contracts. | |||
* '''Type Template Discovery''': Apply factory or builder patterns to facilitate the dynamic discovery and instantiation of type templates, enhancing the flexibility and maintainability of the codebase. | |||
* '''Enhancing Functionality with Extension Methods''': Use extension methods to add new capabilities to existing classes or interfaces without modifying their source code, which is particularly useful when working with third-party libraries or APIs. | |||
Latest revision as of 14:03, 20 January 2025
Decorator Pattern
The Decorator pattern (Structural) allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class.
Benefits
- Flexibility: Decorators provide a more flexible approach to extending an object's behavior compared to subclassing.
- Non-intrusive: Add new responsibilities to objects without changing their existing classes.
- Scalability: Easily add new decorators to introduce additional behaviors without disturbing the existing infrastructure.
Use Cases
- Extension methods in C#: use this before the type in the first parameter of a static method within a static class.
- UI Customization: Enhancing user interface components with additional features without modifying the core components.
- Data Handling: Adding new data handling capabilities like encryption or compression to streams on-the-fly.
- Service Extensions: Introducing new functionalities to services in a cloud-first architecture without altering the original services.
How It Works
- Component: An interface for objects that can have responsibilities added.
- ConcreteComponent: A class that implements the Component interface.
- Decorator: An abstract class that wraps a Component and implements the Component interface.
- ConcreteDecorator: A subclass of Decorator that adds responsibilities to the component.
Code Example
Let's assume we have an entity "Data":
Data and Repository
namespace DecoratorExample;
public class Data {
public Guid Id { get; private set; }
public string Name { get; private set; }
public Data( Guid id, string name ) {
Id = id;
ChangeName( name );
}
public void ChangeName( string name = "" ) {
Name = name;
}
}
And we have a simple Repository class for interacting with our database. This class provides the classic four CRUD operations (Create, Read, Update, Delete):
namespace DecoratorExample;
public interface IRepository {
void DeleteData( Guid id );
List<Data> GetAllData();
void InsertData( Data myData );
void UpdateData( Data myData );
}
// Skip the following Repository implementation if you like
public class Repository : IRepository {
private string connectionString;
public Repository( string dbFilePath ) {
connectionString = $"Data Source={dbFilePath};Version=3;";
CreateMyTable(); // Ensure the table exists when the repository is instantiated.
}
/// <summary>
/// CREATE
/// </summary>
/// <param name="myData"></param>
public void InsertData( Data myData ) {
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) {
connection.Open();
using( SQLiteCommand command = new SQLiteCommand( connection ) ) {
command.CommandText = "INSERT INTO MyTable (Id, Name) VALUES (@id, @name)";
command.Parameters.AddWithValue( "@id", myData.Id.ToString() ); // Convert Guid to string
command.Parameters.AddWithValue( "@name", myData.Name );
command.ExecuteNonQuery();
}
}
}
/// <summary>
/// READ
/// </summary>
/// <returns></returns>
public List<Data> GetAllData() {
List<Data> data = new List<Data>();
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) {
connection.Open();
using( SQLiteCommand command = new SQLiteCommand( "SELECT Id, Name FROM MyTable", connection ) ) {
using( SQLiteDataReader reader = command.ExecuteReader() ) {
while( reader.Read() )
{
Guid id = ParseGuid( reader[ "Id" ] );
string name = ParseString( reader[ "Name" ] );
data.Add( new Data( id, name ) );
}
}
}
}
return data;
}
/// <summary>
/// UPDATE
/// </summary>
/// <param name="myData"></param>
public void UpdateData( Data myData ) {
// Update database
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) {
connection.Open();
using( SQLiteCommand command = new SQLiteCommand( connection ) ) {
command.CommandText = "UPDATE MyTable SET Name = @name WHERE Id = @id";
command.Parameters.AddWithValue( "@name", myData.Name );
command.Parameters.AddWithValue( "@id", myData.Id.ToString() ); // Convert Guid to string
command.ExecuteNonQuery();
}
}
}
/// <summary>
/// DELETE
/// </summary>
/// <param name="id"></param>
public void DeleteData( Guid id ) {
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) {
connection.Open();
using( SQLiteCommand command = new SQLiteCommand( connection ) ) {
command.CommandText = "DELETE FROM MyTable WHERE Id = @id";
command.Parameters.AddWithValue( "@id", id.ToString() ); // Convert Guid to string
command.ExecuteNonQuery();
}
}
}
private void CreateMyTable() {
using( SQLiteConnection connection = new SQLiteConnection( connectionString ) ) {
connection.Open();
using( SQLiteCommand command = new SQLiteCommand( connection ) ) {
command.CommandText = "CREATE TABLE IF NOT EXISTS MyTable (Id TEXT PRIMARY KEY, Name TEXT)";
command.ExecuteNonQuery();
}
}
}
private Guid ParseGuid( object value ) {
if( value == DBNull.Value || value == null ) {
return Guid.Empty; // Return a default value when the value is null
} else {
return Guid.Parse( value.ToString() );
}
}
private string ParseString( object value ) {
if( value == DBNull.Value || value == null ) {
return string.Empty; // Return an empty string when the value is null
} else {
return value.ToString();
}
}
}
Test Code
static void Main( string[] args ) {
string dbFilePath = "your_database.db";
var repository = new Repository( dbFilePath );
// Insert a new Data object
Guid newId = Guid.NewGuid();
var newData = new Data( newId, "Sample Data" );
repository.InsertData( newData );
Console.WriteLine( "MAIN: Inserted data." );
// Retrieve all Data objects
List<Data> allData = repository.GetAllData();
Console.WriteLine( "MAIN: All Data:" );
foreach( var data in allData ) {
Console.WriteLine( $"MAIN: ID: {data.Id}, Name: {data.Name}" );
}
// Update an existing Data object
newData.ChangeName( "Updated Data" );
repository.UpdateData( newData );
Console.WriteLine( "MAIN: Updated data." );
// Retrieve the updated object
var updatedData = repository.GetAllData().Find( d => d.Id == newId );
Console.WriteLine( $"MAIN: Updated Data - ID: {updatedData.Id}, Name: {updatedData.Name}" );
// Delete a Data object
repository.DeleteData( newId );
Console.WriteLine( "MAIN: Deleted data." );
// Check if the data was deleted
var deletedData = repository.GetAllData().Find( d => d.Id == newId );
if( deletedData == null ) {
Console.WriteLine( "MAIN: Data with ID not found. It has been deleted." );
} else {
Console.WriteLine( "MAIN: Data with ID still exists. Deletion failed." );
}
}
Adding Functionality
The concrete repository implementation is also not that relevant right now; the important question is "How to add functionality?" Let's add some logging so that each time Data is updated, we would like to log that. We could add the logging to the repository method:
But that would be bad for multiple reasons. For one, the repository class’s job is interacting with the database, not logging. Also, we modify existing code, which goes against the Open/Closed principle.
Another way to solve this is the usage of the decorator pattern. Please be aware, this is an example for understanding, not may be not "real world practically".
Decorator Class
namespace DecoratorExample;
public class LoggingRepositoryDecorator : IRepository {
private readonly IRepository _repository;
public LoggingRepositoryDecorator( IRepository repository ) {
_repository = repository;
}
public void InsertData( Data myData ) {
Console.WriteLine( "LOGGING: Adding data: " + myData.Name );
_repository.InsertData( myData );
}
public List<Data> GetAllData() {
Console.WriteLine( "LOGGING: Getting all data" );
return _repository.GetAllData();
}
public void UpdateData( Data myData ) {
Console.WriteLine( "LOGGING: Updating data: " + myData.Name );
_repository.UpdateData( myData );
}
public void DeleteData( Guid id ) {
Console.WriteLine( "LOGGING: Deleting data by ID: " + id );
_repository.DeleteData( id );
}
}
Then, instead of just using the Repository class in the Main method
var repository = new Repository( dbFilePath );
We create a LoggingRepositoryDecorator object:
var repository = new LoggingRepositoryDecorator( new Repository( dbFilePath ) );
Now, if we call this, the logging has been added without changing the original implementation of the repository:
Class Diagram
General implementation:
}
Our implementation:
In general Decorator pattern makes usage of a "Decorator" abstract class or interface - since I skipped that our implementation is limited, but still represents the decorator pattern.
Practical Usage in C#
- Materialized Views in Data Access: Utilize design patterns like the Abstract Factory to manage the creation of materialized views in applications, which can significantly improve query performance and application efficiency in data-intensive scenarios.
- Service Communication: Implement adapter or facade patterns to standardize communication between different services, especially when dealing with HTTP REST-based APIs or service bus architectures. This ensures a consistent and reliable data exchange process.
- Dependency Injection: Leverage design patterns to manage dependency injection, allowing for more flexible and testable code. Use extension methods to add additional functionality to injected services without altering their original contracts.
- Type Template Discovery: Apply factory or builder patterns to facilitate the dynamic discovery and instantiation of type templates, enhancing the flexibility and maintainability of the codebase.
- Enhancing Functionality with Extension Methods: Use extension methods to add new capabilities to existing classes or interfaces without modifying their source code, which is particularly useful when working with third-party libraries or APIs.