I develop this mini project as an exercise. It is a simplified employee salary calculator.
Requirements:
- Employees are characterized by name, employment date, employee group and base salary.
- There are three groups of employees: Employee, Manager, Salesman.
- Each employee might have a supervisor.
- Each employee except "Employee" group might have subordinates of any group.
Salary should be calculated the following way:
For "Employee" group - base salary + 3% for each year of work since employment date, but no more than 30% over.
For "Manager" group - base salary + 5% for each year of work, but no more than 40% over + 0,5% of salary of all direct(first level) subordinates.
For "Salesman" group - base salary + 1% for each year of work, but no more than 35% over + 0,3% of salary of ALL subordinates, direct or indirect.
The app should allow a user to get calculated salary of any selected employee at any given moment as well as summary of ALL company employees' salaries.
My solution:
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime EmploymentDate { get; set; }
public string EmployeeGroup { get; set; }
public int BaseSalary { get; set; }
public int? SupervisorId { get; set; }
public string Lineage { get; set; }
public int Depth { get; set; }
public Employee Supervisor { get; set; }
public List<Employee> Subordinates { get; set; }
}
public class EmployeesDbContext : DbContext
{
public DbSet<Employee> Employees { get; set; }
public EmployeesDbContext(DbContextOptions<EmployeesDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.HasOne(e => e.Supervisor)
.WithMany(e => e.Subordinates)
.HasForeignKey(e => e.SupervisorId);
}
}
public interface IEmployeeRepository
{
void Add(Employee employee);
void AddRange(IEnumerable<Employee> employees);
void Delete(Employee employee);
Task<List<Employee>> ListAllAsync();
Task<Employee> FindByIdAsync(int id);
Task<List<Employee>> GetAllSubordinatesAsync(Employee employee);
Task<List<Employee>> GetFirstLevelSubordinatesAsync(Employee employee);
IQueryable<Employee> GetAsQueryable();
Task<int> GetEmployeeTreeDepth();
Task<List<Employee>> GetAllOfLevel(int depth);
Task SaveChangesAsync();
}
public class EmployeeRepository : IEmployeeRepository
{
private readonly EmployeesDbContext _employeesDbContext;
public EmployeeRepository(EmployeesDbContext dbContext)
{
_employeesDbContext = dbContext;
}
public void Add(Employee employee)
{
_employeesDbContext.Add(employee);
}
public void Delete(Employee employee)
{
_employeesDbContext.Remove(employee);
}
public void AddRange(IEnumerable<Employee> employees)
{
_employeesDbContext.AddRange(employees);
}
public async Task<List<Employee>> GetAllSubordinatesAsync(Employee employee)
{
var result = await _employeesDbContext.Employees
.FromSql("SELECT * FROM Employees WHERE lineage LIKE '{0}-%'", employee.Lineage)
.ToListAsync();
return result;
}
public async Task<Employee> FindByIdAsync(int id)
{
return await _employeesDbContext.FindAsync<Employee>(id);
}
public async Task<List<Employee>> GetFirstLevelSubordinatesAsync(Employee employee)
{
var result = await _employeesDbContext.Employees
.FromSql("SELECT * FROM Employees WHERE lineage LIKE '{0}-%' AND depth = {1}", employee.Lineage, employee.Depth + 1)
.ToListAsync();
return result;
}
public async Task<List<Employee>> ListAllAsync()
{
return await _employeesDbContext.Employees.AsNoTracking().ToListAsync();
}
public IQueryable<Employee> GetAsQueryable()
{
return _employeesDbContext.Employees.AsQueryable().AsNoTracking();
}
public async Task<int> GetEmployeeTreeDepth()
{
return await _employeesDbContext.Employees.MaxAsync(e => e.Depth);
}
public async Task<List<Employee>> GetAllOfLevel(int depth)
{
return await _employeesDbContext.Employees.Where(e => e.Depth == depth).ToListAsync();
}
public async Task SaveChangesAsync()
{
await _employeesDbContext.SaveChangesAsync();
}
}
public interface ISalaryCalculator
{
Task<int> CalculateSalaryAsync(Employee employee);
Task<int> CalculateSalariesSumAsync();
}
public class SalaryCalculator : ISalaryCalculator
{
private readonly ISalaryCalculatorFactory _salaryCalculatorFactory;
private readonly IEmployeeRepository _employeeRepository;
public SalaryCalculator(ISalaryCalculatorFactory salaryCalculatorFactory, IEmployeeRepository employeeRepository)
{
_salaryCalculatorFactory = salaryCalculatorFactory;
_employeeRepository = employeeRepository;
}
public async Task<int> CalculateSalariesSumAsync()
{
var employees = await _employeeRepository.ListAllAsync();
IEnumerable<Task<int>> calcSalaryTasksQuery = employees.Select(e => CalculateSalaryAsync(e));
Task<int>[] calcSalaryTasks = calcSalaryTasksQuery.ToArray();
int[] salaries = await Task.WhenAll(calcSalaryTasks);
var salarySum = salaries.Sum();
return salarySum;
}
public async Task<int> CalculateSalaryAsync(Employee employee)
{
var calculator = _salaryCalculatorFactory.CreateCalculator(employee.EmployeeGroup);
var salary = await calculator.CalculateSalaryAsync(employee);
return salary;
}
}
public interface ISalaryCalculatorFactory
{
BaseSalaryCalculator CreateCalculator(string employeeGroup);
}
public class SalaryCalculatorFactory : ISalaryCalculatorFactory
{
private Dictionary<string, Func<BaseSalaryCalculator>> salaryCalculators;
public SalaryCalculatorFactory(IEmployeeRepository employeeRepository)
{
salaryCalculators = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => typeof(BaseSalaryCalculator).IsAssignableFrom(t) && t.IsAbstract == false)
.Select(t => new Func<BaseSalaryCalculator>(() => Activator.CreateInstance(t,employeeRepository, this) as BaseSalaryCalculator))
.ToDictionary(f => f().EmployeeGroup);
}
public BaseSalaryCalculator CreateCalculator(string employeeGroup)
{
return salaryCalculators[employeeGroup]();
}
}
public abstract class BaseSalaryCalculator
{
public abstract string EmployeeGroup { get; }
protected readonly IEmployeeRepository _employeeRepository;
protected readonly ISalaryCalculatorFactory _salaryCalculatorFactory;
protected BaseSalaryCalculator(IEmployeeRepository employeeRepository, ISalaryCalculatorFactory salaryCalculatorFactory)
{
_employeeRepository = employeeRepository;
_salaryCalculatorFactory = salaryCalculatorFactory;
}
public abstract Task<int> CalculateSalaryAsync(Employee employee);
protected int CalcEmployeeExperience(DateTime employmentDate)
{
return (int)Math.Floor(DateTime.Now.Subtract(employmentDate).TotalDays / 365);
}
protected int CalcExperiencePremium(int baseSalary, int premiumPercentForEachYearExp, int maxExperiencePremiumPercent, int experience)
{
var experiencePremium = baseSalary / 100 * premiumPercentForEachYearExp;
var maxExpPremium = baseSalary / 100 * maxExperiencePremiumPercent;
var premium = experiencePremium * experience;
if (premium > maxExpPremium) premium = maxExpPremium;
return premium;
}
protected async Task<int> CalcSupervisorPremium(IEnumerable<Employee> subordinates, float supervisorPremiumPercent)
{
int salarySum = 0;
foreach (var employee in subordinates)
{
var calculator = _salaryCalculatorFactory.CreateCalculator(employee.EmployeeGroup);
var salary = await calculator.CalculateSalaryAsync(employee);
salarySum += salary;
}
var premium = (int)Math.Ceiling(salarySum / 100 * supervisorPremiumPercent);
return premium;
}
}
public class EmployeeSalaryCalculator : BaseSalaryCalculator
{
public override string EmployeeGroup => "Employee";
private const int _premiumPercentForEachYearExp = 3;
private const int _maxExperiencePremiumPercent = 30;
public EmployeeSalaryCalculator(IEmployeeRepository employeeRepository, ISalaryCalculatorFactory salaryCalculatorFactory)
: base(employeeRepository, salaryCalculatorFactory)
{
}
public override async Task<int> CalculateSalaryAsync(Employee employee)
{
var salary = await Task.Run(() =>
{
var experience = CalcEmployeeExperience(employee.EmploymentDate);
var experiencePremium = CalcExperiencePremium(employee.BaseSalary, _premiumPercentForEachYearExp, _maxExperiencePremiumPercent, experience);
var totalSalary = employee.BaseSalary + experiencePremium;
return totalSalary;
});
return salary;
}
}
public class ManagerSalaryCalculator : BaseSalaryCalculator
{
public override string EmployeeGroup => "Manager";
private const int _premiumPercentForEachYearExp = 5;
private const int _maxExperiencePremiumPercent = 40;
private const float _supervisorPremiumPercent = 0.5f;
public ManagerSalaryCalculator(IEmployeeRepository employeeRepository, ISalaryCalculatorFactory salaryCalculatorFactory)
: base(employeeRepository, salaryCalculatorFactory)
{
}
public override async Task<int> CalculateSalaryAsync(Employee employee)
{
var experience = CalcEmployeeExperience(employee.EmploymentDate);
var experiencePremium = CalcExperiencePremium(employee.BaseSalary, _premiumPercentForEachYearExp, _maxExperiencePremiumPercent, experience);
var subordinates = await _employeeRepository.GetFirstLevelSubordinatesAsync(employee);
var supervisorPremium = await CalcSupervisorPremium(subordinates, _supervisorPremiumPercent);
var totalSalary = employee.BaseSalary + experiencePremium + supervisorPremium;
return totalSalary;
}
}
public class SalesmanSalaryCalculator : BaseSalaryCalculator
{
public override string EmployeeGroup => "Salesman";
private const int _premiumPercentForEachYearExp = 1;
private const int _maxExperiencePremiumPercent = 35;
private const float _supervisorPremiumPercent = 0.3f;
public SalesmanSalaryCalculator(IEmployeeRepository employeeRepository, ISalaryCalculatorFactory salaryCalculatorFactory)
: base(employeeRepository, salaryCalculatorFactory)
{
}
public override async Task<int> CalculateSalaryAsync(Employee employee)
{
var experience = CalcEmployeeExperience(employee.EmploymentDate);
var experiencePremium = CalcExperiencePremium(employee.BaseSalary, _premiumPercentForEachYearExp, _maxExperiencePremiumPercent, experience);
var subordinates = await _employeeRepository.GetAllSubordinatesAsync(employee);
var supervisorPremium = await CalcSupervisorPremium(subordinates, _supervisorPremiumPercent);
var totalSalary = employee.BaseSalary + experiencePremium + supervisorPremium;
return totalSalary;
}
}
As you can see I ended up with a kind of circular dependency between SalaryCalculatorFactory and specific calculators it creates. And I don't know how to solve this elegantly and avoid duplicate code in CalcSupervisorPremium
method without creating the circular dependency and tight coupling (or it is okay and there's a good way to call SalaryCalculator's method, I don't know).
It seems this circularity is a natural part of the algorithm since to calculate some managers' and salesmans' salaries we need to calculate salaries of their subordinates.
So, how to improve the design?