A design pattern or pattern is a repeatable architectural construct that represents a solution to a design problem within a context that often arises.
It seems that we have heard this definition a thousand times ... In addition to knowledge of terms and patterns, it is interesting to know how they are used in real projects.
In this article, I will look at several of the most popular patterns used in .NET. Some of them are deeply integrated into the .NET infrastructure, while others are simply applied when designing base classes in the BCL.
')
Not one dozen books are devoted to design patterns, but one book stands alone and this is the famous book “Gangs of Four”. Therefore, for a better understanding of the situation, I will give a brief description of this book.
We will look at the following six patterns:
- observer;
- iterator;
- decorator;
- adapter;
- factory;
- strategy.
So, let's begin.
Observer
Perhaps the most famous pattern that is applied in .NET is the observer. It is known because it laid the foundation for delegates who have been an integral part of .NET since its inception. It is difficult not to appreciate their contribution to the .NET object model, especially considering what they have evolved into in later versions (lambdas, closures, etc.).
The purpose of the pattern observer from GOF: determines the one-to-many dependency between objects in such a way that when a state of a single object changes, all dependent on it are notified of this and automatically updated.
The simplest implementation of this pattern in C # may look something like this:
Simple observerpublic interface IObserver
As I said in the .NET platform, delegates implement the abstraction of the observer. For more convenient work with delegates in C # events are used. In the following example, we will use them:
Observer in .NET public delegate void MyEventHandler();
Analogy with the observer pattern on the face. An event acts as a subject, while delegates as an observer.
On a noteThe code described above has one drawback: when using lambda expressions, you will not be able to detach the delegate, that is, the code
s.MyEvent -= () => Console.WriteLine("Hello habrahabr");
will not remove this delegate since it is a different instance. To further detach a delegate, you will have to save the delegate (lambda expression) in a variable.
static void Main(string[] args) { Subject s = new Subject(); MyEventHandler myEv1 = () => Console.WriteLine("Hello habarhabr"); MyEventHandler myEv2 = () => Console.WriteLine("Hello world"); s.MyEvent += myEv1; s.MyEvent += myEv2; s.MyEvent -= myEv1; s.RaiseEvent();
In .NET 4.0, interfaces have appeared that allow you to directly implement an observer pattern. They look like this:
public interface IObservable<out T> { IDisposable Subscribe(IObserver<T> observer); } public interface IObserver<in T> { void OnCompleted(); void OnError(Exception error); void OnNext(T value); }
As you can see, the IObservable interface is implemented a little differently than our observer. Instead of having methods for adding and removing observers, it only has a method that registers an observer. The method returns an implementation of IDisposable, which allows observers to cancel notifications at any time before the supplier stops sending them. You can now use lambda expressions, and then detach them by calling Dispose.
On a noteIterator
The following pattern, no less popular in use in .NET, is an iterator.
Pattern assignment iterator from GOF: provides a method of sequential access to all elements of a compound object, without revealing its internal representation.
On the .NET platform, two interfaces are responsible for implementing the iterator pattern: IEnumerable and IEnumerator
public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); }
as well as their generalized analogues:
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); } public interface IEnumerator<out T> : IDisposable, IEnumerator { T Current { get; } }
In order to be able to iterate some entity using the foreach loop, it is necessary to implement the IEnumerable <> interface, as well as to create an iterator itself - a class / structure implementing the IEnumerator <> interface.
Given the fact that iterator blocks appeared in C # 2.0 using the
yield keyword, creating custom types that implement the IEnumerator <> interface now rarely occurs (the compiler does this for us).
The foreach loop works side by side with the iterator pattern. Following code
foreach (var item in Enumerable.Range(0, 10)) { Console.WriteLine(item); }
it's just syntactic sugar for the code:
IEnumerator<int> enumerator = Enumerable.Range(0, 10).GetEnumerator(); try { while (enumerator.MoveNext()) { int item = enumerator.Current; Console.WriteLine(item); } } finally { if (enumerator != null) { enumerator.Dispose(); } }
Thus, the iterator pattern is the basis of the C # language, since its language construct (foreach) uses it.
On a noteThe fact that the foreach loop does not actually require the iterated entity to implement the IEnumerable interface (and only requires the presence of certain methods with given signatures) was written by many, so I will not talk about this.
SergeyTeplyakov SergeyT has a good
post dedicated to the work of the foreach cycle.
In .NET, there are several optimizations regarding the foreach loop. Since each iteration creates an iterator object, this can adversely affect garbage collection, especially if there are several foreach nested loops, so when iterating over arrays and strings (types deeply integrated into the CLR), the foreach loop turns into a normal for loop.
On a noteDecorator
Pattern number 3 - decorator objects.
Pattern assignment decorator from GOF: dynamically adds new responsibilities to the object. It is a flexible alternative to spawning subclasses to extend functionality.
An abstraction decorator in .NET represents the System.IO.Stream class and its heirs. Consider the following code:
public static void WriteBytes(Stream stream) { int oneByte; while ((oneByte = stream.ReadByte()) >= 0) { Console.WriteLine(oneByte); } } static void Main(string[] args) { var memStream = new MemoryStream(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); WriteBytes(memStream); var buffStream = new BufferedStream(memStream); WriteBytes(buffStream); Console.ReadLine(); }
The WriteBytes method works with any stream, but it is not always effective. Reading one byte from the disk is not very good, so we can use the BufferedStream class, which reads data as a block and then returns it quickly. The BufferedStream class in the constructor takes a type Stream (any stream), thereby wrapping (decorating) it into a more efficient wrapper. BufferedStream overrides the basic methods of the Stream class, such as Read and Write, to provide more functionality.
The System.Security.Cryptography.CryptoStream class allows you to encrypt and decrypt streams on the fly. It also accepts a Stream parameter in the constructor, wrapping it in its shell.

