iT邦幫忙

0

[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'), 'test1@test.com');
    
    	INSERT INTO Contracts
    		VALUES (2, 2, 1000, date('2020-03-15'), 'test2@test.com');
    
    	INSERT INTO Contracts
    		VALUES (3, 3, 24000, date('2020-07-25'), 'test3@test.com');
    	";
    	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)


尚未有邦友留言

立即登入留言