Object oriented - three tier architecture

Presentation layer

The presentation layer, also known as the presentation layer UI, is located at the top of the three-tier architecture and is in direct contact with users, mainly Web browsing pages in the B/S information system. As a Web browsing page, the main function of the presentation layer is to realize the input and output of system data. In this process, the data can be transmitted to the BBL system for data processing without the help of logical judgment operation. After processing, the processing results will be fed back to the presentation layer. In other words, the presentation layer is to realize the user interface function, convey and feed back the user's needs, and debug with BLL or Models to ensure the user experience [4]

Business logic layer

The function of the business logic layer BLL is to logically judge and perform operations on specific problems. After receiving the user instructions of the presentation layer UI, it will connect the data access layer DAL. The access layer is located in the middle of the presentation layer and the data layer in the three-tier architecture. At the same time, it is also a bridge between the presentation layer and the data layer to realize the data connection and instruction transmission between the three layers, It can logically process the received data, realize the functions of data modification, acquisition and deletion, and feed back the processing results to the presentation layer UI to realize the software functions.

Data access layer

The data access layer DAL is the main control system of the database, which realizes the operations of data addition, deletion, modification and query, and feeds back the operation results to the business logic layer BLL

structural system

 
Architecture of three-tier architecture: entity class objects of object model are used to transfer data between presentation layer and business logic layer, entity class objects of object model are used to transfer data between business logic layer and data access layer, and the data access layer operates the database through ADO.NET components provided by. NET, Or use the stored procedures of SQL Server database server to complete data operations. The three-tier architecture is shown in Figure 1. [1]  
such Layered architecture It has the following four advantages: [1]  
(1) Avoid direct access to the presentation layer Data access layer , the presentation layer is only related to the business logic layer, which improves the data security. [1]  
(2) It is conducive to the decentralized development of the system. Each layer can be developed by different personnel. As long as the interface standards are followed and the same object model entity classes are used, the development speed of the system can be greatly improved. [1]  
(3) It is convenient to transplant the system. If you want to turn a C/S system into a B/S system, you only need to modify the presentation layer of the three-tier architecture. The business logic layer and data access layer can easily transplant the system to the network without modification. [1]  
(4) The project structure is clearer and the division of labor is clearer, which is conducive to later maintenance and upgrading.
 

case

In this article, we will explain dependency injection and IoC containers by refactoring a very simple code example with C# refactoring.  

Introduction:

Dependency injection and IoC may seem quite complex at first glance, but they are very easy to learn and understand.

In this article, we will explain dependency injection and IoC containers by refactoring a very simple code example in C#.

requirement:

Build an application that allows users to view available products and search for products by name.

First attempt:

We'll start by creating a layered architecture. There are several benefits to using a layered architecture, but we won't list them in this article because we focus on dependency injection.

  The following is the class diagram of the application:

First, we will start by creating a Product class:

public class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

Then we will create a data access layer:

public class ProductDAL
{
    private readonly List<Product> _products;

    public ProductDAL()
    {
        _products = new List<Product>
        {
            new Product { Id = Guid.NewGuid(), Name= "iPhone 9", 
                          Description = "iPhone 9 mobile phone" },
            new Product { Id = Guid.NewGuid(), Name= "iPhone X", 
                          Description = "iPhone X mobile phone" }
        };
    }

    public IEnumerable<Product> GetProducts()
    {
        return _products;
    }

    public IEnumerable<Product> GetProducts(string name)
    {
        return _products
            .Where(p => p.Name.Contains(name))
            .ToList();
    }
}

Then we will create a business layer:

public class ProductBL
{
    private readonly ProductDAL _productDAL;

    public ProductBL()
    {
        _productDAL = new ProductDAL();
    }

    public IEnumerable<Product> GetProducts()
    {
        return _productDAL.GetProducts();
    }

    public IEnumerable<Product> GetProducts(string name)
    {
        return _productDAL.GetProducts(name);
    }
}

Finally, we will create the UI:

class Program
{
    static void Main(string[] args)
    {
        ProductBL productBL = new ProductBL();

        var products = productBL.GetProducts();

        foreach (var product in products)
        {
            Console.WriteLine(product.Name);
        }

        Console.ReadKey();
    }
}

The code we have written in the first attempt is a good work result, but there are several problems:

