Skip to content

Latest commit

 

History

History
960 lines (641 loc) · 52.2 KB

DeveloperGuide.adoc

File metadata and controls

960 lines (641 loc) · 52.2 KB

ZeroToOne - Developer Guide

1. Setting up

Refer to the guide here.

2. Design

2.1. Architecture

ArchitectureDiagram
Figure 1. Architecture Diagram

The Architecture Diagram given above explains the high-level design of the App. Given below is a quick overview of each component.

Main has two classes called Main and MainApp. It is responsible for,

  • 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.

  • UI: The UI of the App.

  • Logic: The command executor.

  • Model: Holds the data of the App in-memory.

  • Storage: Reads data from, and writes data to, the hard disk.

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.

LogicClassDiagram
Figure 2. Class Diagram of the Logic Component

How the architecture components interact with each other

The Sequence Diagram below shows how the components interact with each other for the scenario where the user issues the command exercise delete 1.

ArchitectureSequenceDiagram
Figure 3. Component interactions for exercise delete 1 command

The sections below give more details of each component.

2.2. Main Component

MainClassDiagram
Figure 4. Structure of the Main 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.

2.3. UI Component

UiClassDiagram
Figure 5. Structure of the UI Component

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.

2.4. Logic Component

LogicClassDiagram
Figure 6. Structure of the Logic Component

API : Logic.java

  1. Logic uses the ParserManager class to parse the user command

  2. The ParserManager creates and returns a Command object

  3. The Command object is executed by the LogicManager

  4. The command execution can affect the Model (e.g. adding a exercise).

  5. The result of the command execution is encapsulated as a CommandResult object which is passed back to the Ui.

  6. In addition, the CommandResult object can also instruct the Ui 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.

DeleteSequenceDiagram
Figure 7. Interactions Inside the Logic Component for the delete 1 Command
ℹ️
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.

2.5. Model Component

ModelClassDiagram
Figure 8. Structure of the Model Component

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>, and ObservableList<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.

2.6. Storage Component

StorageClassDiagram
Figure 9. Structure of the Storage Component

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.

2.7. Common Classes

Classes used by multiple components are in the seedu.zerotoone.commons package.

3. Implementation

This section describes some noteworthy details on how each features are implemented.

3.1. Session (Home)

3.1.1. Implementation

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.

StartStopActivityDiagram
Figure 10. Session Feature Activity Diagram

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 immutable CompletedWorkout object for saving and logging.

Further operations are exposed in the Model interface as Model#startSession() and Model#stopSession().

SessionModel
Figure 11. Session related models

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.

sessionExample
Figure 12. Example Workout Session

Step 1. The user selects an existing Workout called Arms Day with ID = 2 and starts a session with start 2.

  • Logic and Parser redirect control to StartCommand#execute() where an instance of OngoingWorkout is created with the Arms 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 calls Model#done() to obtain the last CompletedSet object to display on the UI, and statefully updates the instance of OngoingWorkout.

  • The rest timer resets to 00:00 and starts counting.

DoneCommandSequenceDiagram
Figure 13. DoneCommand sequence diagram
  • 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 of cs 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 of done except this time the instance of CompletedSet is created with isFinished set to false.

  • Since this is the final set of the session, OngoingWorkout#hasSetLeft() returns false, and OngoingWorkout#finish() is called which creates and returns an immutable CompletedWorkout object. This object is saved and passed on for use by the Log 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.

3.1.2. Design Considerations

Aspect: Seamless user-first experience
  • 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.

Aspect: Beginner-friendly set order
  • 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.

3.2. Exercise

3.2.1. Overview

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.

3.2.2. Implementation

The following portion will explain in-detail each component of the Exercise feature.

Model
ExerciseModelClassDiagram
Figure 14. Exercise Model Structure

The figure above depicts the Model of the Exercise feature. Starting from the most primitive type:

  1. NumReps stores the number of repetitions a person has to complete for a particular exercise set

  2. Weight stores the weight in kg that a person has to complete for a particular exercise set

  3. ExerciseSet represents an exercise set that a person has to complete. Each ExerciseSet comprises of exactly 1 NumReps object and 1 Weight object

  4. ExerciseName stores the name of a particular exercise

  5. Exercise represents a collection of exercise sets that a person has to complete. Each Exercise will consists of exactly 1 ExerciseName object and any number of ExerciseSet (including zero)

  6. UniqueExerciseList represents a collection of Exercise objects. There can be any number of Exercise objects in UniqueExerciseList, but they must be unique.

  7. ExerciseList implements the interface ReadOnlyExerciseList that ensures that the outward-facing exercise list is unmodifiable by other modules. The ExerciseList object has exactly 1 UniqueExerciseList for storage purposes.

  8. ExerciseList is controlled by the ModelManager.

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.

