[PoEAA] Domain Logic Pattern - Service Layer

本篇同步发布於个人Blog: [PoEAA] Domain Logic Pattern - Service Layer

1. What is Service Layer

According to [PoEAA], this definition is "Defines an application's boundary with a layer of services that establishes a set of available operations and coordinates the application's response in each operation."

Figure 1. Service Layer Architecture (From PoEAA Page)

1.1 How it works

Business Logic is generally split into "Domain Logic" and "Application Logic". Domain Logic focuses on the domain problem like the calculation of contract's revenue recognition. Application Logic is responsible for integrating "workflow". The workflow example: When the calculation of contract's revenue recognition is finished, system sends the result email to contract's owner and prints contract paper. Every service layer packs an kind of application logic and makes the domain objects reusable.

Implementation:

  1. Domain Facade: This facade have no business logic and hides the a domain object's implementation.
  2. Operation Script: This implements the above Application Logic and some services are composed as a Operation Script for clients.

Service Layer's operations are based on the use case model and user interface design. Most use cases are "CRUD" with database for every domain model. In some applications have to interact with other application, so the service layer organizes the integration.

By application's scale, if the application is large, then split it vertically into subsystems and every subsystem has a service name.

The other way is based on Domain Model to build the services (like ContractService/ProductService) or bases on the behavior to build the services (like RecognitionService)

1.2 When to use it

If the application has only one resource transaction when client interact with every operation, Service Layer is not required and sending request to Data Source is straight.

Otherwise, all clients interact this system with Service Layer's public operations.

2. Pattern Practice: The Revenue Recognition Problem

This problem is introduced in the previous article [PoEAA] Domain Logic Pattern - Transaction Script. Original Transaction Script example has a service class. This article extends this service.

Define the service layer classes for the revenue recognition, product and contract as the following figure:

Figure 2. Class diagram of Revenue Recognition Service

2.1 Implementation by C#

This pattern is implemented by C# based on the content of Chapter 9 Domain Logic Pattern - Service Layer of PoEAA. 

2.1.1 New Features

When use RecognitionService's CalculateRevenueRecognitions function, the calculated revenue result will be sent to contract's administrator owner by email and broadcast to integrate other system. These 2 features are mocking implementation, not real sending/broadcasting systems.

2.1.2 IEmailGateway/IIntegrationGateway interfaces/implementation

The above new features are implemented in IEmailGateway and IIntegrationGateway as Figure 2 shows. Their implementation just prints parameter content by console.

    internal class IntegrationGateway : IIntegrationGateway
    {
    	public void PublishRevenueRecognitionCalculation(DataRow contract)
    	{
    		Console.WriteLine($"Id : {contract["Id"]} , Signed Date: {((DateTime)contract["DateSigned"]):MM/dd/yyyy}");
    	}
    }
    
    public interface IIntegrationGateway
    {
    	void PublishRevenueRecognitionCalculation(DataRow contract);
    }
    
    internal class EmailGateway : IEmailGateway
    {
    	public void SendEmailMessage(string toAddress, string subject, string body)
    	{
    		Console.WriteLine($"To Address : {toAddress} , subject: {subject}, body: {body}");
    	}
    }
    
    public interface IEmailGateway
    {
    	void SendEmailMessage(string toAddress, string subject, string body);
    }

2.1.3 ApplicationService base class

All service layer's classes have reusable and common functions, so this ApplicationService provides those. In this example, ApplicationService only generates the IEmailGateway and IIntegrationGateway's instances.

    public class ApplicationService
    {
    	protected virtual IEmailGateway GetEmailGateway()
    	{
    		return new EmailGateway();
    	}
    
    	protected virtual IIntegrationGateway GetIntegrationGateway()
    	{
    		return new IntegrationGateway();
    	}
    }

2.1.4 RecognitionService Class 

This class extends ApplicationService to get IEmailGateway and IIntegrationGateway's instances. When the revenue recognitions are calculated, call these services to complete the new features.

    public class RecognitionService : ApplicationService
    {
    	public Money RecognizedRevenue(int contractNumber, DateTime beforeDate)
    	{
    		Money result = Money.Dollars(0m);
    		Gateway db = new Gateway();
    		var dt = db.FindRecognitionsFor(contractNumber, beforeDate);
    		for (int i = 0; i < dt.Rows.Count; ++i)
    		{
    			var amount = (decimal) dt.Rows[i]["Amount"];
    			result += Money.Dollars(amount);
    		}
    
    		return result;
    	}
    
    	public void CalculateRevenueRecognitions(int contractId)
    	{
    		Gateway db = new Gateway();
    		var contracts = db.FindContract(contractId);
    		Money totalRevenue = Money.Dollars((decimal) contracts.Rows[0]["Revenue"]);
    		DateTime recognitionDate = (DateTime) contracts.Rows[0]["DateSigned"];
    		string type = contracts.Rows[0]["Type"].ToString();
    
    		if(type == "S")
    		{
    			Money[] allocation = totalRevenue.Allocate(3);
    			db.InsertRecognitions(contractId, allocation[0], recognitionDate);
    			db.InsertRecognitions(contractId, allocation[1], recognitionDate.AddDays(60));
    			db.InsertRecognitions(contractId, allocation[2], recognitionDate.AddDays(90));
    		}
    		else if(type == "W")
    		{
    			db.InsertRecognitions(contractId, totalRevenue, recognitionDate);
    		}
    		else if(type == "D")
    		{
    			Money[] allocation = totalRevenue.Allocate(3);
    			db.InsertRecognitions(contractId, allocation[0], recognitionDate);
    			db.InsertRecognitions(contractId, allocation[1], recognitionDate.AddDays(30));
    			db.InsertRecognitions(contractId, allocation[2], recognitionDate.AddDays(60));
    		}
    
    		GetEmailGateway().SendEmailMessage(
    			contracts.Rows[0]["AdministratorEmail"].ToString(), 
    			"RE: Contract #" + contractId, 
    			contractId + " has had revenue recognitions calculated");
    
    		GetIntegrationGateway().PublishRevenueRecognitionCalculation(contracts.Rows[0]);
    	}
    }