For any stream, we can add the ability to efficiently read by surrounding it with a BufferedStream, without changing the data access interface. Since we do not inherit functionality, but only “decorate” it, we can do it at runtime, and not at compilation as it would be if we used inheritance.
Adapter
Assigning a pattern adapter from GOF: converts the interface of one class to the interface of another that customers expect. The adapter ensures that classes work with incompatible interfaces, which would be impossible without it.
COM and .NET have a different internal architecture. Therefore, it is necessary to adapt one interface to another. The CLR provides access to COM objects through an intermediary, called the Runtime Shell Called (RCW).
The runtime creates one runtime shell called per COM object, regardless of the number of existing references to this object.

Among other operations, the invoked runtime shell marshals data between managed and unmanaged code on behalf of a wrapped object. In particular, the invoked runtime shell marshalls the arguments and return values ​​of the method if the data exchanged between the client and server is represented in them differently.
For example, when a .NET client sends an unmanaged object as part of an argument of type String, the shell converts this string to BSTR (the
post in which I described the features of strings in .NET). If a COM object returns BSTR data to a managed caller, the caller will receive String data. Both the client and server send and receive data in a manner that they understand.
In fact, RCW is an adapter that converts one interface to another.
Factory
The fifth pattern is a kind of factory.
Purpose of a pattern factory from GOF: provides an interface for creating families of interrelated or interdependent objects without specifying their specific classes.
The
System.Convert class contains a set of static methods that allow you to create objects without explicitly calling their constructors, and works like a factory. To convert an integer to a boolean, for example, we can call and pass an integer to the Convert.ToBoolean method as a parameter. The return value of this method will be true if the number was non-zero and false otherwise. Other type conversion methods work similarly.
Such a strategy for creating new instances of objects is known as the Factory pattern. We can ask the factory to create the necessary object without invoking the constructor. Thus, the Factory pattern can hide the complexity of creating an object. If we want to change the details of the creation of an object, we just need to change the factory itself, we will not have to change every place in the code in which the constructor is called.
The
Activator.CreateInstance method in combination with information about types implements an abstract class factory, i.e. one that can create instances of any type.
Strategy
The purpose of the pattern strategy from GOF: defines a family of algorithms, encapsulates each of them and makes them interchangeable. The strategy allows you to change algorithms, regardless of the customers who use them.
A good example of using this simple pattern is sorting arrays. One of the overloads of the Array.Sort method takes a parameter of type IComparable. Using this interface, we can create a whole series of algorithms for sorting the so-called strategies that are independent of each other and are easily interchangeable.
public interface IComparable<T> { int CompareTo(T other); }
The sorting code is virtually independent of the element comparison algorithms and may remain unchanged.
This also includes the Array.BinarySearch methods, which also accepts the IComparable interface and the Array.Find method, which the Predicate delegate accepts. Thus, by varying the various delegates (strategies) we can change the behavior of the method and get the result we need.
In general, the pattern strategy is used very often. Before writing this article, I didn’t really think that I use the pattern strategy when sorting arrays with different comparators.
Conclusion
Now that we have covered some of the patterns used in the .NET Framework, I think it should be even easier for you to recognize them in the code you are working with. It is hard not to appreciate the contribution of events and iterators to the C # language and the .NET infrastructure in general, so knowing that this is the implementation of classic design patterns is simply necessary.
Thanks for reading. I hope the article was helpful.