1. We can't let three different teams work on each layer.

2. The business layer is difficult to expand because it depends on the implementation of the data access layer.

3. The business layer is difficult to maintain because it depends on the implementation of the data access layer.

4. The source code is difficult to test.

Second attempt:

High level objects should not depend on low-level objects. Both must rely on abstraction. So what are abstract concepts?

Abstraction is the definition of function. In our example, the business layer relies on the data access layer to retrieve books. In C #, we use interfaces to implement abstractions. Interfaces represent abstractions of functions.

Let's create an abstraction.

The following is the abstraction of the data access layer:

public interface IProductDAL
{
    IEnumerable<Product> GetProducts();
    IEnumerable<Product> GetProducts(string name);
}

  We also need to update the data access layer:

public class ProductDAL : IProductDAL

We also need to update the business layer. In fact, we will update the business layer to rely on the abstraction of the data access layer rather than the implementation of the data access layer:

public class ProductBL
{
    private readonly IProductDAL _productDAL;

    public ProductBL()
    {
        _productDAL = new ProductDAL();
    }

    public IEnumerable<Product> GetProducts()
    {
        return _productDAL.GetProducts();
    }

    public IEnumerable<Product> GetProducts(string name)
    {
        return _productDAL.GetProducts(name);
    }
}

We must also create an abstraction of the business layer:

public interface IProductBL
{
    IEnumerable<Product> GetProducts();
    IEnumerable<Product> GetProducts(string name);
}

We also need to update the business layer:

public class ProductBL : IProductBL

Finally, we need to update the UI:

class Program
{
    static void Main(string[] args)
    {
        IProductBL productBL = new ProductBL();

        var products = productBL.GetProducts();

        foreach (var product in products)
        {
            Console.WriteLine(product.Name);
        }

        Console.ReadKey();
    }
}

The code we made in the second attempt is effective, but we still rely on the specific implementation of the data access layer:

public ProductBL()
{
    _productDAL = new ProductDAL();
}

So, how to solve it?

This is where the dependency injection pattern works.

Final attempt

So far, all our work has nothing to do with dependency injection.

In order for the higher-level business layer to rely on the functions of lower-level objects without specific implementation, classes must be created by others. Others must provide a concrete implementation of the underlying object, which is what we call dependency injection. It literally means that we inject dependent objects into higher-level objects. One way to implement dependency injection is to use constructors for dependency injection.

Let's update the business layer:

public class ProductBL : IProductBL
{
    private readonly IProductDAL _productDAL;

    public ProductBL(IProductDAL productDAL)
    {
        _productDAL = productDAL;
    }

    public IEnumerable<Product> GetProducts()
    {
        return _productDAL.GetProducts();
    }

    public IEnumerable<Product> GetProducts(string name)
    {
        return _productDAL.GetProducts(name);
    }
}

  Infrastructure must provide dependencies on Implementation:

class Program
{
    static void Main(string[] args)
    {
        IProductBL productBL = new ProductBL(new ProductDAL());

        var products = productBL.GetProducts();

        foreach (var product in products)
        {
            Console.WriteLine(product.Name);
        }

        Console.ReadKey();
    }
}

The control of creating a data access layer is combined with the infrastructure. This is also called control reversal. Instead of creating an instance of the data access layer in the business layer, we create it in the of the infrastructure.   The Main method injects the instance into the business logic layer. Therefore, we inject instances of low-level objects into instances of high-level objects.

This is called dependency injection.

Now, if we look at the code, we only rely on the abstraction of the data access layer in the business access layer, which uses the interface implemented by the data access layer. Therefore, we follow the principle that both higher-level objects and lower-level objects depend on abstraction. Abstraction is the contract between higher-level objects and lower-level objects.

Now, we can let different teams work on different layers. We can let one team deal with the data access layer, one team deal with the business layer, and one team deal with the UI.

Next, the benefits of maintainability and scalability are shown. For example, if we want to create a new data access layer for SQL Server, we only need to implement the abstraction of the data access layer and inject instances into the infrastructure.

Finally, the source code is now testable. Because we use interfaces everywhere, we can easily provide another implementation in lower unit tests. This means that lower tests will be easier to set up.

Now let's test the business layer.

We will use xUnit for unit testing and Moq to simulate the data access layer.

The following is the unit test of the business layer:

