Click here to Skip to main content
15,879,535 members
Articles / Programming Languages / C#

A Sample Code Submitted for Senior C# Developer Position + Unit Tests

Rate me:
Please Sign up or sign in to vote.
4.62/5 (25 votes)
11 Mar 2016CPOL4 min read 43.4K   775   34   21
A real code assessment for a senior C#.NET role

Image 1

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.

C#
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.

C#
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.

C#
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; // To detect redundant calls

    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.

C#
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; // 10% price reduction 2 times more than 5%
            }
            else
            {
                cheese.Price *= 1.05; // 5% price raise
            }
            cheese.Price= Math.Round(cheese.Price, 2, MidpointRounding.ToEven);// rounding
        });

        _rules.Add(CheeseTypes.Unique, (ICheese cheese, DateTime now) => {  }); // No action

        _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; // 10% price reduction 2 times more than 5%
            }
            else
            {
                cheese.Price *= 0.8;    // 20% price reduction as it has passed
                    // the BestBeforeDate 2 times more than 10%
            }
            cheese.Price = Math.Round(cheese.Price, 2,MidpointRounding.ToEven);// rounding
        });

        _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; // 10% price reduction 2 times more than 5%
                cheese.Price = Math.Round(cheese.Price, 2, MidpointRounding.ToEven);// rounding
                return;
            }

            if (cheese.DaysToSell <= 10 && cheese.DaysToSell > 5)
            {
                cheese.Price *= 1.05; // 5% price raise
            }
            if (cheese.DaysToSell <= 5 && cheese.DaysToSell > 0)
            {
                cheese.Price *= 1.1; // 10% price raise
            }
            cheese.Price = Math.Round(cheese.Price, 2, MidpointRounding.ToEven);// rounding
        });

        _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; // 5% price reduction
            }
            else
            {
                cheese.Price *= 0.9; // 10% price reduction as it has passed the BestBeforeDate
            }
            cheese.Price = Math.Round(cheese.Price, 2, MidpointRounding.ToEven); // rounding
        });
    }

    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.

C#
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.

C#
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.

C#
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.

C#
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.

License

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


Written By
Software Developer (Senior)
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
PraiseInformative explanation Pin
Well Done House7-Nov-17 23:16
Well Done House7-Nov-17 23:16 
Generalupdated atricle Pin
Masoud Zehtabi Oskuie25-Mar-16 9:10
professionalMasoud Zehtabi Oskuie25-Mar-16 9:10 
QuestionCode smells. Pin
PauloJuanShirt23-Mar-16 2:10
PauloJuanShirt23-Mar-16 2:10 
QuestionStrange sentence Pin
Alessandro Cavalieri14-Mar-16 11:43
Alessandro Cavalieri14-Mar-16 11:43 
QuestionCyclomatic Complexity Pin
Ed Hill14-Mar-16 6:23
Ed Hill14-Mar-16 6:23 
AnswerRe: Cyclomatic Complexity Pin
Masoud Zehtabi Oskuie25-Mar-16 19:25
professionalMasoud Zehtabi Oskuie25-Mar-16 19:25 
QuestionSomething about an expandability Pin
Member 1048548712-Mar-16 2:29
Member 1048548712-Mar-16 2:29 
Answer- Pin
Fabrice CARUSO11-Mar-16 23:09
Fabrice CARUSO11-Mar-16 23:09 
GeneralMessage Closed Pin
12-Mar-16 7:25
professionalMasoud Zehtabi Oskuie12-Mar-16 7:25 
AnswerMessage Closed Pin
12-Mar-16 9:37
Fabrice CARUSO12-Mar-16 9:37 
GeneralRe: I'm sorry for what i'm going to tell... Pin
Member 1048548712-Mar-16 10:47
Member 1048548712-Mar-16 10:47 
QuestionThanks for the post! Pin
Teddy Olson11-Mar-16 18:50
Teddy Olson11-Mar-16 18:50 
AnswerRe: Thanks for the post! Pin
Masoud Zehtabi Oskuie12-Mar-16 7:27
professionalMasoud Zehtabi Oskuie12-Mar-16 7:27 
Thank you very much Teddy. I will regularly upload more project for you guys and I would be happy to answer all. I will follow you posts.
GeneralStick it in your blog Pin
PIEBALDconsult11-Mar-16 17:07
mvePIEBALDconsult11-Mar-16 17:07 
GeneralRe: Stick it in your blog Pin
Masoud Zehtabi Oskuie11-Mar-16 18:10
professionalMasoud Zehtabi Oskuie11-Mar-16 18:10 
GeneralRe: Stick it in your blog Pin
PIEBALDconsult11-Mar-16 18:51
mvePIEBALDconsult11-Mar-16 18:51 
Suggestioncode formatting issue Pin
Sascha Lefèvre11-Mar-16 0:57
professionalSascha Lefèvre11-Mar-16 0:57 
Questionproblem statement missing Pin
peterkmx10-Mar-16 22:28
professionalpeterkmx10-Mar-16 22:28 
AnswerRe: problem statement missing Pin
Masoud Zehtabi Oskuie11-Mar-16 13:47
professionalMasoud Zehtabi Oskuie11-Mar-16 13:47 
GeneralRe: problem statement missing Pin
peterkmx21-Mar-16 3:49
professionalpeterkmx21-Mar-16 3:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.