2.1.5 Demo

Create a console program and create 3 Products and 3 Contracts to calculate the revenue recognitions for the 3 products.

As the following code:

    using (var connection = DbManager.CreateConnection())
    {
    	connection.Open();
    
    	var command = connection.CreateCommand();
    	command.CommandText =
    	@"
    		DROP TABLE IF EXISTS Products;
    		DROP TABLE IF EXISTS Contracts; 
    		DROP TABLE IF EXISTS RevenueRecognitions;
    	";
    	command.ExecuteNonQuery();
    
    
    	command.CommandText =
    	@"
    		CREATE TABLE Products (Id int primary key, Name TEXT, Type TEXT);
    		CREATE TABLE Contracts (Id int primary key, Product int, Revenue decimal, DateSigned date, AdministratorEmail TEXT);
    		CREATE TABLE RevenueRecognitions (Contract int, Amount decimal, RecognizedOn date, PRIMARY KEY(Contract, RecognizedOn));
    	";
    	command.ExecuteNonQuery();
    
    	command.CommandText =
    	@"
    	   
    	INSERT INTO Products
    		VALUES (1, 'Code Paradise Database', 'D');
    
    	INSERT INTO Products
    		VALUES (2, 'Code Paradise Spreadsheet', 'S');
    
    	INSERT INTO Products
    		VALUES (3, 'Code Paradise Word Processor', 'W');
    
    	INSERT INTO Contracts
    		VALUES (1, 1, 9999, date('2020-01-01'), '[email protected]');
    
    	INSERT INTO Contracts
    		VALUES (2, 2, 1000, date('2020-03-15'), '[email protected]');
    
    	INSERT INTO Contracts
    		VALUES (3, 3, 24000, date('2020-07-25'), '[email protected]');
    	";
    	command.ExecuteNonQuery();
    }
    
    RecognitionService service = new RecognitionService();
    
    // database product
    service.CalculateRevenueRecognitions(1);
    var databaseRevenue = service.RecognizedRevenue(1, new System.DateTime(2020, 1, 25));
    Console.WriteLine($"database revenue before 2020-01-25 = {databaseRevenue.Amount}");
    
    // spreadsheet product
    service.CalculateRevenueRecognitions(2);
    var spreadsheetRevenue = service.RecognizedRevenue(2, new System.DateTime(2020, 6, 1));
    Console.WriteLine($"spreadsheet revenue before 2020-06-01 = {spreadsheetRevenue.Amount}");
    
    // word processor product
    service.CalculateRevenueRecognitions(3);
    var wordProcessorRevenue = service.RecognizedRevenue(3, new System.DateTime(2020, 9, 30));
    Console.WriteLine($"word processor revenue before 2020-09-30 = {wordProcessorRevenue.Amount}");

The console shows:

This revenue recognition result is the same as the Transaction Script example and additionally shows the email sending messages/broadcasting messages.

3. Conclusions

"Service Layer" is a very popular domain logic's pattern. All domain logic underlying implementation is coordinated by service layer and clients only interact the domain logic by service layer. In many open sources, services classes/patterns/architectures are ubiquitous. Each service (subsystem) plays a role that cooperates with the other to finish a requirement.

The above sample code is uploaded to this Github Repository.

For next article I will write Table Data Gateway pattern according to Chapter 10 Data Source Architectural Pattern - Table Data Gateway of PoEAA.

4. References

Patterns of Enterprise Application Architecture Book(Amazon)


<<:  进击的软件工程师之路-软件战斗营 第十六周

>>:  如何拥有一个好的网页设计

[day-21] Python-决策的开始,认识 if 判断式

甚麽是判断式?   简单说,判断式就是为程序设定一个条件,当符合条件的时候做出适当的选择。 这边我们...

DAY13 Kotlin基础 Class

大学期间上系统分析时,教授在台上说: 「今天的内容呢,是 Class 的 Class 。」 ????...

学习Python纪录Day24 - 建立Excel及编辑试算表

新增工作表 使用create_sheet()方法新增工作表,title参数为工作表名称 wb = l...

完赛日,心得与阶段学习验收

今年度的铁人赛於今天即将完赛,终於要告一段落啦!!! 回顾这三十天的的学习,给自己打了个差强人意的分...

Day25 - 补充 Container 和 Hashing

大家好,我是长风青云。今天是铁人赛的第二十五天。 突然发现我有些东西没说,而Hash这个我也忘记说,...