public class ProductBLTest
{
    private readonly List<Product> _products = new List<Product>
    {
        new Product { Id = Guid.NewGuid(), Name= "iPhone 9", 
                      Description = "iPhone 9 mobile phone" },
        new Product { Id = Guid.NewGuid(), Name= "iPhone X", 
                      Description = "iPhone X mobile phone" }
    };
    private readonly ProductBL _productBL;

    public ProductBLTest()
    {
        var mockProductDAL = new Mock<IProductDAL>();
        mockProductDAL
            .Setup(dal => dal.GetProducts())
            .Returns(_products);
        mockProductDAL
            .Setup(dal => dal.GetProducts(It.IsAny<string>()))
            .Returns<string>(name => _products.Where(p => p.Name.Contains(name)).ToList());

        _productBL = new ProductBL(mockProductDAL.Object);
    }

    [Fact]
    public void GetProductsTest()
    {
        var products = _productBL.GetProducts();
        Assert.Equal(2, products.Count());
    }

    [Fact]
    public void SearchProductsTest()
    {
        var products = _productBL.GetProducts("X");
        Assert.Single(products);
    }
}

As you can see, it's easy to set up unit tests using dependency injection.

IoC container

Containers are just things that help implement dependency injection. Container, which usually realizes three different functions:

1. Mapping between registered interface and concrete implementation

2. Create objects and resolve dependencies

3. Release

Let's implement a simple container to register mappings and create objects.

First, we need a data structure to store mappings. We will choose Hashtable. The data structure stores the mapping.

First, we will initialize the Hashtable in the constructor of the container. Then, we will create a RegisterTransient method to register the mapping. Finally, we will create a method to create an object   Create  : 

public class Container
{
    private readonly Hashtable _registrations;

    public Container()
    {
        _registrations = new Hashtable();
    }

    public void RegisterTransient<TInterface, TImplementation>()
    {
        _registrations.Add(typeof(TInterface), typeof(TImplementation));
    }

    public TInterface Create<TInterface>()
    {
        var typeOfImpl = (Type)_registrations[typeof(TInterface)];
        if (typeOfImpl == null)
        {
            throw new ApplicationException($"Failed to resolve {typeof(TInterface).Name}");
        }
        return (TInterface)Activator.CreateInstance(typeOfImpl);
    }
}

Finally, we will update the UI:

class Program
{
    static void Main(string[] args)
    {
        var container = new Container();
        container.RegisterTransient<IProductDAL, ProductDAL>();

        IProductBL productBL = new ProductBL(container.Create<IProductDAL>());
        var products = productBL.GetProducts();

        foreach (var product in products)
        {
            Console.WriteLine(product.Name);
        }

        Console.ReadKey();
    }
}

Now, let's implement the Resolve method in the container. This method resolves dependencies.

The Resolve method is as follows:

public T Resolve<T>()
{
    var ctor = ((Type)_registrations[typeof(T)]).GetConstructors()[0];
    var dep = ctor.GetParameters()[0].ParameterType;
    var mi = typeof(Container).GetMethod("Create");
    var gm = mi.MakeGenericMethod(dep);
    return (T)ctor.Invoke(new object[] { gm.Invoke(this, null) });
}

Then we can use the following Resolve method in the UI:

class Program
{
    static void Main(string[] args)
    {
        var container = new Container();
        container.RegisterTransient<IProductDAL, ProductDAL>();
        container.RegisterTransient<IProductBL, ProductBL>();

        var productBL = container.Resolve<IProductBL>();
        var products = productBL.GetProducts();

        foreach (var product in products)
        {
            Console.WriteLine(product.Name);
        }

        Console.ReadKey();
    }
}

In the above source code, the container uses the container. Resolve < iproductbl > () method to create an object of the ProductBL class. The ProductBL class is a dependency of IProductDAL. Therefore, container. Resolve < iproductbl > ()   Returns an object of the ProductBL class by automatically creating and injecting a ProductDAL object into it. All this is going on behind the scenes. The ProductDAL object was created and injected because we registered the ProductDAL type with IProductDAL.

This is a very simple and basic IoC container. It shows you the content behind the IoC container. this is it. I hope you enjoy reading this article.

 

Tags: .NET

Posted on Mon, 08 Nov 2021 06:57:46 -0500 by ganeshasri