Laboratory Training 5

Design Patterns

1 Training Task

Modify the previously created program of the individual assignment of Laboratory training #4 by building a code using the design patterns. Be sure to implement the "Facade", "Singleton", "Lazy Initialization" and "Factory Method" patterns. Additionally, you can apply the patterns "Abstract Factory" and "Observer", as well as any other patterns on your own.

2 Instructions

2.1 Design Patterns Summary

Design pattern is a description of the interaction of objects and classes, adapted to solve a specific task in a specific context.

A design pattern names, abstracts, and identifies the key aspects of a common design structure that make it useful for creating a reusable object-oriented design. Design patterns occupy an intermediate position between classes and application frameworks. Design patterns are more abstract than classes, but as architectural elements they are smaller than application frameworks.

The Model-View-Controller design pattern (MVC, Model-View-Controller) was mentioned earlier. It was first used on the Smalltalk-80 system. MVC may even be seen not as a pattern, but as a fundamental design concept whereby the application data model (business logic), user interface (visual representation) and user interaction are separated from each other.

The model provides data and methods for working with data and does not contain information about the visual representation of this data.

The view is responsible for the visual presentation of information. For example, this is a graphical user interface (a window with graphical elements).

The controller provides communication between the user, model, and presentation.

This distribution allows the development of a model regardless of the visual representation, as well as creating several different representations for one model.

