patterncsharpModerate
Simple Age Calculator
Viewed 0 times
simplecalculatorage
Problem
I have written an age calculator that takes a
I'd like a general review of this. I'm especially concerned about the
birthDate as input.I'd like a general review of this. I'm especially concerned about the
message variable and the lines after the try/catch statement.namespace Age
{
class Program
{
static void Main(string[] args)
{
while (true)
{
try
{
Console.Write("Enter your birtdate: ");
DateTime birthDate = DateTime.Parse(Console.ReadLine());
int Days = (DateTime.Now.Year * 365 + DateTime.Now.DayOfYear) - (birthDate.Year * 365 + birthDate.DayOfYear);
int Years = Days / 365;
string message = (Days >= 365) ? "Your age: " + Years + " years" : "Your age: " + Days + " days";
Console.WriteLine(message);
}
catch
{
Console.WriteLine("You have entered an invalid date.\n");
}
Console.WriteLine("Exit? (y/n)");
string userValue = Console.ReadLine();
if (userValue == "y")
{
Environment.Exit(0);
}
}
}
}
}Solution
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
This method being
If you want object-oriented code, the
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:
What's in the
Notice that the
In this case the application logic part will feature our main loop, from which we will exit based on a condition.
At this point, we've reached a point of no return. Anything else we code in the
The key resides in delegating the work. The
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.
With that - and that only, we already have enough to go back to the
I'll get back to the
Mocking
If we were to write a test to see if the
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 `
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(string prompt, IUserInputValidator 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 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 `
Code Snippets
static void Main(string[] args)
{
var app = new MyApplication();
app.Run();
}public class MyApplication
{
public MyApplication()
{
// initialisation here
}
public void Run()
{
// app logic here
}
}public void Run()
{
var keepRunning = true;
while(keepRunning)
{
// application logic
keepRunning = false; // exit
}
}public interface IUserInputProvider
{
string GetUserInput(string prompt);
T GetUserInput<T>(string prompt, IUserInputValidator<T> validator);
}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
}
}
}Context
StackExchange Code Review Q#44606, answer score: 18
Revisions (0)
No revisions yet.