Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Designing Business Logic Layer: Some Guidelines

4.75/5 (4 votes)
4 Oct 2010CPOL3 min read 20.6K  
Some guidelines for designing business logic layer

Business Logic Layer is a very crucial layer for any database applications. A timely thought when applied to this layer from the beginning of application layers design can save lots of time and complexity. Software architects divide the software into modules, then different layers, and core functioning layers for important application features. But when actual development work starts, complexity of different layers and modules start crawling into code gradually. Reason being:

  • We try to mix business rules of different modules wishfully
  • Writing methods with abundant codes
  • Not clearly separating responsibilities of presentation and data access layers
  • Creating code duplicity, i.e.; writing same set of code or methods at various places

Results are:

  • Difficult to debug
  • Difficult to understand the flow
  • Difficult to maintain and modify business rules correctly when such rules exist across layers and modules
  • Difficult to write Unit Tests

We can avoid these things if we take care of these things when writing codes.

  • Write methods that do single meaningful task with one call. Do not mix other code logic with the methods. For example, if we write SavePayment() method, then this method should only focus on save task, and not update or delete or check connection status or read XML files, etc. This is what we call Single Responsibility Principle.
  • Encourage use of factory methods for object creation instead of writing lots of If-else constructs based upon some input type values.
  • When you need data or result sets (DTO) of other modules, then preferably call business logic layer methods of that module instead of writing that module code logic into yours. This is quite important aspect for any business logic layer.
  • Classes in this layer should be loosely coupled. For this, different injection patterns like dependency injection or inversion of control, etc. can help. Sometimes, even a simple Enum type can come to a great rescue.
  • Write business methods that accept valid entity class object or DTO object in business rather than single valued parameters like integer or string or array or even optional params. This ensures business logic layer code function even unmodified when there are database table and entity or DTO class changes in behind.
  • Avoid lots of business rules in stored procedures or even in presentation UI.
  • Business logic layer methods should not be aware of presentation UI controls’ properties or values. These methods should accept values in integer or string instead.

Let me explain all these points by one example. I worked in an accounting module of a project where customers can make payments of their bills in various ways. They can make either full payment or in-partial or even in installments. For each payment mode, there were different rules and validations. So this module had clear separation of implementation with rules of each mode functioning without depending upon others. This way, lots of our coding and debugging time got saved.

Let’s see the code snippets.

Enum showing different Payment mode:

C#
public enum PaymentMode 
{ 
Normal, 
Part, 
Installment 
}

Custom BillDTO:

C#
public class CustomerBillDTO 
{ 
private Int64 intBillNo; 
private 
Int16 intBillMonth; 
private Int16 intBillYear; 
private double 
dblBillAmount; 
private string strCustomerID; 
private Int64 intPayAmount; 

//And many other fields... 
}

Payment Processor factory class and its implementation:

C#
interface IPaymentProcessorFactory
{
//
IPaymentProcessor GetPaymentProcessor(PaymentMode mode);
}
public class PaymentProcessorFactory : IPaymentProcessorFactory
{
//
private IPaymentProcessor objPaymentProcessor = null;

public IPaymentProcessor GetPaymentProcessor(PaymentMode mode)
{
//
switch (mode)
{
case PaymentMode.Normal:
objPaymentProcessor = new NormalPaymentProcessor();
break;

case PaymentMode.Part:
objPaymentProcessor = new PartPaymentProcessor();
break;
case PaymentMode.Installment:
objPaymentProcessor = new InstallmentPaymentProcessor();
break;
}
return objPaymentProcessor;
}
}

Different Payment Processor classpublic interface IPaymentProcessor
{
//
bool SavePayment(CustomerBillDTO Bill);
}
Main class that processes each paymentclass PaymentProcess
{
private IPaymentProcessorFactory objProcessor = null;
public PaymentProcess(IPaymentProcessorFactory Processor)
{
//
this.objProcessor = Processor;
}
At the calling end, we simply make a generous call as:
private void BtnSave_Click(object sender, EventArgs e)
{
//
PaymentProcessorFactory objFactory = new PaymentProcessorFactory();
PaymentProcess objProcess = new PaymentProcess(objFactory);
bool result = objProcess.ProcessPayment(objCustomerBillDTO(), PaymentMode.Normal);
}

As we see, this is how we have clearly separated each logical functioning of a SavePayment() method. Even in future, if Part or Installment payment mode is stopped, we do not have to modify the code logic to add any If-else construct to branch out or skip any code flows. In case a new payment mode is added, then writing a new XModePaymenetProcessor class, adding one more Enum value and finally one more object instantiation code in factory class will do enough.

Adding or removing any Bill or Customer related fields in CustomerBillDTO do not even pose threat to this business logic layer.

Finally, one should always keep in mind that you write class and class methods for others. So you should be very clear here: what the class should offer and how.

Thanks for reading!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)