Storage
ExerciseStorageClassDiagram
Figure 15. Exercise Storage Structure

The Storage component provides the functionalities that enable the persistent storage of the model. Starting from the most primitive type:

  1. JacksonExerciseSet contains exactly two String objects, one for the number of repetitions and one for the weight. This class has a dependency to the NumReps and Weight model due to the toModelType() function. This function converts JacksonExerciseSet into the model’s ExerciseSet so that it can be used in other parts of the application.

  2. JacksonExercise contains exactly one String object that represents the exercise name, and any number of JacksonExerciseSet objects. Similarly, it has a dependency to ExerciseName, ExerciseSet and Exercise object due to the toModelType() function.

  3. JacksonExerciseList is the persistent storage for the ExerciseList model, and it contains any number of JacksonExercise objects.

  4. ExerciseListStorageManager implements the ExerciseListStorage which provides certain functionalities required for the storage to work properly. The ExerciseListStorageManager is controlled by the StorageManager.

Logic - Commands
ExerciseCommandClassDiagram
Figure 16. Exercise Commands Structure

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.

Logic - Parsers
ExerciseParserClassDiagram
Figure 17. Exercise Parser Structure

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.

Sample Command Execution

This section will illustrate an example of an exercise command execution using the input exercise create e/Bench Press.

CreateCommandSequenceDiagram
Figure 18. Exercise’s CreateCommand Sequence Diagram

In this portion, we will trace the sequence diagram of the exercise create command to better understand the internals of the Exercise feature.

  1. The user enters the command exercise create e/Bench Press

  2. LogicManager will pass the command to the ParserManager for parsing

  3. ParserManager upon seeing that the command is prefixed by exercise creates a ExerciseCommandParser

  4. ParserManager then pass create e/Bench Press to ExerciseCommandParser

  5. ExerciseCommandParser upon seeing that the command is prefixed by create creates a CreateCommandParser

  6. ExerciseCommandParser then pass the argument e/Bench Press to CreateCommandParser

  7. CreateCommandParser then attempts to create an ExerciseName object using the String in the argument

  8. Using the ExerciseName, CreateCommandParser then create a CreateCommand object with the exercise name

  9. The CreateCommand is then passed back to the LogicManager

  10. LogicManager calls c.execute()

  11. CreateCommand will attempt to create an Exercise using the exercise name

  12. After creating the Exercise object, the CreateCommand will attempt to store the new exercise by calling the addExercise method of Model

  13. After the exercise is successfully added, a CommandResult object is created

  14. This result is then passed back to the LogicManager which will display the output on the GUI

Summary
EditSetActivityDiagram
Figure 19. Editing Exercise Set Activity Diagram

At this point, you should have gather enough information to start developing the Exercise feature. As a summary, this is a sample Activity Diagram that depicts a user flow when they want to edit an exercise set.

3.2.3. Design Considerations

Parser Component

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.

Model Component

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.

3.3. Workout

3.3.1. Overview

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.

WorkoutActivityDiagram
Figure 20. Activity Diagram for Creating a Workout

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.

3.3.2. Components

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:

WorkoutClassDiagram
Figure 21. Class Diagram for Workout
  • WorkoutList implements the interface ReadOnlyWorkoutList

  • The WorkoutList is managed by the ModelManager

  • FilteredWorkoutList is an aggregation of one WorkoutList, to filter the view of the workout list shown to the user

  • WorkoutList composes of a UniqueWorkoutList

  • In turn, a UniqueWorkoutList contains any number of Workout objects

  • Each Workout is comprised of two things: a WorkoutName and any number of Exercise objects

    • WorkoutName contains the name of the Workout

    • Exercise is an exercise that belongs in a Workout

ℹ️
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”.

3.3.3. Workout Model

ZeroToOne’s Model extends the WorkoutModel. Here are all the functions to carry out workout-related activities:

  • Model#getWorkoutListFilePath() - Retrieves the Path of the WorkoutList

  • Model#setWorkoutListFilePath(Path workoutListFilePath) - Sets the Path of the WorkoutList

  • Model#setWorkoutList(ReadOnlyWorkoutList workoutList) - Sets the WorkoutList to be a ReadOnlyWorkoutList

  • Model#getWorkoutList() - Returns an unmodifiable ReadOnlyWorkoutList

  • 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 of ObservableList<Workout>

  • Model#updateFilteredWorkoutList(Predicate<Workout> predicate) - Updates the filter of FilteredWorkoutList to show filtered views of the list to the user

3.3.4. Storage of Workouts

