Assumptions
Please consider the following:
- This is NOT a production code.
- This is NOT thread safe.
- Only some of the objects have unit test.
- This is a sample code to show how to decouple the concerns.
- You can go to github and clone this project or download it from Code Project.
Introduction
These days, most of the companies require their applicants to submit a code sample based on an imaginary problem as an assessment. The main goal of these code samples is to measure the skill of the applicants in code coherence, design pattern, Object Oriented Programming (OOP), SOLID Principles and so on.
Background
As an application developer who has been involved in development of several projects in Canada and Iran, I wanted to share with all of you one of my code samples which I submitted for senior C#.NET developer, to show how these kinds of assessments can be solved in an acceptable way. Of course, there are millions of ways to make an excellent software but I think this is one of the good ways.
Notice
Please refer to the problem statement file (zip file) to know the requirements and conditions applied to this project.
Architecture
N-Tier application architecture is one of the best practices which usually suggests to decouple the concents and in this case I decoupled logics, views and models. Let's see how...
Model
The problem statement clearly mentioned that the store manager wants to calculate the price of cheese based on the predefined logics. So, it's obvious that the model is cheese and must have the following properties and function.
As you see, the cheese has BestBeforeDate
, DaysToSell
, Name
, Price
, Type
. The BestBeforeDate
can be null
as the unique cheese do not have best before and DaysToSell
.
public class Cheese : ICheese
{
public DateTime? BestBeforeDate { get; set; }
public int? DaysToSell { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public CheeseTypes Type { get; set; }
public object Clone()
{
return MemberwiseClone();
}
public void CopyTo(ICheese cheese)
{
cheese.BestBeforeDate = BestBeforeDate;
cheese.DaysToSell = DaysToSell;
cheese.Name = Name;
cheese.Price = Price;
cheese.Type = Type;
}
public Tuple<bool, validationerrortype=""> Validate(ICheeseValidator cheeseValidator)
{
return cheeseValidator.Validate(this);
}
}
public interface ICheese : ICloneable
{
string Name { get; set; }
DateTime? BestBeforeDate { get; set; }
int? DaysToSell { get; set; }
double Price { get; set; }
CheeseTypes Type { get; set; }
Tuple<bool, validationerrortype=""> Validate(ICheeseValidator cheeseValidator);
void CopyTo(ICheese cheese);
}
public enum CheeseTypes
{
Fresh,
Unique,
Special,
Aged,
Standard
}
Validator
Any model must have a validation logic which checks the validity of the model after certain operations. In this case, the validatior is passed to the Validate
function of the cheese. The main reason for this injection is to decouple the validation logic from Model. In this way, the validation logic can be maintained and scaled without any changes on the model. The following is the code for validator.
As you can see, the validator has different type of Errors as provided by an Enum
.
public class CheeseValidator : ICheeseValidator
{
public Tuple<bool, validationerrortype> Validate(ICheese cheese)
{
return cheese.DaysToSell == 0
? Tuple.Create<bool, validationerrortype>
(false, ValidationErrorType.DaysToSellPassed)
: (cheese.Price < 0
? Tuple.Create<bool, validationerrortype>
(false, ValidationErrorType.ExceededMinimumPrice)
: (cheese.Price > 20
? Tuple.Create<bool, validationerrortype>
(false, ValidationErrorType.ExceededMaximumPrice)
: Tuple.Create<bool, validationerrortype>
(true, ValidationErrorType.None)));
}
}
public interface ICheeseValidator
{
Tuple<bool, validationerrortype> Validate(ICheese cheese);
}
public enum ValidationErrorType
{
None = 0,
ExceededMinimumPrice = 1,
ExceededMaximumPrice = 2,
DaysToSellPassed = 3
}
Business Logic
The business logic contains the logic which calculates the price of cheese and two manager classes which keep track of days and store managements.
Days Manager
Days manager is responsible for keeping track of day changes. it simply simulates the day changes by utilizing a time ticker. The following code shows its functionalities and properties. Event manager has its own event which is raised when the day changes and passes a custom event arguments. This event is very helpful to notify the StoreManger
that a new day has come.
public class DaysManager : IDaysManager, IDisposable
{
public event DaysManagerEventHandler OnNextDay;
private readonly Timer _internalTimer;
private int _dayCounter = 1;
public DateTime Now { get; private set; }
public DaysManager(int interval, DateTime now)
{
Now = now;
_internalTimer = new Timer(interval);
_internalTimer.Elapsed += _internalTimer_Elapsed;
_internalTimer.AutoReset = true;
_internalTimer.Enabled = true;
Stop();
}
public void Start()
{
_internalTimer.Start();
}
public void Stop()
{
_dayCounter = 1;
_internalTimer.Stop();
}
private void _internalTimer_Elapsed(object sender, ElapsedEventArgs e)
{
_dayCounter++;
Now = Now.AddDays(+1);
var eventArgs = new DaysManagerEventArgs(Now, _dayCounter);
OnNextDay?.Invoke(this, eventArgs);
}
#region IDisposable Support
private bool _disposedValue = false;
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_internalTimer.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion IDisposable Support
}
public interface IDaysManager
{
event DaysManagerEventHandler OnNextDay;
DateTime Now { get; }
void Start();
void Stop();
}
public delegate void DaysManagerEventHandler(object sender, DaysManagerEventArgs e);
public class DaysManagerEventArgs : EventArgs
{
public readonly DateTime Now;
public readonly int DayNumber;
public DaysManagerEventArgs(DateTime now, int dayNumber)
{
Now = now;
DayNumber = dayNumber;
}
}
Price Rule Container
PriceRuleContainer
is a class which encapsulates the logic used for Price Calculation. The main reason of this class is to decouple the price calculation logic for the rest of the code to increase the level of scalability and maintainability of the application.
public class PriceCalculationRulesContainer : IPriceCalculationRulesContainer
{
private Dictionary<cheesetypes, action="">> _rules;
public PriceCalculationRulesContainer()
{
_rules = new Dictionary<cheesetypes, action="" datetime="">>();
RegisterRules();
}
private void RegisterRules()
{
_rules.Add(CheeseTypes.Aged, (ICheese cheese, DateTime now) =>
{
if (cheese.DaysToSell==0)
{
cheese.Price = 0.00d;
return;
}
if (cheese.BestBeforeDate < now)
{
cheese.Price *= 0.9; }
else
{
cheese.Price *= 1.05; }
cheese.Price= Math.Round(cheese.Price, 2, MidpointRounding.ToEven); });
_rules.Add(CheeseTypes.Unique, (ICheese cheese, DateTime now) => { });
_rules.Add(CheeseTypes.Fresh, (ICheese cheese, DateTime now) =>
{
if (cheese.DaysToSell == 0)
{
cheese.Price = 0.00d;
return;
}
if (cheese.BestBeforeDate >= now)
{
cheese.Price *= 0.9; }
else
{
cheese.Price *= 0.8; }
cheese.Price = Math.Round(cheese.Price, 2,MidpointRounding.ToEven); });
_rules.Add(CheeseTypes.Special, (ICheese cheese, DateTime now) =>
{
if (cheese.DaysToSell == 0)
{
cheese.Price = 0.00d;
return;
}
if (cheese.BestBeforeDate < now)
{
cheese.Price *= 0.9; cheese.Price = Math.Round(cheese.Price, 2, MidpointRounding.ToEven); return;
}
if (cheese.DaysToSell <= 10 && cheese.DaysToSell > 5)
{
cheese.Price *= 1.05; }
if (cheese.DaysToSell <= 5 && cheese.DaysToSell > 0)
{
cheese.Price *= 1.1; }
cheese.Price = Math.Round(cheese.Price, 2, MidpointRounding.ToEven); });
_rules.Add(CheeseTypes.Standard, (ICheese cheese, DateTime now) =>
{
if (cheese.DaysToSell == 0)
{
cheese.Price = 0.00d;
return;
}
if (cheese.BestBeforeDate >= now)
{
cheese.Price *= 0.95; }
else
{
cheese.Price *= 0.9; }
cheese.Price = Math.Round(cheese.Price, 2, MidpointRounding.ToEven); });
}
public Action<icheese, datetime=""> GetRule(CheeseTypes cheeseType)
{
return _rules[cheeseType];
}
}
public interface IPriceCalculationRulesContainer
{
Action<icheese, datetime=""> GetRule(CheeseTypes cheeseType);
}
PriceResolversContainer
PriceResolversContainer
is a class which holds the strategies to resolve the validity issue of the mode. Basically, any time that the mode is not valid, it gives a logic to address the issue. The following is the actual implementation of this class.
public class PriceResolversContainer : IPriceResolversContainer
{
private Dictionary<validationerrortype, action="">> _rules;
public PriceResolversContainer()
{
_rules = new Dictionary<validationerrortype, action="">>();
RegisterRules();
}
public Action<icheese> GetRule(ValidationErrorType errorType)
{
return _rules[errorType];
}
private void RegisterRules()
{
_rules.Add(ValidationErrorType.ExceededMinimumPrice,
(ICheese cheese) => cheese.Price = 0.00);
_rules.Add(ValidationErrorType.ExceededMaximumPrice,
(ICheese cheese) => cheese.Price = 20.00);
_rules.Add(ValidationErrorType.None, (ICheese cheese) => { });
_rules.Add(ValidationErrorType.DaysToSellPassed, (ICheese cheese) => { });
}
}
public interface IPriceResolversContainer
{
Action<icheese> GetRule(ValidationErrorType errorType);
}
Store Manager
StoreManager
is responsible for calculating the price and sticking it to the cheese and also opening and closing the store. These features are not directly implemented in it but instead it utilizes the capabilities of the above mentioned class to do so. This class receives the above classes as its own dependencies through its constructor.(Dependency injection). Let's see how.
public class StoreManager : IStoreManager
{
public IList<icheese> Cheeses { get; set; }
private readonly IPriceCalculator _priceCalculator;
private readonly IPrinter _printer;
private readonly IDaysManager _daysManager;
private const int Duration = 7;
public StoreManager(IPriceCalculator priceCalculator,
IPrinter printer,
IDaysManager daysManager)
{
_priceCalculator = priceCalculator;
_printer = printer;
_daysManager = daysManager;
_daysManager.OnNextDay += DaysManager_OnNextDay;
}
private void DaysManager_OnNextDay(object sender, DaysManagerEventArgs e)
{
_printer.PrintLine($"Day Number: {e.DayNumber}");
CalculatePrices(e.Now);
if (e.DayNumber > Duration)
{
CloseStore();
}
}
public void CalculatePrices(DateTime now)
{
foreach (var cheese in Cheeses)
{
DecrementDaysToSell(cheese);
_priceCalculator.CalculatePrice(cheese,now);
}
_printer.Print(Cheeses, now);
}
public void OpenStore()
{
_printer.PrintLine
("Welcome to Store Manager ....The cheese have been loaded as listed below.");
_printer.PrintLine("Day Number: 1 ");
_printer.Print(Cheeses, _daysManager.Now);
_daysManager.Start();
}
public void CloseStore()
{
_daysManager.Stop();
_printer.PrintLine("The store is now closed....Thank you for your shopping.");
}
private void DecrementDaysToSell(ICheese cheese)
{
if (cheese.DaysToSell > 0)
cheese.DaysToSell--;
}
}
public interface IStoreManager
{
IList<icheese> Cheeses { get; set; }
void CalculatePrices(DateTime now);
void OpenStore();
void CloseStore();
}
View
Since this application does not have any special user interface, I used a class on github which helped me to draw a responsive table in this console application. I modified that class and added to my project. The original project can be found here. I have also defined a class called printer
to show the actual result.
public class Printer : IPrinter
{
private string[] _header;
public Printer()
{
}
public void Print(List<icheese> cheeses, DateTime now)
{
if (cheeses == null) throw new ArgumentNullException(nameof(cheeses));
if (cheeses.Count == 0) throw new ArgumentException
("Argument is empty collection", nameof(cheeses));
_header = new string[] { "RustyDragonInn",
"(Grocery Store)", "Today", now.ToShortDateString() };
PrintItems(cheeses);
}
public void PrintLine(string message)
{
if (message == null) throw new ArgumentNullException(nameof(message));
Console.WriteLine(message + Environment.NewLine);
}
private void PrintItems(IList<icheese> cheeseList)
{
if (cheeseList == null) throw new ArgumentNullException(nameof(cheeseList));
if (cheeseList.Count == 0) throw new ArgumentException
("Argument is empty collection", nameof(cheeseList));
Shell.From(cheeseList).AddHeader(_header).Write();
}
}
public interface IPrinter
{
void Print(IList<icheese> cheeses, DateTime now);
void PrintLine(string message);
}
How to Wire up Components
Now, it's time to wire up everything together to see the actual result on the screen. Let's see how. I did not use any container such as Unity but instead I directly injected the required components.
static void Main(string[] args)
{
var printer = new Printer.Printer();
if (args.Length==0)
{
printer.PrintLine("No input file path was specified.");
Console.Read();
return;
}
try
{
var filePath = args[0].Trim();
var reader = new Reader.Reader();
var cheeseList = reader.Load(filePath);
printer.PrintLine("");
printer.PrintLine(
"This application has been designed and implemented by
Masoud ZehtabiOskuie as an assessment for Senior C# Developer role");
var currentDate = Helper.GetDateTime('_', filePath, 1);
var cheeseValidator = new CheeseValidator();
var priceCalculationRulesContainer = new PriceCalculationRulesContainer();
var priceResolversContainer = new PriceResolversContainer();
var priceCalculator =
new PriceCalculator(cheeseValidator, priceCalculationRulesContainer,
priceResolversContainer);
var daysManager = new DaysManager(3000, currentDate);
var storeManager = new StoreManager(priceCalculator, printer, daysManager)
{Cheeses = cheeseList};
storeManager.OpenStore();
}
catch (FileNotFoundException)
{
printer.PrintLine("File Does not exists. Please make sure that the path is correct.");
}
catch (XmlSchemaException)
{
printer.PrintLine("The XML files is not well format.");
}
catch (DateTimeFormatException dex)
{
printer.PrintLine(dex.Message);
}
Console.Read();
}
Points of Interest
Object oriented programming helps the developer to split the application into several maintainable and scalable portions and combine them together to meet the requirements in an application from small to large scale.
I applied some of the design patterns that I assumed you got. Enjoy coding... thank you for reading my article. I hope that it would be helpful for you.