Abstract Factory Pattern in C#: Streamlining Salary Processing in HR

Daniel Azevedo - Sep 9 - - Dev Community

Hey devs!

Today, I want to talk about the Abstract Factory Pattern, one of the most flexible creational design patterns. To keep things relatable, we’ll explore how this pattern can be applied in a Human Resources (HR) system, specifically in handling salary processing across different employee types (e.g., full-time, part-time, and contractors).

What is the Abstract Factory Pattern?

The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. It’s a great fit when you need to create objects that belong to different categories or "families" but want to keep the code flexible and easy to extend.

In simple terms, it’s like having a factory that produces other factories! It gives you the flexibility to switch between different product families without changing much in your code.

Why Use the Abstract Factory Pattern?

Here’s why you might want to consider the Abstract Factory Pattern:

  1. Consistency Across Related Objects: When you have multiple related objects that need to work together, Abstract Factory ensures consistency.
  2. Extensibility: It’s easy to add new families of objects without touching existing code.
  3. Loose Coupling: It helps decouple your code from specific implementations, which is key for maintainability.

Salary Processing Scenario

Let’s say you’re building an HR system to process salaries for different types of employees: full-time employees, part-time employees, and contractors. Each type has its own rules for calculating salaries, taxes, and benefits.

Here’s how we can use the Abstract Factory Pattern to cleanly handle these differences without cluttering our code with if-else statements all over the place.

Implementing Abstract Factory Pattern in C

Step 1: Define Abstract Product Interfaces

First, we need abstract interfaces for the different processes related to salary: ISalaryProcessor, ITaxProcessor, and IBenefitsProcessor. Each of these will have specific implementations for full-time, part-time, and contract employees.

// Salary Processor Interface
public interface ISalaryProcessor
{
    decimal CalculateSalary();
}

// Tax Processor Interface
public interface ITaxProcessor
{
    decimal CalculateTax(decimal salary);
}

// Benefits Processor Interface
public interface IBenefitsProcessor
{
    decimal CalculateBenefits();
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Concrete Product Implementations

Now, let’s implement these interfaces for full-time, part-time, and contractor employees.

// Full-time Implementations
public class FullTimeSalaryProcessor : ISalaryProcessor
{
    public decimal CalculateSalary() => 5000;
}

public class FullTimeTaxProcessor : ITaxProcessor
{
    public decimal CalculateTax(decimal salary) => salary * 0.2m; // 20% tax
}

public class FullTimeBenefitsProcessor : IBenefitsProcessor
{
    public decimal CalculateBenefits() => 1000; // Flat benefits
}

// Part-time Implementations
public class PartTimeSalaryProcessor : ISalaryProcessor
{
    public decimal CalculateSalary() => 2000;
}

public class PartTimeTaxProcessor : ITaxProcessor
{
    public decimal CalculateTax(decimal salary) => salary * 0.1m; // 10% tax
}

public class PartTimeBenefitsProcessor : IBenefitsProcessor
{
    public decimal CalculateBenefits() => 500; // Reduced benefits
}

// Contractor Implementations
public class ContractorSalaryProcessor : ISalaryProcessor
{
    public decimal CalculateSalary() => 3000;
}

public class ContractorTaxProcessor : ITaxProcessor
{
    public decimal CalculateTax(decimal salary) => salary * 0.15m; // 15% tax
}

public class ContractorBenefitsProcessor : IBenefitsProcessor
{
    public decimal CalculateBenefits() => 0; // No benefits for contractors
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the Abstract Factory Interface

Next, we define the abstract factory interface. This will have methods for creating each type of processor (salary, tax, benefits).

public interface IEmployeeProcessingFactory
{
    ISalaryProcessor CreateSalaryProcessor();
    ITaxProcessor CreateTaxProcessor();
    IBenefitsProcessor CreateBenefitsProcessor();
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Implement Concrete Factories

Now, we create concrete factories for full-time, part-time, and contractor employees. Each factory will return the appropriate processor implementations for the respective employee type.

// Full-time Employee Factory
public class FullTimeEmployeeFactory : IEmployeeProcessingFactory
{
    public ISalaryProcessor CreateSalaryProcessor() => new FullTimeSalaryProcessor();
    public ITaxProcessor CreateTaxProcessor() => new FullTimeTaxProcessor();
    public IBenefitsProcessor CreateBenefitsProcessor() => new FullTimeBenefitsProcessor();
}

// Part-time Employee Factory
public class PartTimeEmployeeFactory : IEmployeeProcessingFactory
{
    public ISalaryProcessor CreateSalaryProcessor() => new PartTimeSalaryProcessor();
    public ITaxProcessor CreateTaxProcessor() => new PartTimeTaxProcessor();
    public IBenefitsProcessor CreateBenefitsProcessor() => new PartTimeBenefitsProcessor();
}

// Contractor Factory
public class ContractorFactory : IEmployeeProcessingFactory
{
    public ISalaryProcessor CreateSalaryProcessor() => new ContractorSalaryProcessor();
    public ITaxProcessor CreateTaxProcessor() => new ContractorTaxProcessor();
    public IBenefitsProcessor CreateBenefitsProcessor() => new ContractorBenefitsProcessor();
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Use the Factory in a Client

Finally, in our salary processing system, we can easily switch between employee types by simply choosing the right factory. The client code doesn’t need to know the details of which specific classes are being used—it just interacts with the abstract interfaces.

public class PayrollService
{
    private readonly IEmployeeProcessingFactory _employeeFactory;