For the first time design patterns were systematically outlined in the book "Patterns of Reusable Object-Oriented Software", by the authors of E Gamma R. Helm R. Johnson J. Vlissides (http://www.uml.org.cn/c++/pdf/DesignPatterns.pdf). This book was published in English in 1995 by Addison Wesley Longman, Inc. The further development of the patterns is reflected in the book "Applying UML and Patterns" by Craig Larman.

Pattern identification involves determining such elements:

  • name; naming patterns provides the ability to design at a higher level of abstraction;
  • problem; a description of when the pattern should be used;
  • solution; description of the elements of the design solution, the relationships between them, the functions of each element;
  • consequences of applying the pattern and possible compromises

The standard pattern description includes the definition of the following basic specifications:

  • Pattern Name and Classification;
  • Intent;
  • "Also Known As";
  • Motivation;
  • Applicability;
  • Structure;
  • Participants;
  • Collaborations;
  • Consequences;
  • Implementation;
  • Sample Code;
  • Known Uses;
  • Related Patterns.

Solving design tasks with the help of design patterns include the following steps:

  • finding the appropriate objects,
  • determining the degree of detail of the object,
  • specifying the interfaces of the object,
  • specifying the implementation of the object.

Design patterns are based on reuse mechanisms. These include:

  • inheritance and composition;
  • delegation;
  • parameterized types.

In order to use the design pattern, you need to read its description, make sure you understand the classes and objects mentioned, see the "Sample Code" section, define the appropriate names for the participants, define the classes, define the names of operations, implement operations that perform responsibilities and are responsible for the relationships defined in the design pattern.

2.2 Classification of Design Patterns

The following groups of design patterns can be distinguished:

  • creational design patterns; the most famous of them:
    • Abstract Factory
    • Builder
    • Factory Method
    • Dependency Injection
    • Lazy Initialization
    • Prototype
    • Object pool
    • Singleton
  • structural patterns; in the book "Design Patterns: Elements of Reusable Object-Oriented Software", they include:
    • Adapter
    • Bridge
    • Composite
    • Decorator
    • Facade
    • Flyweight
    • Proxy
  • behavioral patterns; these are, for example, the following patterns
    • Visitor
    • Interpreter
    • Iterator
    • Command
    • Chain of Responsibility
    • Mediator
    • Observer
    • State
    • Strategy
    • Memento
    • Template Method
  • Responsibility assignment patterns (General Responsibility Assignment Software Patterns, GRASP), by book "Applying UML and Patterns – An Introduction to Object-Oriented Analysis and Design and Iterative Development" by Craig Larman; for example, these are:
    • Information Expert
    • Creator
    • Low Coupling
    • Protected Variations

There are also system patterns, control patterns, and so on. The patterns of the first three groups are most commonly used.

2.3 Examples of Creational Patterns

2.3.1 Factory Method Pattern

Factory Method is a creational pattern that provides subclasses with an interface to create instances of a class. At the time of creation, the derived classes can determine which class to create. The factory delegates the creation of objects to the derived classes of the base class. This allows you to manipulate abstract objects at a higher level. Also known as Virtual Constructor.

The factory method is used in such cases:

  • the class does not know in advance whose objects of subclasses it needs to create;
  • the class is designed so that the objects it creates are specified by subclasses;
  • the class delegates its responsibility to one of several subclasses, and it is planned to localize the data on which class obtains this responsibility.

There are two approaches to implementing the Factory Method in C#. The first approach is based on the use of static methods. Objects of certain types are created depending on the state or parameters. Consider a simple example.

Suppose there is a Shape interface:

public interface IShape
{
    void Draw();
}

This interface is actively used in some library, including the need to create objects that implement the IShape interface. When using the library, we will need to work with some classes that implement this interface:

public class Circle : IShape
{
    public void Draw()
    {
        Console.WriteLine("Circle");
    }
}

public class Square : IShape
{
    public void Draw()
    {
        Console.WriteLine("Square");
    }

}

Now it is possible to create a class factory, in the method of which the object is created by the name of the type and perform the testing:

public class ShapeFactory
{
    public IShape GetShape(string shapeType)
    {
        switch (shapeType.ToUpper())
        {
            case "CIRCLE":
                return new Circle();
            case "SQUARE":
                return new Square();
            default:
                return null;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        ShapeFactory shapeFactory = new ShapeFactory();
        IShape shape1 = shapeFactory.GetShape("CIRCLE");
        shape1.Draw();
        IShape shape2 = shapeFactory.GetShape("SQUARE");
        shape2.Draw();
    }
}

Another approach is based on the use of virtual methods. Suppose there is an abstract class AbstractNumber that declares abstract function of finding the sum of two numbers and the abstract factory method, and also implements a method for finding the sum of the elements of an array of numbers. An abstract factory method is used to create an initial value of the sum:

public abstract class AbstractNumber
{
    abstract public AbstractNumber GetInstance(); // factory method
    abstract public AbstractNumber Sum(AbstractNumber a, AbstractNumber b);

    public AbstractNumber Sum(AbstractNumber[] arr)
    {
        AbstractNumber result = GetInstance();
        foreach (AbstractNumber elem in arr)
        {
            result = Sum(result, elem);
        }
        return result;
    }
}

Create a derived class IntegerNumber:

public class IntegerNumber : AbstractNumber
{
    private int k = 0;

    public IntegerNumber()
    {
    }

    public IntegerNumber(int k)
    {
        this.k = k;
    }

    override public AbstractNumber GetInstance()
    {
        return new IntegerNumber();
    }

    override public AbstractNumber Sum(AbstractNumber a, AbstractNumber b)
    {
        IntegerNumber result = new IntegerNumber();
        result.k = ((IntegerNumber)a).k + ((IntegerNumber)b).k;
        return result;
    }

    override public String ToString()
    {
        return "IntegerNumber { k = " + k + " }";
    }
}

Create a derived class ComplexNumber:

public class ComplexNumber : AbstractNumber
{
    private double re = 0;
    private double im = 0;

    public ComplexNumber()
    {
    }

    public ComplexNumber(double re, double im)
    {
        this.re = re;
        this.im = im;
    }

    override public AbstractNumber GetInstance()
    {
        return new ComplexNumber();
    }

    override public AbstractNumber Sum(AbstractNumber a, AbstractNumber b)
    {
        ComplexNumber result = new ComplexNumber();
        result.re = ((ComplexNumber)a).re + ((ComplexNumber)b).re;
        result.im = ((ComplexNumber)a).im + ((ComplexNumber)b).im;
        return result;
    }

    override public String ToString()
    {
        return "ComplexNumber { re = " + re + ", im = " + im + " }";
    }
}

Test all:

class Program
{
    static void Main(string[] args)
    {
        AbstractNumber[] arr1 = { new IntegerNumber(1), new IntegerNumber(2), new IntegerNumber(4) };
        AbstractNumber result1 = arr1[0]; // in this way we determine the type of object
        Console.WriteLine(result1.Sum(arr1));
        AbstractNumber[] arr2 = { new ComplexNumber(1, 2), new ComplexNumber(3, 4) };
        AbstractNumber result2 = arr2[0]; // in this way we determine the type of object
        Console.WriteLine(result2.Sum(arr2));
    }
}

Assigning items to an array is only necessary to determine the real type of object.

2.3.2 Abstract Factory Pattern

The Abstract Factory pattern is used when it is necessary to change the behavior of the system by varying the objects that are created while maintaining the interfaces. It allows you to create groups of interconnected objects that implement common behavior.

An abstract class factory defines the general interface of class factories. Subclasses have specific implementation of methods for creating different objects. Since the abstract factory implements the process of creating class-factories and the procedure of object initialization itself, it isolates the application from the details of class implementation.

Using an abstract factory has the following advantages:

  • isolates specific classes;
  • simplifies replacement of product families;
  • guarantees product compatibility;
  • the system should not depend on how the objects of which it consists are created, assembled and represented.

Interrelated family objects must be used together and this restriction must be met.

2.3.3 Lazy Initialization Pattern

The Lazy Initialization pattern assumes the creation of an object immediately before this object is first used. Thus, the initialization is performed "on demand". For example, the field of SomeType type contains a reference to an object that can be immediately created simultaneously with the field definition:

SomeType field = new SomeType();

void First()
{
    field.SetValue();
}

void Fecond()
{
    field.GetValue();
}

Sometimes a particular object may not be applicable, but its creation requires significant resources. Then it is better to create it in a certain method, which first refers to this object:

SomeType field = null;

void First()
{
    field = new SomeType();
    field.SetValue();
}

void Second()
{
    field.GetValue();
}

The problem is that the order of the method call can be arbitrary. If another method that uses this object is called first, we will get a NullReferenceException. If you create an object in different methods, the object will be new each time, and the previous one will be destroyed. To solve this problem, you can create a getter method that creates the necessary object:

SomeType field = null;

public SomeType GetField()
{
    if (field == null)
    {
        field = new SomeType();
    }
    return field;
}

Now it is important to ensure that all calls to the field are made only through the getter:

void First()
{
    GetField().SetValue();
}

void Second()
{
    GetField().GetValue();
}

2.3.4 Singleton Pattern

The Singleton pattern is applied when the only instance of some class (or no one) is allowed. It can be any large object that should not be replicated. For example, an object may be associated with a file in which debugging information is recorded, etc.

Using static data and methods instead of a single object cannot solve this problem, since the corresponding static data members are created automatically when we first access the class, but we need an approach that allows us to control the creation of a single object.

The easiest solution on C# is to apply the "factory method":

public class Singleton
{
    private static Singleton instance;

    private Singleton()
    {
    }

    public static Singleton GetInstance()
    {
        if (instance == null)
        {
            instance = new Singleton();
        }
        return instance;
    }
}

To avoid loss of scalability of the project, the Singleton pattern should be used only if the instance uniqueness is unconditional, and not as a temporary solution.

2.4 Examples of Structural Patterns

2.4.1 Adapter Pattern

The Adapter pattern turns the interface of one class into another interface, the one that customers expect. For example, there is some class with the necessary functionality, but it would be desirable if the class is part of a certain hierarchy, or so that it implements a certain interface. The C# language provides two ways to implement this pattern – through inheritance (we create a derived class that implements a specific interface) and through composition (we create a reference to the class that needs to be adapted). The second approach is more universal, since it can be applied to both interfaces and classes, to which another class must be adapted (which, moreover, can be implemented with the sealed modifier).

For example, there is a generalized SomeArray class that represents an array:

using System;
using System.Linq;

class SomeArray<T>
{
    private T[] arr = null;
    
    public SomeArray(params T[] arr)
    {
        this.arr = arr.ToArray(); // the function is added in LINQ through an extension mechanism
    }

    public int Size()
    {
        return arr.Length;
    }

    public T this[int i]
    {
        get => arr[i];
        set => arr[i] = value;
    }
    // other methods
}

Suppose we want to use this class instead of the standard generic List class:

IList<int> list = new SomeArray<int>(); // Error

Because SomeArray does not implement the IList interface, this will cause a syntax error. The SomeArray class needs to be adapted. Create a new class that implements the IList interface and contains a reference to SomeArray:

class ArrayAdapter<T> : IList<T>
{
    private SomeArray<T> someArray;
	
    public ArrayAdapter(params T[] arr)
    {
        someArray = new SomeArray<T>(arr);
    }
	
    public T this[int i]
    { 
        get => someArray[i]; 
        set => someArray[i] = value;
    }

    public int Count => someArray.Size();

    // other methods for implementing IList
}

Now this class can be used, for example, instead of List:

IList<int> list = new ArrayAdapter<int>();

2.4.2 Facade Pattern

The Facade pattern is a design pattern intended to unite a group of subsystems under one unified interface, providing access to them through one entry point.

Since a facade class can be used to create the only object, it is advisable to implement such a class using the Singleton pattern.

Consider the application of the Facade pattern for the previously discussed example with a bookshelf and books. In our case, the model is a library of classes that describes the domain. We add Facade class to the library, which contains a reference to the BookshelfWithLINQ class. If necessary, it may also contain references to other model objects.

namespace BookshelfLib
{
    public class Facade
    {
        private BookshelfWithLINQ<Author> bookshelf;
        // references to other model classes, if necessary

        // implementation of the Singleton pattern:
        // private modifier makes it impossible to create
// objects not through the getInstance () method: private static Facade instance = null; private Facade() { } public static Facade GetInstance() { if (instance == null) { instance = new Facade(); } return instance; } // Methods corresponding to the functions of the GUI application // and requiring interaction with the model: public void DoNew() { //... } public void DoOpen(string fileName) { //... } public void DoSave(string fileName) { //... } public void DoSort() { //... } } }

The controller now only interacts with the facade:

public partial class MainWindow : Window
{
    private Facade facade = Facade.GetInstance();
    // ...

    // Event handlers:

    private void ButtonNew_Click(object sender, RoutedEventArgs e)
    {
        facade.DoNew();
        // Update of visual components
    }

    // ...

}

2.5 Examples of Behavioral Patterns

2.5.1 Observer Pattern

The Observer pattern is used when it is necessary to determine the "one to many" connection between objects in such a way that when a state of a single object changes, all objects associated with it receive notifications about it and automatically change their state.

Consider the use of the pattern in this example. The OperationObserver class defines the update interface for the objects that should be notified of the subject's change:

using System;
using System.Collections.Generic;

public abstract class OperationObserver
{
    public abstract double ValueChanged(Rectangle observed);
}

There is a Rectangle class (subject), which has information about its observers and provides an interface for registering and notifying observers:

public class Rectangle
{
    private double width;
    private double height;

    private readonly List<OperationObserver> observerList = new List<OperationObserver>();

    public void AddObserver(OperationObserver observer)
    {
        observerList.Add(observer);
    }

    public double Width
    {
        get => width;
        set
        {
            width = value;
            NotifyObservers();
        }
    }

    public double Height
    {
        get => height;
        set
        {
            height = value;
            NotifyObservers();
        }
    }

    private void NotifyObservers()
    {
        foreach (OperationObserver o in observerList)
        {
            o.ValueChanged(this);
        }
    }

    override public String ToString()
    {
        String s = "Width = " + Width + " Height = " + Height + "\n";
        foreach (OperationObserver o in observerList)
        {
            s += o.ToString() + '\n';
        }
        return s;
    }
}

The Perimeter and Square classes are "subscribers" who receive messages about changing the size of the rectangle and update their data when they receive the message:

public class Perimeter : OperationObserver
{
    private double perimeter;

    override public double ValueChanged(Rectangle observed)
    {
        return perimeter = 2 * (observed.Width + observed.Height);
    }

    override public String ToString()
    {
        return "Perimeter = " + perimeter;
    }
}

public class Area : OperationObserver
{
    private double area;

    override public double ValueChanged(Rectangle observed)
    {
        return area = observed.Width * observed.Height;
    }

    override public String ToString()
    {
        return "Area = " + area;
    }
}

We can now test:

class Program
{
    static void Main(string[] args)
    {
        Rectangle rectangle = new Rectangle();
        rectangle.AddObserver(new Perimeter());
        rectangle.AddObserver(new Area());
        rectangle.Width = 1;
        rectangle.Height = 2;
        Console.WriteLine(rectangle);
        rectangle.Width = 3;
        Console.WriteLine(rectangle);
        rectangle.Height = 4;
        Console.WriteLine(rectangle);
    }
}

2.6 Solving design Problems using Patterns

Design patterns can be useful for solving a number of problems:

  • search for suitable objects; useful templates are Composer, Strategy, State;
  • determining the degree of detail of the object; useful are Facade, Flyweight, Abstract Factory, Builder, Visitor, Command;
  • specification of object interfaces; useful may be Decorator, Proxy, Visitor;
  • specifying the implementation of objects; used templates Abstract Factory, Builder, Factory Method, Prototype, Singleton;
  • reuse mechanisms; here used are State, Strategy, Visitor, Chain of Responsibility;
  • construction of runtime structures (Composer, Decorator, Chain of Responsibility);
  • designing considering future changes; all the design samples, especially the Facade, Bridge, Template Method, may be useful here.

3 Quiz

  1. What is a design pattern?
  2. Describe the components of the MVC pattern.
  3. When and by whom were the design patterns first systematically described?
  4. What elements do pattern identification include?
  5. What specifications does the standard pattern description include?
  6. What mechanisms are the patterns based on?
  7. Provide a classification of design patterns.
  8. What is the idea and implementation of the Factory Method pattern?
  9. How to implement Factory Method pattern in C#?
  10. What is the purpose of the Abstract Factory pattern?
  11. Why use the Lazy Initialization pattern?
  12. How to implement the Lazy Initialization pattern in C#?
  13. When is the Singleton pattern used?
  14. How to implement Singleton pattern in C#?
  15. What is the purpose of the Adapter pattern?
  16. How to implement the Pattern Adapter in C#?
  17. When and for what use is the Facade pattern?
  18. How is the Facade pattern related to the MVC pattern?
  19. What is the idea and implementation of the Observer pattern?
  20. How should patterns be used to solve software design problems?
  21. Give examples of the use of composition and polymorphism in design patterns.

 

up