By: Team AY1920S2 W16-2
Since: Feb 2020
Refer to the guide here.
The Architecture Diagram given above explains the high-level design of the App. Given below is a quick overview of each component.
-
At app launch: Initializes the components in the correct sequence, and connects them up with each other.
-
At shut down: Shuts down the components and invokes cleanup method where necessary.
Commons
represents a collection of classes used by multiple other components.
The following class plays an important role at the architecture level:
-
LogsCenter
: Used by many classes to write log messages to the App’s log file.
The rest of the App consists of four components.
Each of the four components
-
Defines its API in an
interface
with the same name as the Component. -
Exposes its functionality using a
{Component Name}Manager
class.
For example, the Logic
component (see the class diagram given below) defines it’s API in the Logic.java
interface and exposes its functionality using the LogicManager.java
class.
The Sequence Diagram below shows how the components interact with each other for the scenario where the user issues the command exercise delete 1
.
The sections below give more details of each component.
API : Main.java
The Main
class consists of the SplashScreen
and the MainApp
.
When the app starts up, Main#main
is called. This sets the Preloader
of the Application
to be SplashScreen
. Thereafter, it launches MainApp
.
During the GUI start up process, the SplashScreen
will first load on a Thread
. In the meantime, MainApp#init
will run. Once MainApp#init
completes, and just as MainApp#start
is about to run, MainApp
will send a Preloader.StateChangeNotification
to SplashScreen
to close its window. After that, UiManager#start
is called, which creates the view for the main application.
API : Ui.java
The UI consists of a MainWindow
that is made up of a CommandBox
, ResultDisplay
and other panels. All these, including the MainWindow
, inherit from the abstract UiPart
class.
The UI
component uses JavaFx UI framework. The layout of these UI parts are defined in matching .fxml
files that are in the src/main/resources/view
folder. For example, the layout of the MainWindow
is specified in MainWindow.fxml
The UI
component executes user commands using the Logic
component.
For more information about each component, check out the 'Implementation' section.
API :
Logic.java
-
Logic
uses theParserManager
class to parse the user command -
The
ParserManager
creates and returns aCommand
object -
The
Command
object is executed by theLogicManager
-
The command execution can affect the
Model
(e.g. adding a exercise). -
The result of the command execution is encapsulated as a
CommandResult
object which is passed back to theUi
. -
In addition, the
CommandResult
object can also instruct theUi
to perform certain actions, such as displaying the list of workouts.
Inside the Logic
package, there are some utility classes such as the ArgumentTokenizer
. It is responsible for tokenizing an input and returning an ArgumentMultimap
, which can be used to get the value of a particular Prefix
.
Given below is the Sequence Diagram for interactions within the Logic
component for the execute("delete 1")
API call.
ℹ️
|
The lifeline for ExerciseCommandParser and DeleteCommandParser should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
|
For more information about each component, check out the 'Implementation' section.
API : Model.java
The Model
:
-
stores a
UserPref
object that represents the user’s preferences. -
stores an
ExerciseList
that represents the list of exercises. -
stores a
WorkoutList
that represents the list of workouts available. -
stores a
ScheduleList
that represents the list of upcoming schedules. -
stores a
LogList
that represents the list of completed data. -
stores a
TimerList
that represents a time delta in seconds. -
stores an
Optional
OngoingWorkout
that may contain a session, if started. -
exposes an unmodifiable
ObservableList<Exercise>
,ObservableList<Workout>
,ObservableList<ScheduledWorkout>
,ObservableList<Log>
,ObservableList<OngoingSet>
, andObservableList<Integer>
(time delta) that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list changes. -
does not depend on any of the other components.
For more information about each component, check out the 'Implementation' section.
API : Storage.java
The Storage
component is resonsible for the persistent storage of the application. It can save these class in JSON format and read it back:
-
UserPrefs
-
Exercise
-
Workout
-
Schedule
-
Log
For more information about each component, check out the 'Implementation' section.
This section describes some noteworthy details on how each features are implemented.
The Session feature resides on the Home page and is comprised of Start
, Stop
, Done
, and Skip
commands.
OngoingWorkout
, OngoingSession
, OngoingSet
, and OngoingSetList
are models that are vital components of the feature.
The Session feature also seamlessly incorporates a functional timer. Timer functionality is found embedded in the session related functions in ModelManager
.
The activity diagram below demonstrates the actions of a user with this feature.
Start depicts the start of a valid (non-zero sets remaining) workout session.
Upon depletion of all remaining sets, the OngoingWorkout
automatically stops and produces a CompletedWorkout
which is saved.
The only inputs the user is required to give throughout the session are done
(corresponding to the yes of the conditional branch)
and skip
(no of the conditional branch). Stop
is optional.
This is a conscious design choice as elaborated in the next section.
The operations handling these actions are found in OngoingWorkout
:
-
OngoingWorkout#skip()/OngoingWorkout#done()
— removes and returns the next set and updates queue of remaining sets. -
OngoingWorkout#finish()
— creates and returns an immutableCompletedWorkout
object for saving and logging.
Further operations are exposed in the Model
interface as Model#startSession()
and Model#stopSession()
.
The above class diagram briefly depicts how the session related models (in OngoingWorkout
) interact with surrounding classes.
The OngoingWorkout
class has a dependency on Workout
, which has been abstracted for clarity.
In turn, CompletedWorkout
class has a dependency on OngoingWorkout
.
OngoingWorkout
objects are created at the start of a workout session, and can be thought of as 'converted' to CompletedWorkout
objects at the end of a workout session. Workout
and CompletedWorkout
objects are immutable, hence the design choice for using an intermediary, stateful OngoingWorkout
class.
The inner workings of the feature are briefly expounded below through a simple simulation.
Arms Day
is a Workout
with ID = 2
and consists of 2 sets of one exercise called Bench Press
.
Step 1. The user selects an existing Workout
called Arms Day
with ID = 2
and starts a session with start 2
.
-
Logic
andParser
redirect control toStartCommand#execute()
where an instance ofOngoingWorkout
is created with theArms Day
object. The upcoming set of the next exercise is displayed for the user. -
The timer starts counting. This acts as a visual cue for the user that notifies the start of the workout and is essentially a once-off, superfluous action, since it’s main purpose is to time rest between sets.
Step 2. The user completes the first set and types done
.
-
DoneCommand#execute()
is invoked, which in turn callsModel#done()
to obtain the lastCompletedSet
object to display on the UI, and statefully updates the instance ofOngoingWorkout
. -
The rest timer resets to 00:00 and starts counting.
-
The sequence diagram above demonstrates the interaction between the Logic and Model of ZeroToOne. For brevity, some inner details between
OngoingWorkout#done()
and the return ofcs
are omitted.
Step 3. After resting, the user fails the last and final set of Bench Press
and types skip
.
-
SkipCommand#execute()
is invoked. The process is almost identical to the above execution ofdone
except this time the instance ofCompletedSet
is created withisFinished
set tofalse
. -
Since this is the final set of the session,
OngoingWorkout#hasSetLeft()
returns false, andOngoingWorkout#finish()
is called which creates and returns an immutableCompletedWorkout
object. This object is saved and passed on for use by theLog
feature. -
The workout session is now complete, and the timer and UI is reset.
-
The workout session can be viewed in the
Log
tab.
[Note] In the usual flow, the stop
command is not used, and is reserved only for a premature ending of a
workout session, where all remaining sets are marked as incomplete.
-
Current Implementation: Minimal commands and typing during an ongoing session. Timer functionality is completely automatic.
-
Pros: Less interruption during actual exercising to enter commands.
-
Cons: More experienced users are not able to have a more customizable workout.
-
-
Current Implementation: Exercises and sets have fixed ordering based on their creation.
-
Pros: Easier for new user to follow a workout session, less typing required during a workout session. Session has simple and predictable behaviour.
-
Cons: Experienced users are not able to have an ad-hoc, customizable workout without creating a new workout with the specific order they want.
-
The Exercise feature forms the basic building block for the application. It allows users to CRUD exercises, which will then be used for the creation of workouts. The command is prefixed by the keyword exercise
. Available commands are create
, edit
, list
, delete
and many more.
The following portion will explain in-detail each component of the Exercise feature.
The figure above depicts the Model of the Exercise feature. Starting from the most primitive type:
-
NumReps
stores the number of repetitions a person has to complete for a particular exercise set -
Weight
stores the weight in kg that a person has to complete for a particular exercise set -
ExerciseSet
represents an exercise set that a person has to complete. EachExerciseSet
comprises of exactly 1NumReps
object and 1Weight
object -
ExerciseName
stores the name of a particular exercise -
Exercise
represents a collection of exercise sets that a person has to complete. EachExercise
will consists of exactly 1ExerciseName
object and any number ofExerciseSet
(including zero) -
UniqueExerciseList
represents a collection ofExercise
objects. There can be any number ofExercise
objects inUniqueExerciseList
, but they must be unique. -
ExerciseList
implements the interfaceReadOnlyExerciseList
that ensures that the outward-facing exercise list is unmodifiable by other modules. TheExerciseList
object has exactly 1UniqueExerciseList
for storage purposes. -
ExerciseList
is controlled by theModelManager
.
Do note that some of the Exercise
objects in the ExerciseList
are also referenced in the WorkoutList
. For more information, refer to the Workout
implementation.
The Storage component provides the functionalities that enable the persistent storage of the model. Starting from the most primitive type:
-
JacksonExerciseSet
contains exactly twoString
objects, one for the number of repetitions and one for the weight. This class has a dependency to theNumReps
andWeight
model due to thetoModelType()
function. This function convertsJacksonExerciseSet
into the model’sExerciseSet
so that it can be used in other parts of the application. -
JacksonExercise
contains exactly oneString
object that represents the exercise name, and any number ofJacksonExerciseSet
objects. Similarly, it has a dependency toExerciseName
,ExerciseSet
andExercise
object due to thetoModelType()
function. -
JacksonExerciseList
is the persistent storage for theExerciseList
model, and it contains any number ofJacksonExercise
objects. -
ExerciseListStorageManager
implements theExerciseListStorage
which provides certain functionalities required for the storage to work properly. TheExerciseListStorageManager
is controlled by theStorageManager
.
The Exercise Commands package stores the business logic of the exercise feature. The commands are organised in a hierarchical fashion, in the order of precedence in a valid input. For example, SetCommand
inherits from ExerciseCommand
as the set
comes after exercise
in the input exercise set
.
Each command contains a COMMAND_WORD
which is a single word that is unique to the command. Each command also implements an execute
method that represents the logic of the command. Instructions to control the model, storage and view are stored inside this method.
The parsers are responsible for parsing a user input into a Command
object. For the Exercise component, there are parsers for every command that accepts user arguments. For example, since exercise list
does not take in any argument, there is no parser for the ListCommand
. After parsing the user input, the parser will return Command
object to the caller, which will execute the command via the execute
method.
This section will illustrate an example of an exercise command execution using the input exercise create e/Bench Press
.
In this portion, we will trace the sequence diagram of the exercise create
command to better understand the internals of the Exercise feature.
-
The user enters the command
exercise create e/Bench Press
-
LogicManager
will pass the command to theParserManager
for parsing -
ParserManager
upon seeing that the command is prefixed byexercise
creates aExerciseCommandParser
-
ParserManager
then passcreate e/Bench Press
toExerciseCommandParser
-
ExerciseCommandParser
upon seeing that the command is prefixed bycreate
creates aCreateCommandParser
-
ExerciseCommandParser
then pass the argumente/Bench Press
toCreateCommandParser
-
CreateCommandParser
then attempts to create anExerciseName
object using theString
in the argument -
Using the
ExerciseName
,CreateCommandParser
then create aCreateCommand
object with the exercise name -
The
CreateCommand
is then passed back to theLogicManager
-
LogicManager
callsc.execute()
-
CreateCommand
will attempt to create anExercise
using the exercise name -
After creating the
Exercise
object, theCreateCommand
will attempt to store the new exercise by calling theaddExercise
method ofModel
-
After the exercise is successfully added, a
CommandResult
object is created -
This result is then passed back to the
LogicManager
which will display the output on the GUI
One of the consideration while designing was that the commands in exercise are extremely nested. We have commands such as exercise set create r/1 m/10
. While we could have chucked all the parsing in ExerciseSetCreateParser
class, we realised that it will be better if we were to abstract the parser into separate classes. This allows us to group the functionalities of the parser in a single file. For example, ExerciseCommandParser
will parse any string that has the word exercise
as the prefix. SetCommandParser
will do so for a prefix of set
. This means that for the above command, while we have to go through multiple parsers which can make the performance of the application suffer, each of the parsers have a single responsibility which makes it a better design choice.
For the Model component, note that Exercise
objects are supposed to be unique whereas ExerciseSet
objects are not. This is created due to our observations of the workout regimes in the real world.
For ExerciseSet
, while set weights and number of repetitions tend to vary during an exercise, users may want to have the freedom to do multiple sets with the same configuration during the course of the exercise. Hence, it is unwise to make it unique.
However, for exercises, we noted that users tend to reuse the same exercise throughout different workout plans. At the same time, there is a high chance of users creating duplicate exercises when the number of exercises in the application increases significantly. Therefore, we chose to make Exercise
a unique object instead.
The workout feature in ZeroToOne allows users to manage their workouts! Users will be able to create a workout, add exercises to it, as well as edit the workout and its exercises, and more.
This is an activity diagram for the user to create a new workout. Essentially, the user will first create a workout with a name that does not contain any exercises yet. The user will then try to find exercises to add, and if their desired exercises cannot be found, the user will create them and add them into the workout.
The Workout Manager consists of a WorkoutList
, which contains a number of workouts that the user has created.
Each workout consists of a WorkoutId
, a WorkoutName
and finally, an WorkoutExerciseList
.
The following class diagram shows the overview of the Workout feature:
-
WorkoutList
implements the interfaceReadOnlyWorkoutList
-
The
WorkoutList
is managed by theModelManager
-
FilteredWorkoutList
is an aggregation of oneWorkoutList
, to filter the view of the workout list shown to the user -
WorkoutList
composes of aUniqueWorkoutList
-
In turn, a
UniqueWorkoutList
contains any number ofWorkout
objects -
Each
Workout
is comprised of two things: aWorkoutName
and any number ofExercise
objects-
WorkoutName
contains the name of theWorkout
-
Exercise
is an exercise that belongs in aWorkout
-
ℹ️
|
Each Workout can contain any number of Exercise objects, including zero. However, a Workout must not be empty i.e. have at least one Exercise , in order to be scheduled. This is implemented in the Schedule feature as referenced in Section 3.4, “Schedule”.
|
ZeroToOne’s Model
extends the WorkoutModel
. Here are all the functions to carry out workout-related activities:
-
Model#getWorkoutListFilePath()
- Retrieves thePath
of theWorkoutList
-
Model#setWorkoutListFilePath(Path workoutListFilePath)
- Sets thePath
of theWorkoutList
-
Model#setWorkoutList(ReadOnlyWorkoutList workoutList)
- Sets theWorkoutList
to be aReadOnlyWorkoutList
-
Model#getWorkoutList()
- Returns an unmodifiableReadOnlyWorkoutList
-
Model#hasWorkout(Workout workout)
- Returns true if a workout exists in the workout list -
Model#deleteWorkout(Workout target)
- Deletes a specified workout from the workout list -
Model#addWorkout(Workout target)
- Adds a new workout to the workout list -
Model#setWorkout(Workout target, Workout editedWorkout)
- Replaces a particular workout with an edited workout -
Model#setExerciseInWorkout(Exercise target, Exercise editedExericse
- Replaces a target exercise in any workout with the edited exercise -
Model#deleteExerciseFromWorkout(Exercise exercise)
- Deletes any instance of an exercise from all workouts -
Model#getFilteredWorkoutList()
- Returns an unmodifiable view ofObservableList<Workout>
-
Model#updateFilteredWorkoutList(Predicate<Workout> predicate)
- Updates the filter ofFilteredWorkoutList
to show filtered views of the list to the user
-
The
WorkoutListStorageManager
implements the interfaceWorkoutListStorage
. It also creates aJacksonWorkoutList
. -
The
JacksonWorkoutList
composes of any number ofJacksonWorkout
objects. -
In turn, each
JacksonWorkout
composes of aString
which is the workout name, and any number ofJacksonExercise
objects which are the exercises in each workout. -
The relationship of the Storage component to the Model component is also shown.
I will go on to explain the Parser structure for the workout commands.
-
The
WorkoutCommandParser
creates allWorkout
related Parsers. These parsers allow the user to create, edit, delete and find workouts, as can be seen in the left hand side of the above diagram. -
In addition, the
WorkoutCommandParser
creates theWorkoutExerciseCommandParser
. -
In turn,
WorkoutExerciseCommandParser
creates all workoutExercise
related Parsers. These parsers allow the user to add, edit and delete saidExercise
objects.
To illustrate an example of a command from the Workout Manager, the following sequence diagram
depicts flow of the program when the command workout find w/Arms Workout
is run.
-
When the user runs the command
workout find w/Arms Workout
, theLogicManager
will first take in the command, by calling theexecute()
function on it. -
The
ParserManager
then has toparse("workout find w/Arms Workout")
. -
Next, the
WorkoutCommandParser
has toparse("find w/Arms Workout")
for the command. -
Once the command has been parsed as a
FindCommand
, it will be passed on to theFindCommandParser
. -
The
FindCommandParser
can then create aFindCommand
. This is constructed with theWorkoutName
for "Arms Workout" (a low-level detail that has been abstracted from the diagram). -
The
FindCommand
can be returned to theLogicManager
, where it willexecute()
theFindCommand
. -
Model#updateFilteredWorkoutList(Predicate<Workout> predicate)
is used to update the view of the workout list to show the requested Workout(s), using thePredicateFilterWorkoutName
that is returned by theFindCommand
(a low-level detail that has been abstracted from the diagram). -
Finally, the resulting output message will be returned as the
CommandResult
.
-
Option 1: Use existing
Exercise
class inWorkout
-
Advantage: Building on an existing class is simpler and more intuitive.
-
Disadvantage: Introduces a dependency on the
Exercise
class
-
-
Option 2: Create a new
WorkoutExercise
class for theWorkout
class-
Advantage: Creates an extra layer of abstraction and removes the dependency on
Exercise
. -
Disadvantage: More code needed which may be redundant.
-
In the end, I decided to stick with Option 1. This is because creating a new WorkoutExercise
class
is redundant and unnecessary, when there is no functional difference between an Exercise
and a WorkoutExercise
,
other than the context that they are referenced in. In addition, this would make the deletion of any instance of a particular Exercise
from a Workout easier, when an Exercise
is deleted from the ExerciseList
. Hence, to simplify matters, using the existing Exercise
class to construct workouts was better.
The schedule feature in ZeroToOne allows users to plan their workouts! Users will be able to create a schedule on a specific date and time in the future, edit the date and time, as well as delete the schedule.
The following class diagram shows the overview of the Scheduler
:
As seen in the diagram above, The Scheduler
consists two lists, ScheduleList
and ScheduledWorkoutList
.
The ScheduleList
contains a number of Schedule
that the user has created, while the ScheduledWorkoutList
is essentially a chronologically sorted list of ScheduledWorkout
that allows JavaFX listeners to track changes when they occur.
ℹ️
|
The decision to have both ScheduleList and ScheduledWorkoutList may seem bizarre at first glance, but it is implemented this way after careful consideration, to better support operations on 'RecurringSchedule'(Proposed). Refer to Section 3.4.5, “Model Design Considerations” for more information.
|
ZeroToOne’s Model
extends the SchedulerModel
. Here are all the functions to carry out schedule-related activities:
-
Model#hasSchedule(Schedule schedule)
- Returns true if a schedule exists in the schedule list -
Model#addSchedule(Schedule schedule)
- Adds a new schedule to the schedule list -
Model#setSchedule(Schedule scheduleToEdit, Schedule editedSchedule)
- Replaces a particular schedule with an edited schedule -
Model#populateSortedScheduledWorkoutList()
- Clear and populateSortedScheduledWorkoutList
by querying everySchedule
inSchedueList
through methodSchedule#getScheduledWorkout()
-
Model#getSortedScheduledWorkoutList()
- Returns an unmodifiable view ofObservableList<ScheduledWorkout>
-
Model#deleteScheduledWorkout(ScheduledWorkout scheduledWorkoutToDelete)
- Deletes theSchedule
that generated the specifiedScheduledWorkout
from the schedule list, then re-populatesSortedScheduledWorkoutList
-
Model#deleteWorkoutNameFromSchedule(WorkoutName workoutNameToDelete)
- Deletes all instances of schedules that are associated with the specified workout name -
Model#editWorkoutNameInSchedule(WorkoutName workoutNameToEdit, WorkoutName editedWorkoutName)
- Updates all instances of schedules that are associated with the specified workout name to the new workout name -
Model#getScheduleList()
- Returns an unmodifiableReadOnlyScheduleList
To illustrate an example of a command from the Scheduler
, the following sequence diagram depicts flow of the program when the command schedule create 1 d/2020-04-01 14:00
is run.
-
When the user runs the command
schedule create 1 d/2020-04-01 14:00
, theLogicManager
will first take in the command, by calling theexecute()
function on it. -
The
ParserManager
then has toparse("schedule create 1 d/2020-04-01 14:00")
. -
Next, the
ScheduleCommandParser
has toparse("create 1 d/2020-04-01 14:00")
for the command. -
Once the command has been parsed as a
CreateCommand
, it will be passed on to theCreateCommandParser
, which will then parse the index usingParserUtil
, and parse the date and time usingScheduleParserUtil
. -
The
CreateCommandParser
can then create aCreateCommand
, which is then passed back toLogicManager
to execute. -
When
LogicManager
executes theCreateCommand
, it creates a newSchedule
, and checks whether the model already contains thisSchedule
. -
If the model does not contain this
Schedule
, theLogicalManager
will then add it to the model by callingModel#addSchedule(schedule)
. -
Finally, the resulting output message will be returned as the
CommandResult
.
ℹ️
|
Date and time must not be outdated and follow the format {yyyy}-{mm}-{dd} {HH:mm} |
And the following sequence diagram shows how the ScheduledWorkoutList
is populated after the method Model#addSchedule(schedule)
is called:
-
After the
LogicManager
calls the methodModel#addSchedule(schedule)
,ModelManager
callsScheduler#addSchedule(schedule)
, which in turns callsScheduleList#addSchedule(schedule)
and adds theSchedule
in theScheduleList
. -
The
Scheduler
will then populate theScheduledWorkoutList
by calling the methodpopulateSortedScheduledWorkoutList()
. -
First,
Scheduler
gets the current date and time by callingDateTime#now()
. -
Then,
Scheduler
iterates through everySchedule
inScheduleList
, and requests to getScheduledWorkout
by callingSchedule#getScheduledWorkout(now)
on each of them. -
Finally,
Scheduler
resets theSortedScheduledWorkoutList
with the latestScheduledWorkout
by callingScheduledWorkoutList#setScheduledWorkouts(scheduledWorkouts)
, and user’s view ofScheduledWorkout
is refreshed. -
And in the end, the resulting output message will be passed back to
LogicManager
and display to user.
ℹ️
|
Scheduler#populateSortedScheduledWorkoutList() is called whenever the schedules in ScheduleList have been changed.
|
-
Option 1:
Model
stores onlyScheduledWorkout
objects.-
Pros: Easier to implement.
-
Cons: Harder to maintain when it comes to
edit
ordelete
of recurring scheduled workouts.
-
-
Option 2 (current choice): Saves
Schedule
objects that can be used to produceScheduledWorkout
when requested.-
Pros: Changes can be made to recurring schedules instead, and in turn reflect in all related
ScheduledWorkout
. -
Cons: Makes model much more complicated.
-
In the end, I decided to go with option 2 as it is easier to maintain, more extensible and less computationally expensive.
When the system has a number of recurring schedules that do not have an end date, it introduces a few problems:
-
How many scheduled workouts to create/store for each recurring schedule
-
How to efficiently retrieve and update all related scheduled workouts when a recurring schedule is edited or deleted
Implementing option 1 would mean that we will have to impose an additional constraint onto the system, which is either how many scheduled workouts to display for each recurring schedule or what is the longest time frame of scheduled workouts to display to user, as we simply can’t generate indefinite number of scheduled workouts for technically everlasting recurring schedules. Additionally, when the number of high frequency recurring schedules in the system increases, it will be extremly computationally expensive to edit or delete recurring schedules, as the system will have to iterate through every ScheduledWorkout objects and update accordingly.
On the other hand, implementing option 2 makes it easier to handle these problems by storing changes to the Schedule objects instead, which can be used to generate ScheduleWorkout objects when requested. Moreover, it is more extensible because if we are going to introduce a calendar view in the future, where user can view a specific time frame of scheduled workouts, the system only needs to generate for that time frame only, which can be done much more efficiently!
The log statistics feature in ZeroToOne allows users to view their completed workouts visually with meaningful aggregated information so they can see how much progress made.
The ZeroToOne user can generate and view these statistics via the log display
command which takes in two optional parameters startRange
and endRange
. Workouts will only be considered if they fall within the startTime
and endTime
range.
The log statistics feature was built from the ground up with modularity and extensibility in mind which is inline with SLAP and SOLID principles. This is exemplified in the decision to have a DataPoint
abstract class. The class diagram is shown below.
A DataPoint
is metric that takes gives the user some information on his or her performance. Data points are list row by row to the user with the labelName
on the left followed by the result
on the right in a table format.
This abstract class is then extended by concrete class as shown below.
Through this abstraction we make it easy to extend and include new metrics to track in the future. All that needs to be done is to create a new class that extends DataPoint
and implements the calculate
function that takes as input a list of CompletedWorkout
and does the necessary calculations and sets the results.
To help explain the implementation of this feature, this portion of the developer guide will examine the command execution of the log display
command.
For the sake of clarity, this portion of implementation has been broken down into two phases.
Phase 1 will primarily deal with the actual command parsing, execution and the storing of the startRange
and endRage
in the model while Phase 2 will then go on to generate statistics based on the startRange
and endRange
provided in phase 1.
The sequence diagram below describes the flow when the log display
command is invoked by the user.
This next section goes over in a little more detail how the program unfolds.
Step 1: When the user enters the log display
command, the MainWindow#executeCommand()
is executed and in turn calls on the LogicManager
to help with this.
Step 2: The LogicManager
then goes through with the normal flow of parsing the command and its parameters and then returning a valid DisplayCommand
. This section will not go into details and specifics of this process as it has already been covered above.
Step 3: Once the Logic Manager
receives the DisplayCommand
, it will call the concrete implementation of the DisplayCommand#execute(Model model)
to run the command with the current model.
Step 4: The execute
function will then go ahead and store the startRange
and endRange
in the model via the Model#setStatisticsDataRange
function.
Step 5: This is where the display command differs from all the other commands. When constructing the CommandResult
to be returned, the showReport
flag in CommandResult
is set to True
.
Summary:
startRange
and endRange
are updated in the current Model
The showReport
flag in CommandResult
being returned is set to True
Following immediately from phase 1, phase 2 will commence with the return of CommandResult
to the MainWindow#executeCommand
function. The sequence diagram below documents the execution flow.
Similar to phase 1, this next section will detail the steps taken in a little more detail.
Step 1: When CommandResult
is returned to the main window, it is checked for the showReport
to decide if the report page should be shown.
Step 2: Since the showResult
flag will be set to True
, the MainWindow
will call LogicManager#generateStatistics
to help with the generating of the statistics.
Step 3: The LogicManager
will then query all the information it requires from the model including the startRange
and endRange
described in phase 1.
Step 4: Next, the static function Statistics#generate
will be invoked. This function will then instantiate new instances of all the DataPoints
configured.
Step 5: Once instantiated, the DataPoint#calculate
function will be invoked on all the DataPoint
objects. This used to calculate and populate each data point with valid data from the list of workouts.
Step 6: Once this is done, a new StatisticsData
object is created with the data points and the list of workouts and returned.
Step 7: Lastly, the view StatisticsWindow#show
function is called with the StatisticsData
previously returned. This renders the new window with the computed statistics.
The following section will detail some of the design consideration and design choices we made while implementing the log statics feature.
We wanted to build the statistics feature to be flexible. This is why we decided to include a startRange
and endRange
to the log display
command. This allows users to generate reports for custom datetime ranges.
Due to this flexibility we had to think hard of sensible defaults for this date range if they were not specified. We eventually decided on the flow specified by the activity diagram below.
-
Alternative 1: Do not support date range in log command
-
Pros: Easy to implement.
-
Cons: Less flexible and metrics used will be limited.
-
-
Alternative 2 : Require
startRange
andendRange
in all display commands-
Pros: Would have to consider sensible defaults
-
Cons: More for the user to type if he/she wants to view statistics over all workouts.
-
-
Alternative 3 : Allow optional
startRange
andendRange
use start of epoch for start time and current datetime for end date.-
Pros: Easy to implement
-
Cons: Statistics generated using the number of days might not make sense. [Note] For example, the average workout time per day would not make sense since the number of days since epoch will be huge.
-
-
Alternative 4 (current choice): Allow optional
startRange
andendRange
and if not given take earliest start date time and latest end datetime-
Pros: More meaningful metrics and statistics
-
Cons: More complicated model and defaults might not seem intuitive at first
-
We decided to build these features with security at the forefront. This can be seen in
-
The use of optionals and
ifPresent()
functions guards againstNullPointerExceptions
when the user does not provide optional fields. -
requireNonNull
are frequently used to check fornull
values. -
Strong exception handling when parsing
startRange
andendRange
in the display command and throwing aParseCommandException
if date times provided are not valid in the specified format.
Refer to the guide here.
Refer to the guide here.
Refer to the guide here.
Target user profile:
-
Enjoys exercising and does it regularly
-
Has a need to manage a significant number of workouts
-
Prefers desktop apps over other types
-
Can type fast
-
Prefers typing over mouse input
-
Is reasonably comfortable using CLI apps
Value proposition: Manage workouts faster than a typical mouse/GUI-driven app
Priorities: High (must have) - * * *
, Medium (nice to have) - * *
, Low (unlikely to have) - *
Priority | As a … | I want to … | So that I can… |
---|---|---|---|
|
User who wants to track live progress while exercising |
Time each part of my workout |
Stay on track for each workout session I do |
|
User who does a large variety of kinds of exercises |
Keep track of all my exercises for different fitness purposes |
Stay organised |
|
User with many different workouts |
Keep track of all my workouts |
Stay organised |
|
User that is busy with multiple commitments |
Keep on schedule for my exercise regime |
Stay disciplined and keep exercising regularly |
|
User who has exercise goals |
See or track my current progress |
Check if I am reaching my goals |
|
User who likes to schedule single workout sessions, but will occasionally do recurring ones |
Keep track of the two types of workouts |
Manage different types of workouts simultaneously |
|
User who wants to track live progress while exercising |
Have a timer for each part of my workout |
Stay on schedule for each workout session I do |
|
User who enjoys social activities |
Share my workout routines with my friends |
Help each other out by sharing new exercise regimes |
|
User who has a regular workout schedule, but likes to schedule one-off sessions |
Some way to keep track of both regular and one-off workouts |
Differentiate between the workouts and manage them both simultaneously |
|
User who loses motivation over a long period of time |
Something to help me stay motivated |
Keep going and not give up halfway |
|
User who lacks self discipline at times |
Something to help me maintain the discipline to stay on track |
Stick to a regular exercise schedule and regime |
|
User who needs some inspiration or a change of pace |
Have sample workout routines to follow |
Get more ideas and ways to structure my workouts and find new exercises |
|
User who has many different types of workouts |
To categorize the different types of workout |
Achieve better organisation |
|
New user who is not familiar with setting up exercise regimes |
To have a way to consult with fitness trainers or professionals |
Gain better knowledge and understanding of exercise regimes |
|
User who is sociable and largely influenced by my peers |
To see my friends’ progress and achievements |
Motivate myself to work harder and help each other out |
|
User who sometimes gets a little bored when working out |
To have a way to entertain or distract myself when working out |
Keep myself on the task at hand by giving an extra motivation boost |
(For all use cases below, the System is the ZeroToOne
and the Actor is the user
, unless specified otherwise)
MSS
-
User requests for the existing exercises in the workout
-
System shows a list of the existing exercises
-
User requests to add an exercise to the workout
-
System adds the exercise to the workout
Use case ends.
Extensions
-
2a. The requested workout does not exist
-
2a1. Use case ends
-
-
2b. There are no exercises in the workout
-
2b1. System shows a message to inform the user
Use case resumes at step 2
-
-
3a. User request format is invalid
-
3a1. System shows an error message
Use case resumes at step 3
-
MSS
-
User requests to list workouts
-
System shows a list of all workouts
-
User requests to delete a specific workout
-
System deletes the workout
Use case ends.
Extensions
-
2a. The list is empty
Use case ends
-
3a. The given user index is invalid
-
3a1. System shows an error message
Use case resumes at step 2
-
MSS
-
User requests a log of past workouts
-
System shows a list of all past logged workouts
-
User requests to find a past workout by keyword
-
System shows the past workouts that match the keyword
Use case ends
Extensions
-
2a. The log is empty
Use case ends
-
3a. The keyword does not match any past workouts
-
3a1. System shows an error message
Use case ends
-
-
Should work on any mainstream OS as long as it has Java
11
or above installed. -
Should be able to hold up to 100 workouts without a noticeable sluggishness in performance for typical usage.
-
A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse.
-
Should not depend on a remote server, so that a user can exercise in any condition or environment.
- CLI
-
Stands for Command Line Interface, which processes commands to a computer program in the form of lines of text.
- Exercise
-
A single type of exercise, for example push ups or crunches. GUI Stands for Graphical User Interface, which is a form of user interface that allows users to interact with electronic devices through graphical means, not textual means.
- Instance
-
A specific instantiation of an object.
- Mainstream OS
-
Windows, Linux, Unix, OS-X
- Schedule
-
A workout that has been planned to be carried out on a specific date or dates.
- Workout
-
A set of exercises to be done together, in a certain order and time frame.
Given below are instructions to test the app manually.
ℹ️
|
These instructions only provide a starting point for testers to work on; testers are expected to do more exploratory testing. |
-
Initial launch
-
Download the jar file and copy into an empty folder
-
Double-click the jar file
Expected: Shows the GUI with a set of exercises and workouts. The window size may not be optimum.
-
-
Saving window preferences
-
Resize the window to an optimum size. Move the window to a different location. Close the window.
-
Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained.
-
-
Starting a workout session with a
WORKOUT_ID
-
Prerequisites: At least 1 workout containing valid exercises and sets must exist.
-
Test case:
start 1
Expected: Workout session started with the index of the workout shown in the status message. Timer starts counting from 00:00. -
Test case:
start 0
Expected: No workout session is started. Error details shown in the status message. Status bar remains the same. Timer remains at 00:00. -
Other incorrect delete commands to try:
start
,start x
(where x is larger than the list size),start y
(where y is the ID of an invalid workout)
Expected: Similar to previous.
-
-
Workout sessions are saved automatically at the end of a workout session.
-
Prerequisites: A workout session must be ongoing.
-
Test case:
stop
Expected: Workout session stops and displays in the status message. Timer resets to 00:00. The workout session should appear in theLog
tab. -
Test case:
skip
,done
Expected: Same as above, ifskip
ordone
is given on the last set. -
Other incorrect delete commands to try:
save
,stop 1
,skip
/done
(when there are more sets remaining)
Expected: Error message displayed in the status bar.
-
Feature | AB3 | ZeroToOne |
---|---|---|
LoC |
~10k |
~26k |
Difficulty |
10 |
15 |
Effort |
10 |
15 |
The idea of an exercise app alone itself is not particularly challenging. However, the ZeroToOne team believes that taking a user-experience-first philosophy toward designing an exercise app requires a conscientious design-approach, one that is a challenge.
This is something that we have endeavoured to achieve in this project.
We think that despite the constraints of the CS2103T project, we have managed to demonstrate this to a larger degree.
AB3 deals primarily in the domain of data storage and retrieval. ZeroToOne has taken this one step further, with the ability to:
-
Statefully update new models during a workout session
-
Apply useful statistics across an entire collection of stored data
-
Extend AB3’s models to be more functional
Furthermore, on top of the ~16k LoC added to the project, the ZeroToOne team has managed to:
-
Maintain > 70% test coverage
-
Apply SOLID design principles
-
Maintain a consistent quality of the codebase through stringent PR reviews and hooks.
The largest challenge we faced as a team was communication. Being fast-moving and driven individuals, taking the initiative sometimes meant being able to make decisions for the team as an individual. This meant that it was vital that we were on the same page with our goals and expectations from the project. Thus, we took more time than usual at the beginning of the project to plan our road-map moving forward, delegating work and defining our expectations clearly. This meant we were later to start coding and making PRs than other teams (as seen on reposense), which we felt was a more-than-fair trade-off for our team dynamic.
We hope you have enjoyed this journey and learnt as much as we have! - The ZeroToOne Team