WorkoutStorageClassDiagram
Figure 22. Class Diagram for Workout Storage
  1. The WorkoutListStorageManager implements the interface WorkoutListStorage. It also creates a JacksonWorkoutList.

  2. The JacksonWorkoutList composes of any number of JacksonWorkout objects.

  3. In turn, each JacksonWorkout composes of a String which is the workout name, and any number of JacksonExercise objects which are the exercises in each workout.

  4. The relationship of the Storage component to the Model component is also shown.

3.3.5. Parser for Workouts

I will go on to explain the Parser structure for the workout commands.

WorkoutParserClassDiagram
Figure 23. Class Diagram of the Parser for Workouts
  • The WorkoutCommandParser creates all Workout 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 the WorkoutExerciseCommandParser.

  • In turn, WorkoutExerciseCommandParser creates all workout Exercise related Parsers. These parsers allow the user to add, edit and delete said Exercise objects.

3.3.6. Sample Command Execution

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.

WorkoutSequenceDiagram
Figure 24. Sequence Diagram for Finding a Workout
  1. When the user runs the command workout find w/Arms Workout, the LogicManager will first take in the command, by calling the execute() function on it.

  2. The ParserManager then has to parse("workout find w/Arms Workout").

  3. Next, the WorkoutCommandParser has to parse("find w/Arms Workout") for the command.

  4. Once the command has been parsed as a FindCommand, it will be passed on to the FindCommandParser.

  5. The FindCommandParser can then create a FindCommand. This is constructed with the WorkoutName for "Arms Workout" (a low-level detail that has been abstracted from the diagram).

  6. The FindCommand can be returned to the LogicManager, where it will execute() the FindCommand.

  7. Model#updateFilteredWorkoutList(Predicate<Workout> predicate) is used to update the view of the workout list to show the requested Workout(s), using the PredicateFilterWorkoutName that is returned by the FindCommand (a low-level detail that has been abstracted from the diagram).

  8. Finally, the resulting output message will be returned as the CommandResult.

3.3.7. Design Considerations

Aspect: Exercises in workout
  • Option 1: Use existing Exercise class in Workout

    • 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 the Workout 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.

3.4. Schedule

3.4.1. Overview

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.

3.4.2. Components

The following class diagram shows the overview of the Scheduler:

ModelScheduleClassDiagram
Figure 25. Scheduler Class Diagram

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.

3.4.3. Schedule Model

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 populate SortedScheduledWorkoutList by querying every Schedule in SchedueList through method Schedule#getScheduledWorkout()

  • Model#getSortedScheduledWorkoutList() - Returns an unmodifiable view of ObservableList<ScheduledWorkout>

  • Model#deleteScheduledWorkout(ScheduledWorkout scheduledWorkoutToDelete) - Deletes the Schedule that generated the specified ScheduledWorkout from the schedule list, then re-populates SortedScheduledWorkoutList

  • 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 unmodifiable ReadOnlyScheduleList

3.4.4. Command Execution

Creating a Schedule

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.

ScheduleCreateSequenceDiagram
Figure 26. Sequence Diagram for Creating a Schedule
  1. When the user runs the command schedule create 1 d/2020-04-01 14:00, the LogicManager will first take in the command, by calling the execute() function on it.

  2. The ParserManager then has to parse("schedule create 1 d/2020-04-01 14:00").

  3. Next, the ScheduleCommandParser has to parse("create 1 d/2020-04-01 14:00") for the command.

  4. Once the command has been parsed as a CreateCommand, it will be passed on to the CreateCommandParser, which will then parse the index using ParserUtil, and parse the date and time using ScheduleParserUtil.

  5. The CreateCommandParser can then create a CreateCommand, which is then passed back to LogicManager to execute.

  6. When LogicManager executes the CreateCommand, it creates a new Schedule, and checks whether the model already contains this Schedule.

  7. If the model does not contain this Schedule, the LogicalManager will then add it to the model by calling Model#addSchedule(schedule).

  8. 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}
Populating the SortedScheduledWorkoutList

And the following sequence diagram shows how the ScheduledWorkoutList is populated after the method Model#addSchedule(schedule) is called:

PopulateSortedScheduledWorkoutList
Figure 27. Sequence Diagram for Populating SortedScheduledWorkoutList
  1. After the LogicManager calls the method Model#addSchedule(schedule), ModelManager calls Scheduler#addSchedule(schedule), which in turns calls ScheduleList#addSchedule(schedule) and adds the Schedule in the ScheduleList.

  2. The Scheduler will then populate the ScheduledWorkoutList by calling the method populateSortedScheduledWorkoutList().

  3. First, Scheduler gets the current date and time by calling DateTime#now().

  4. Then, Scheduler iterates through every Schedule in ScheduleList, and requests to get ScheduledWorkout by calling Schedule#getScheduledWorkout(now) on each of them.

  5. Finally, Scheduler resets the SortedScheduledWorkoutList with the latest ScheduledWorkout by calling ScheduledWorkoutList#setScheduledWorkouts(scheduledWorkouts), and user’s view of ScheduledWorkout is refreshed.

  6. 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.

