I was going to edit my other answer, but instead I'm going to take a different approach - I mean, I'll push it to the extreme ;)
Warning
I'll push it to the extreme is to be taken literally. This solution doesn't aim at solving the simple age calculator problem, rather at showing how one would architect a SOLID application - if the goal is just to calculate the difference between two dates, this is absolute overkill. If the goal is to learn how to write good OOP using a trivial/simple problem as an excuse...
Buckle up, you're in for a ride.
static void Main(string[] args)
As you know by now, this is your application's entry point. When this static
method returns, the program ends. To terminate your program, simply use return;
in this method, or structure your program flow so that normal exit simply causes the main thread (imagine a cursor running each instruction sequentially - or step through your code in the debugger) to reach the bottom of the Main
method.
This method being static
, if you're going to call anything outside of it, it's going to have to be static
as well. If you're writing procedural code, it doesn't matter.
If you want object-oriented code, the Main
method will probably have a very high level of abstraction, and will read like pseudo-code, if not like plain English.
The method revolves around the idea that it's a program's birth, life, and death.
Dependency Injection (DI) disciples (guilty!) call this entry point, the composition root. This is where you instantiate the application (and its dependencies), and run it.
Simplified to the extreme:
static void Main(string[] args)
{
var app = new MyApplication();
app.Run();
}
What's in the Run()
method?
public class MyApplication
{
public MyApplication()
{
// initialisation here
}
public void Run()
{
// app logic here
}
}
Notice that the Run
method is not static
. It exists only as a member of the interface of an object defined by this MyApplication
class - in other words, you need an instance of MyApplication
to call this method.
In this case the application logic part will feature our main loop, from which we will exit based on a condition.
public void Run()
{
var keepRunning = true;
while(keepRunning)
{
// application logic
keepRunning = false; // exit
}
}
At this point, we've reached a point of no return. Anything else we code in the Run
method will impact maintainability, testability, and readability. Better keep it to a minimum.
The key resides in delegating the work. The MyApplication
class cannot do its work alone without using the new
keyword, which would increase coupling, and without doing the work all by itself, which would decrease cohesion. Since we want low coupling and high cohesion, we'll start by avoiding the use of static
methods and of the new
keyword.
Why?
Good code is testable code. You'll want to be able to write tests for the code you write - whether you write these tests or not, writing testable code will tend to produce code that is more cohesive and less coupled.
See also: https://stackoverflow.com/a/3085419/1188513
IUserInteraction
We know we want to use the console to interact with the user, but in order to test our application logic, we'll want to be able to substitute the user's input for whatever our tests need.
public interface IUserInputProvider
{
string GetUserInput(string prompt);
T GetUserInput<T>(string prompt, IUserInputValidator<T> validator);
}
With that - and that only, we already have enough to go back to the MyApplication
class:
public class MyApplication
{
private readonly IUserInputProvider _inputProvider;
public MyApplication(IUserInputProvider inputProvider)
{
_inputProvider = inputProvider;
}
public void Run()
{
var keepRunning = true;
while(keepRunning)
{
var prompt = "Enter your birth date:";
var input = _inputProvider.GetUserInput(prompt); // no validation for now
// ...
keepRunning = false; // exit
}
}
}
I'll get back to the IUserInputValidator<T>
later.
Mocking
GetUserInput
is a method that returns a string
. Nothing more, nothing less. We know that we want to call Console.ReadLine()
, but that's an implementation detail that the MyApplication
class does not need to know about.
If we were to write a test to see if the Run
method effectively exits when the user enters "y", we would not bring up a console and wait for someone to enter "y" - instead we would set up a mock - a "fake" implementation of the IUserInputProvider
interface that returns "y" when we ask it to GetUserInput
.
Implementation
The concrete implementation we're going to be using will use the console. Nothing prevents making another concrete implementation that pops up a dialog window instead - as long as we use a prompt
and return a string
, anything can work. This means the implementation(s) can be modified in every possible way, the only assumption the MyApplication
class makes is that there's a GetUserInput
method that takes a string prompt
and returns a string
.
This could be an implementation:
public class ConsoleUserInputProvider : IUserInputProvider
{
public string GetUserInput(string prompt)
{
Console.WriteLine(prompt);
return Console.ReadLine();
}
public T GetUserInput<T>(string prompt, IUserInputValidator<T> validator)
{
string input;
T result;
var isValidInput = false;
while(!isValidInput)
{
input = GetUserInput(prompt);
isValidInput = validator.Validate(input, out result);
}
return result;
}
}
Where IUserInputValidator
is yet another abstraction that exposes a bool Validate(string input)
method.
Let's make it a generic interface:
public interface IUserInputValidator<T>
{
bool Validate(string input, out T result);
}
One could implement it like this:
public class BirthDateValidator : IUserInputValidator<DateTime>
{
public bool Validate(string input, out DateTime result)
{
return DateTime.TryParse(input, out result);
}
}
Or like this:
public enum YesNoResult
{
Unknown,
Yes,
No
}
public class YesNoValidator : IUserInputValidator<YesNoResult>
{
private readonly IDictionary<string, YesNoResult> _values;
public YesNoValidator(IDictionary<string, YesNoResult> values)
{
_values = values;
}
public bool Validate(string input, out YesNoResult result)
{
if (string.IsNullOrEmpty(input))
{
throw new ArgumentException("input", "input string is empty.");
}
var lowerCase = input.Substring(0, 1).ToLower();
var isValue = values.TryGetValue(lowerCase, out result);
if (!isValue)
{
result = YesNoResult.Unknown;
}
return (result != YesNoResult.Unknown);
}
}
As you can see, this approach produces very focused and specialized code - code that does one thing, so well, that it can't possibly even need to change. And yet it remains extensible - you could decorate any Validator
implementation with, say, a ValidationLoggerDecorator
that can log all failed validations:
public class ValidationLoggerDecorator<T> : IUserInputValidator<T>
{
private readonly IUserInputValidator<T> _validator;
private readonly ILogger _logger;
public ValidationLoggerDecorator(IUserInputValidator<T> validator, ILogger logger)
{
_validator = validator;
_logger = logger;
}
public bool Validate(string input, out T result)
{
var isValid = _validator.Validate(input, out result);
if (!isValid)
{
_logger.Info(string.Format("Validation failed for input '{0}'.", input));
}
return isValid;
}
}
Let's step back and take a look at what we've got here:
The code remains focused on a single responsibility - following the Single Responsibility Principle.
The extensibility depicted with the decorator example, is a side-effect of the Open/Closed principle: a class is closed for modification, open for extension.
The fact that the MyApplication
class can work with any implementation of the IUserInputProvider
interface, regardless of what dependencies that implementation might have, is a side-effect of the Liskov Substitution Principle.
Following the Interface Segregation Principle makes our interfaces be very focused as well, ideally exposing only a single member. This point greatly influences cohesion.
The fact that all implementations ("concrete" classes) depend on abstractions, and that these dependencies are injected into their constructor, is following the Dependency Inversion Principle*.
Together, these 5 points spell SOLID.
The IUserInputValidator
we're passing to the GetUserInput()
method, must come from somewhere. But if we create a new BirthDateValidator()
our class will be tightly coupled with that specific implementation, and it will become very hard to test the GetUserInput()
method and control validation from the outside.
The MyApplication
class can receive the validators it needs in its constructor, and we can extract some of the logic from the Run
method, into their own private methods:
public class MyApplication
{
private readonly IUserInputProvider _inputProvider;
private readonly IUserInputValidator<DateTime> _dateValidator;
private readonly IUserInputValidator<YesNoResult> _confirmationValidator;
public MyApplication(IUserInputProvider inputProvider,
IUserInputValidator<DateTime> dateValidator,
IUserInputValidator<YesNoResult> confirmationValidator)
{
_inputProvider = inputProvider;
_dateValidator = dateValidator;
_confirmationValidator = confirmationValidator;
}
public void Run()
{
var keepRunning = true;
while(keepRunning)
{
var date = GetBirthDate();
// ...
keepRunning = !GetExitConfirmation();
}
}
private DateTime GetBirthDate()
{
var prompt = "Enter your birth date:";
var input = _inputProvider.GetUserInput(prompt, _dateValidator);
return DateTime.Parse(input);
}
private bool GetExitConfirmation()
{
var prompt = "Exit (Y|N)?";
var input = _inputProvider.GetUserInput(confirmPrompt, _confirmationValidator);
return input == YesNoResult.Yes;
}
}
Notice how the constructor could easily get bloated with possibly as many validators as there are things we want to get from the user. 3 constructor parameters is probably ok. More than that though, and I would be tempted to extract the validators into their own object, so as to keep the message clear: the MyApplication
class needs validators - it doesn't do validation.
Let's extract them anyway to see what we get:
public class UserInputValidation
{
private readonly IUserInputValidator<DateTime> _dateValidator;
private readonly IUserInputValidator<YesNoResult> _confirmationValidator;
public UserInputValidation(IUserInputValidator<DateTime> dateValidator,
IUserInputValidator<YesNoResult> confirmationValidator)
{
_dateValidator = dateValidator;
_confirmationValidator = confirmationValidator
}
public IUserInputValidator<DateTime> DateValidator { get { return _dateValidator; } }
public IUserInputValidator<YesNoResult> ConfirmationValidator { get { return _confirmationValidator; } }
}
Because the actual calculation algorithm would, by itself, be a reason to change, it's best to encapsulate it in its own class.
@svick's answer could be an implementation of some IAgeCalculator
interface.
The MyApplication
class now looks like this:
public class MyApplication
{
private readonly IUserInputProvider _inputProvider;
private readonly IAgeCalculator _calculator;
private readonly UserInputValidation _validation;
public MyApplication(IUserInputProvider inputProvider,
IAgeCalculator calculator,
UserInputValidation validation)
{
_inputProvider = inputProvider;
_calculator = calculator;
_validation = validation;
}
public void Run()
{
var keepRunning = true;
while(keepRunning)
{
var date = GetBirthDate();
DisplayAge(date);
keepRunning = !GetExitConfirmation();
}
}
private DateTime GetBirthDate()
{
var prompt = "Enter your birth date:";
var input = _inputProvider.GetUserInput(prompt, _validation.DateValidator);
return DateTime.Parse(input);
}
private void DisplayAge(DateTime date)
{
var result = _calculator.GetDifferenceInYearsAndDays(date, DateTime.Today);
var message = string.Format("Your age: {0} years and {1} days", result.Item1, result.Item2);
_inputProvider.ShowMessage(message);
}
private bool GetExitConfirmation()
{
var prompt = "Exit (Y|N)?";
var input = _inputProvider.GetUserInput(confirmPrompt, _validation.ConfirmationValidator);
return input == YesNoResult.Yes;
}
}
The above assumes a ShowMessage
method was added to the IUserInputProvider
interface; this method is implemented like this:
public void ShowMessage(string message)
{
Console.WriteLine(message);
}
Now, the Main
method can serve its purpose: compose the application!
static void Main(string[] args)
{
var input = new ConsoleInputProvider();
var calculator = new AgeCalculator();
var logger = LogManager.GetLogger("logger"); // gets a NLog logger
var dateValidator = new ValidationLoggerDecorator(new BirthDateValidator(), logger);
var yesNoValues = new Dictionary<string, YesNoResult>
{
{ "y", YesNoResult.Yes },
{ "n", YesNoResult.No }
};
var confirmationValidator = new YesNoValidator(values);
var validation = new UserInputValidation(dateValidator, confirmationValidator);
var app = new MyApplication(input, calculator, validation);
app.Run();
}
As you can see, inversion of control makes it easy to change our minds and swap the dateValidator
for a simple BirthDateValidator
, or to write a unit test that will only test how date validation operates, or only how age gets calculated, independently of everything else, without requiring user intervention, and with every dependency under full control.
Of course the composition root for this trivial application is instantiating the objects manually (a.k.a. poor man's DI), and it's quite manageable. For a bigger application, you could leave this daunting task to your favorite IoC container, leaving your Main
method looking something like this (here using Ninject):
static void Main(string[] args)
{
var kernel = new StandardKernel(new MyApplicationNinjectModule());
var app = kernel.Get<MyApplication>();
app.Run();
}
The entire application's dependency graph gets resolved in a single method call - Ninject isn't the fastest at that, but its nice syntax and great extensibility make it a solid candidate to consider. If you don't know what a StandardKernel
does, the above code might seem automagical if I tell you that it does the exact same thing as the previous snippet.
And that's long enough.