    public PayrollService(IEmployeeProcessingFactory factory)
    {
        _employeeFactory = factory;
    }

    public void ProcessSalary()
    {
        var salaryProcessor = _employeeFactory.CreateSalaryProcessor();
        var taxProcessor = _employeeFactory.CreateTaxProcessor();
        var benefitsProcessor = _employeeFactory.CreateBenefitsProcessor();

        decimal salary = salaryProcessor.CalculateSalary();
        decimal tax = taxProcessor.CalculateTax(salary);
        decimal benefits = benefitsProcessor.CalculateBenefits();

        Console.WriteLine($"Salary: {salary}");
        Console.WriteLine($"Tax: {tax}");
        Console.WriteLine($"Benefits: {benefits}");
        Console.WriteLine($"Net Pay: {salary - tax + benefits}");
    }
}

// Example Usage
public class Program
{
    public static void Main(string[] args)
    {
        PayrollService payroll;

        // Full-time employee processing
        payroll = new PayrollService(new FullTimeEmployeeFactory());
        payroll.ProcessSalary();

        // Part-time employee processing
        payroll = new PayrollService(new PartTimeEmployeeFactory());
        payroll.ProcessSalary();

        // Contractor employee processing
        payroll = new PayrollService(new ContractorFactory());
        payroll.ProcessSalary();
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Approach is Better

  • Flexibility: If we need to add a new employee type (e.g., freelancers), we just create a new factory and corresponding implementations. No need to modify the existing code.
  • Consistency: Each employee type has a consistent way of processing salary, tax, and benefits, ensuring that all related objects work well together.
  • Maintainability: By decoupling the client code from the specifics of object creation, the Abstract Factory Pattern makes the codebase much easier to maintain and extend.

When to Use the Abstract Factory Pattern

The Abstract Factory Pattern is especially useful when:

  • You have multiple families of related objects that should work together (like salary, tax, and benefits processors).
  • You need flexibility to easily switch between different families of objects without touching the client code.
  • You want to ensure that related objects are created consistently and in a unified way.

Wrapping Up

The Abstract Factory Pattern is a powerful tool when you need to manage the creation of related objects in a flexible and maintainable way. In our salary processing example, it helped streamline the logic for handling different types of employees, ensuring consistency across the system and making it easy to extend in the future.

Have you used the Abstract Factory Pattern in your own projects? Let me know how it worked out for you in the comments!

Happy coding!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player