I'm new to C# programming and has been following a certain intermediate tutorial that has asked me to write a stopwatch simulation code that includes an exception system:
Exercise 1: Design a Stopwatch
Design a class called Stopwatch. The job of this class is to simulate a stopwatch. It should provide two methods: Start and Stop. We call the start method first, and the stop method next. Then we ask the stopwatch about the duration between start and stop. Duration should be a value in Timespan. Display the duration on the console.
We should also be able to use a stopwatch multiple times. So we may start and stop it and then start and stop it again. Make sure the duration value each time is calculated properly.
We should not be able to start a stopwatch twice in a row (because that may overwrite the initial start time). So the class should throw an InvalidOperationException is it's started twice.
Educational tip: The aim of this exercise is to make you understand that a class should be always in a valid state. We use encapsulation and information hiding to achieve that. The class should not reveal its implementation detail. It only reveals a little bit, like a black box. From the outside, you should not be able to misuse a class because you shouldn't be able to see the implementation detail.
Transcribed from:
I have implemented code that does this exactly, however, I want to learn how to optimize my code to use fewer lines and make it more readable.
using System;
namespace Stopwatch_Training
{
class Program
{
public class Stopwatch
{
private DateTime _startTime;
private DateTime _stopTime;
private TimeSpan _duration;
public DateTime StartTime()
{
_startTime = DateTime.Now;
return _startTime;
}
public DateTime Stoptime()
{
_stopTime = DateTime.Now;
return _stopTime;
}
}
static void Main(string[] args)
{
var stopwatch = new Stopwatch();
int input1;
DateTime starTime;
DateTime endTime;
double duration;
int counter = 0;
while (true)
{
Console.Write("1-Start Timer\n2-Stop Timer\n");
input1 = Convert.ToInt32(Console.ReadLine());
if (input1 == 1)
{
starTime = stopwatch.StartTime();
counter = 1;
}
else
{
throw (new InvalidOperationException("Please Start the timer with 1"));
}
Console.WriteLine("Please enter '2' to stop the timer whenever you want");
input1 = Convert.ToInt32(Console.ReadLine());
if (input1 == 2)
{
endTime = stopwatch.Stoptime();
counter = 0;
}
else
{
throw (new InvalidOperationException("Please end the timer with 2"));
}
if (counter == 1)
{
throw (new InvalidOperationException("stuff"));
}
duration = (endTime - starTime).TotalSeconds;
if (duration > 60.0)
{
duration = (endTime - starTime).TotalMinutes;
Console.WriteLine(duration + "m");
}
else
{
Console.WriteLine(duration + "s");
}
}
}
}
}
2 Answers 2
You have the beginning of a stopwatch representation—you have its data. You still need to add its behavior.
Approach your design of the class as if the class should stand on its own. Input/output will be handled by something else, like the console, but the stopwatch should be the authority on its internal state.
The simplest stopwatch has a start/stop button and a display. We'll interpret "having a display" to mean "a way to get the stored elapsed time".
This means we'll need to provide three methods:
- Start: Sets the stopwatch in a running state.
- Stop: Sets the stopwatch in an idle state.
- TimeElapsed: Reads out the currently elapsed time.
Additionally, Start
and Stop
have the requirement that they should throw InvalidOperationException
when the stopwatch is not a compatible state. Our stopwatch will need a notion of running to do this.
Let's look how we can modify the stopwatch class to adopt this behavior. I've annotated some below:
public class Stopwatch
{
private DateTime _startTime; // we need this
private DateTime _stopTime; // ? either this or duration
private TimeSpan _duration;
public DateTime StartTime() // why do we return a value here?
{
// we still need to check whether we are already running
_startTime = DateTime.Now;
return _startTime;
}
public DateTime Stoptime() // why do we return a value here?
{
// we still need to check whether we are still running
_stopTime = DateTime.Now;
return _stopTime;
}
// We still need the notion of 'currently running'
}
// First attempt
public class Stopwatch
{
private DateTime _startTime;
private DateTime _stopTime;
private Boolean _running;
public void StartTime()
{
if ( _running )
{
throw new InvalidOperationException("Cannot start: already running");
}
_startTime = DateTime.Now;
_running = true;
}
public void Stoptime()
{
if ( !_running )
{
throw new InvalidOperationException("Cannot stop: not running");
}
_stopTime = DateTime.Now;
_running = false;
}
public TimeSpan ElapsedTime()
{
if ( _running )
{
return DateTime.Now - _startTime;
}
else
{
return _stopTime - _startTime;
}
}
}
Note that we check if the stopwatch is currently running when asking for elapsed time: otherwise, we're going to get a very strange reading.
Now we can 'clean up' a little bit to follow conventions:
- Class members are implicitly private, so we don't need to add the
private
modifier. - Members should start with a capital letter.
Which gives us:
public class Stopwatch
{
DateTime StartTime;
Boolean IsRunning;
DateTime StopTime;
public void Start()
{
if (IsRunning)
{
throw new InvalidOperationException("Cannot start: already running");
}
StartTime = DateTime.Now;
IsRunning = true;
}
public void Stop()
{
if (!IsRunning)
{
throw new InvalidOperationException("Cannot stop: not running");
}
IsRunning = false;
StopTime = DateTime.Now;
}
public TimeSpan ElaspedTime()
{
if (IsRunning)
{
return DateTime.Now - StartTime;
}
else
{
return StopTime - StartTime;
}
}
}
And a quick Main to play around with it:
static void Main(string[] args)
{
var stopwatch = new Stopwatch();
int choice;
do
{
Console.Write("1-Start Timer\n2-Stop Timer\n3-Read Timer\n0-Quit\n");
choice = Convert.ToInt32(Console.ReadLine());
switch (choice)
{
case 1: stopwatch.Start(); break;
case 2: stopwatch.Stop(); break;
case 3: Console.WriteLine(stopwatch.ElaspedTime()); break;
}
} while (choice != 0);
}
-
\$\begingroup\$ You have really transcribe the text from the image? Respect ;-) \$\endgroup\$t3chb0t– t3chb0t2017年11月11日 18:50:56 +00:00Commented Nov 11, 2017 at 18:50
-
\$\begingroup\$ @t3chb0t transcribinG iS morE fuN thaN televisioN ... ;-) \$\endgroup\$JvR– JvR2017年11月11日 20:11:17 +00:00Commented Nov 11, 2017 at 20:11
Here's how I implemented the solution, part of optimization I made StopWatach Class into a different file. Also made the Duration to be Displayed when both "Start" and "Stop" operations are performed.
private static void Main(string[] args)
{
Console.WriteLine("*** Welcome to my Stop Watch ***");
var stopWatch = new StopWatch();
string choice;
do
{
Console.WriteLine("1. Start 2. Stop ");
choice = Console.ReadLine().ToLower();
switch (choice)
{
case "start":
stopWatch.Start();
break;
case "end":
stopWatch.Stop();
break;
}
if (!stopWatch._isRunning &&
stopWatch._hasStopped) // Duration only prints when both start and stop Opeations are Completed
Console.WriteLine("Duration is: " + stopWatch.Duration());
} while (choice == "start" || choice == "end" || !string.IsNullOrWhiteSpace(choice));
}
and StopWatch.cs ,
public class StopWatch
{
public DateTime _startTime;
private DateTime _endTime;
public bool _isRunning;
public bool _hasStopped;
public void Start()
{
if (_isRunning)
throw new InvalidOperationException("Already Started", new Exception("Stop Watch is already running"));
_startTime = DateTime.Now;
_isRunning = true;
}
public void Stop()
{
if (!_isRunning)
throw new InvalidOperationException("Start First", new Exception("Please Enter \"Start\" to start Stop Watch"));
_endTime = DateTime.Now;
_isRunning = false;
_hasStopped = true;
}
public TimeSpan Duration()
{
var duration = new TimeSpan();
duration = _endTime - _startTime;
return duration;
}
}
-
1
-
1\$\begingroup\$ This is not a review. It does nothing to help the original poster from OVER 5.5 YEARS AGO. It is an alternative version. Unless you can explain to the OP WHY your code is better, this does not help. \$\endgroup\$Rick Davin– Rick Davin2023年06月30日 20:43:51 +00:00Commented Jun 30, 2023 at 20:43
System.Diagnostics.Stopwatch
class for inspiration. Specifically itsElapsed
andIsRunning
properties, and the fact that none of its methods write anything to the console: that's not their responsibility, and it would add an unnecessary dependency which makes the class more difficult to reuse. \$\endgroup\$