为高级 C# 开发人员职位提交的示例代码 + 单元测试






4.62/5 (24投票s)
资深 C#.NET 职位的实际代码评估
假设
请考虑以下几点
- 这不是生产代码。
- 这不是线程安全的。
- 只有部分对象具有单元测试。
- 这是一个示例代码,用于展示如何解耦关注点。
- 您可以前往 github 克隆此项目,或从 Code Project 下载。
引言
如今,大多数公司都要求应聘者提交基于假想问题的代码样本作为评估。这些代码样本的主要目的是衡量应聘者在代码一致性、设计模式、面向对象编程 (OOP)、SOLID 原则等方面的技能。
背景
作为一名曾在加拿大和伊朗参与过多个项目的应用程序开发者,我想与大家分享我提交的一个用于资深 C#.NET 开发职位的代码样本,以展示如何以可接受的方式解决此类评估。当然,有无数种方法可以构建出色的软件,但我认为这是一种不错的方法。
Notice
请参考问题陈述文件 (zip 文件),了解本项目的要求和条件。
架构
N 层应用程序架构是将关注点解耦的最佳实践之一,在本例中,我解耦了逻辑、视图和模型。让我们来看看如何实现...
模型
问题陈述清楚地说明了商店经理需要根据预定义逻辑计算奶酪的价格。因此,显而易见,模型是奶酪,必须具有以下属性和函数。
正如您所见,奶酪具有 BestBeforeDate
、DaysToSell
、Name
、Price
、Type
。BestBeforeDate
可以是 null
,因为独特的奶酪没有最佳食用日期和可售天数。
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
}
验证器
任何模型都必须具有验证逻辑,用于在执行某些操作后检查模型的有效性。在本例中,验证器被传递给奶酪的 Validate
函数。这样注入的主要原因是为了将验证逻辑与模型解耦。通过这种方式,可以在不更改模型的情况下维护和扩展验证逻辑。以下是验证器的代码。
正如您所见,验证器具有由 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
}
业务逻辑
业务逻辑包含计算奶酪价格的逻辑,以及两个用于跟踪天数和商店管理的管理器类。
天数管理器
天数管理器负责跟踪天数变化。它通过利用时间计时器简单地模拟天数变化。以下代码展示了它的功能和属性。事件管理器有自己的事件,在天数变化时触发,并传递自定义事件参数。此事件对于通知 StoreManger
新的一天已经到来非常有用。
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;
}
}
价格规则容器
PriceRuleContainer
是一个封装用于计算价格逻辑的类。该类的主要目的是将价格计算逻辑与代码的其余部分解耦,以提高应用程序的可扩展性和可维护性。
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
是一个保存用于解析模型有效性问题的策略的类。基本上,任何时候模型无效,它都会提供一个逻辑来解决该问题。以下是该类的实际实现。
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);
}
商店管理器
StoreManager
负责计算价格并将其附加到奶酪,以及打开和关闭商店。这些功能不是直接在该类中实现的,而是通过利用上述类的功能来实现的。该类通过其构造函数接收上述类作为其依赖项 (依赖注入)。让我们看看如何实现。
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();
}
视图
由于此应用程序没有特殊的 UI,我使用了一个 GitHub 上的类,该类帮助我在这个控制台应用程序中绘制了一个响应式表格。我修改了该类并将其添加到我的项目中。原始项目可以在 此处 找到。我还定义了一个名为 printer
的类来显示实际结果。
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);
}
如何连接组件
现在,是时候将所有内容连接起来,在屏幕上看到实际结果了。让我们看看如何实现。我没有使用任何容器,如 Unity,而是直接注入了所需的组件。
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();
}
关注点
面向对象编程帮助开发者将应用程序拆分成多个可维护和可扩展的部分,并将它们组合在一起,以满足从小到大各种规模应用程序的需求。
我应用了一些我假设您已经了解的设计模式。享受编码吧……感谢您阅读我的文章。希望它对您有所帮助。