3.4.5. Model Design Considerations

Aspect: How to handle editing and deletion of recurring schedules
  • Option 1: Model stores only ScheduledWorkout objects.

    • Pros: Easier to implement.

    • Cons: Harder to maintain when it comes to edit or delete of recurring scheduled workouts.

  • Option 2 (current choice): Saves Schedule objects that can be used to produce ScheduledWorkout 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!

3.5. Log Statistics

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.

3.5.1. Overview

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.

Architecture

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.

LogDisplayScreenshot
Figure 28. Log Display Screenshot
DataPointAbstractClass
Figure 29. DataPoint Abstract Class

This abstract class is then extended by concrete class as shown below.

StatisticsDataPointsClassDiagram
Figure 30. Example Concrete Data Points

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.

3.5.2. Implementation

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.

Execution Phase 1

The sequence diagram below describes the flow when the log display command is invoked by the user.

DisplayCommandSequenceDiagramPart1
Figure 31. Display Command Invocation Phase 1

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

ExecutionPhase 2

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.

DisplayCommandSequenceDiagramPart2
Figure 32. Example Display Command Invocation Phase 2

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.

3.5.3. Design Considerations

The following section will detail some of the design consideration and design choices we made while implementing the log statics feature.

3.5.4. Flexibility and Sensible Defaults

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.

LogDateRangeSensibleDefaults
Figure 33. Log sensible date range defaults
  • 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 and endRange 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 and endRange 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 and endRange 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

Defensive Programming

We decided to build these features with security at the forefront. This can be seen in

  • The use of optionals and ifPresent() functions guards against NullPointerExceptions when the user does not provide optional fields.

  • requireNonNull are frequently used to check for null values.

  • Strong exception handling when parsing startRange and endRange in the display command and throwing a ParseCommandException if date times provided are not valid in the specified format.

3.6. Configuration

Certain properties of the application can be controlled (e.g user prefs file location, logging level) through the configuration file (default: config.json).

4. Documentation

Refer to the guide here.

5. Testing

Refer to the guide here.

6. Dev Ops

Refer to the guide here.

Appendix A: Product Scope

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

Appendix B: User Stories

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

Appendix C: Use Cases

(For all use cases below, the System is the ZeroToOne and the Actor is the user, unless specified otherwise)

Use Case 1: Adding Exercises to Workout

MSS

  1. User requests for the existing exercises in the workout

  2. System shows a list of the existing exercises

  3. User requests to add an exercise to the workout

  4. 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

Use Case 2: Deleting Workouts

MSS

  1. User requests to list workouts

  2. System shows a list of all workouts

  3. User requests to delete a specific workout

  4. 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

Use Case 3: Find Past Workout by Keyword

MSS

  1. User requests a log of past workouts

  2. System shows a list of all past logged workouts

  3. User requests to find a past workout by keyword

  4. 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

Appendix D: Non Functional Requirements

  1. Should work on any mainstream OS as long as it has Java 11 or above installed.

  2. Should be able to hold up to 100 workouts without a noticeable sluggishness in performance for typical usage.

  3. 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.

  4. Should not depend on a remote server, so that a user can exercise in any condition or environment.

Appendix E: Glossary

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.

Appendix F: Instructions for Manual Testing

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.

F.1. Launch and Shutdown

  1. Initial launch

    1. Download the jar file and copy into an empty folder

    2. Double-click the jar file
      Expected: Shows the GUI with a set of exercises and workouts. The window size may not be optimum.

  2. Saving window preferences

    1. Resize the window to an optimum size. Move the window to a different location. Close the window.

    2. Re-launch the app by double-clicking the jar file.
      Expected: The most recent window size and location is retained.

F.2. Starting a workout session

  1. Starting a workout session with a WORKOUT_ID

    1. Prerequisites: At least 1 workout containing valid exercises and sets must exist.

    2. 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.

    3. 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.

    4. 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.

F.3. Saving a workout session

  1. Workout sessions are saved automatically at the end of a workout session.

    1. Prerequisites: A workout session must be ongoing.

    2. Test case: stop
      Expected: Workout session stops and displays in the status message. Timer resets to 00:00. The workout session should appear in the Log tab.

    3. Test case: skip, done
      Expected: Same as above, if skip or done is given on the last set.

    4. 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.

Appendix G: Effort

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:

  1. Statefully update new models during a workout session

  2. Apply useful statistics across an entire collection of stored data

  3. 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:

  1. Maintain > 70% test coverage

  2. Apply SOLID design principles

  3. 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