By: Team SE-EDU
Since: Jun 2016
Licence: MIT
Refer to the guide here.
The Architecture Diagram given above explains the high-level design of Dukemon. Given below is a quick overview of each component.
💡
|
The .puml files used to create diagrams in this document can be found in the diagrams folder.
Refer to the Using PlantUML guide to learn how to create and edit diagrams.
|
-
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 Dukemon contains seven componenets.
-
UI
:
The Graphical UI of Dukemon that interacts with the user. -
AppManager
:
The buffer between the User and Dukemon’s internal components. -
Timer
:
The internal Timer that triggers events based on time elapsed. -
Logic
:
The main command executor and performer of operations. -
Model
:
Holds the non-game data in-memory. -
Game
:
Holds the data of live game sessions in-memory. -
Storage
:
Reads data from, and writes data to, the local hard disk.
For the components UI, Logic, Model, Timer, Storage and Game:
-
Defines its API in an
interface
with the same name as the Component. -
Exposes its functionality using a
{Component Name}Manager
class.-
ie.
StorageManager
implementsStorage
,GameTimerManager
implementsGameTimer
.
-
The Sequence Diagram below shows how the components interact with each other for the scenario where the user issues the command delete 1
.
The sections below give more details of each component.
API : Ui.java
The UI consists of a MainWindow
that is made up of parts e.g.CommandBox
, ResultDisplay
, PersonListPanel
, StatusBarFooter
etc. 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
AppManager
component. -
Listens for changes to
Model
data andTimer
through theAppManager
so that the UI can be updated correspondingly.
The AppManager
component serves as a Facade layer and communication hub between the internal components of Dukemon and the UI components.
Using this extra layer provides better abstraction between the UI
and the internal components, especially between the Timer
and the UI
.
AppManager
communicates with both the Logic
and Timer
components to send feedback to the UI
to display back to the user.
-
Gets feedback for commands by through
Logic
-
Starts and Stops the
Timer
when required. -
Makes call-backs to the
UI
to update variousUI
components. -
Initiates collection of
Statistics
by pulling data (eg. Time Elapsed) fromTimer
andLogic
.
API :
GameTimer.java
The Timer
consists of a GameTimer
that will keep track of time elapsed via an internal countdown timer
and notify the AppManager
, who will notify the UI
components.
-
Dealing with the internal countdown timer that runs during a game session.
-
Periodically triggering callbacks that will notify the
AppManager
component. -
Gets timestamps to trigger
Hints
via aHintTimingQueue
Due to the fact that the Timer
has to work closely with the UI
and AppManager
(without being
coupled directly), it is separated from the Logic
, Model
and Game
components.
This section breakdown the logic package into its internal components
Logic is primarily built by two segments: Command and Parser.
Command is an abstract class.
Four other abstract classes (HomeCommand, OpenCommand, GameCommand and SettingsCommand) extend Command.
Concrete Command classes with an execute method implementation extend one of the above four abstract classes.
ParserManager holds reference to a SpecificModeParser and a SwitchModeParser.
The SpecificModeParser changes based on current application mode.
Both of them hold references to all concrete Parser and Command Classes with the help of ClassUtil
Logic fulfils its contracts with other packages through two interfaces: Logic and UiLogicHelper
Examples of transactions promised by Logic API include command execution, command result and update statistics.
UiLogicHelper APIs is a subset of Logic APIs and only contains transactions for AutoComplete. It exposes the functionalities through the following getter methods:
-
List<AutoFillAction>#getMenuItems(String text)
— Gets an List of AutoFillActions to fill up AutoComplete display based on current user input given in text -
ModeEnum#getMode()
— Retrieves the application mode to display visually to the user (represented by enumeration object ModeEnum) -
List<ModeEnum>#getModes()
— Retrieves the possible modes the user can transition to from current mode
API :
Logic.java
UiLogicHelper.java
API : Model.java
The Model
,
-
stores a
UserPref
object that represents the user’s preferences. -
stores the Word Bank data.
-
exposes an unmodifiable
ObservableList<Card>
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 change. -
does not depend on any of the other three components.
The Game
component,
-
stores a shuffled
List<Card>
that is cloned/copied from aReadOnlyWordBank
. -
maintains an
Index
to keep track of the state of the game. -
has an associated
DifficultyEnum
that dictates the time allowed for each question. -
verifies
Guess
that are sent byLogic
(User’s guesses)
API : Storage.java
The Storage
component,
-
can save
UserPref
objects in json format and read it back. -
can save the Address Book data in json format and read it back.
The Statistics component includes 2 main subcomponents:
-
A
GlobalStatistics
, containing the user’s total number of games played and the number of games played in the current week. -
A
WordBankStatisticsList
, which is a collection ofWordBankStatistics
, one for eachWordBank
.
The class diagram of the Statistics component is shown below:
This section describes some noteworthy details on how certain features are implemented.
This section explains how the design choice of Dynamic Parsers fulfils AutoComplete and Command Execution.
ParserManager dynamically changes parser depending on current mode the game is at. When updating the User Interface for every keystroke, it ensures only the right commands get parsed and autocompleted at each moment.
-
ParserManager
instance has reference to aSwitchModeParser
andSpecificModeParser
-
When user enters a keystroke, the
SwitchModeParser
and/orSpecificModeParser
are accessed based on internal state. -
It updates the AutoComplete suggestions for every keystroke.
-
Internal State consists of booleans: gameIsOver, bankLoaded and enumeration ModeEnum: HOME, OPEN, GAME, SETTINGS
-
The above process is aided by
ClassUtil
to handle instantiation of Parser and Command objects.The state management is complex. The below sequence diagram demonstrates all possible workflows.
ℹ️
|
Home (No Switch) means HomeModeParser is used and SwitchModeParser is not used |
-
Command Execution through
Logic Interface
-
A String from Ui package gets to
ParserManager
and gets converted into aCommand
object which is executed by theLogicManager
. -
The command execution can affect the
Model
(e.g. adding a word meaning pair into wordbank). -
The result of the command execution is encapsulated as a
CommandResult
object which is passed back to theUi
andAppManager
. -
In addition, the
CommandResult
object can also instruct theUi
to perform certain actions, such as displaying help to the user.
-
-
AutoComplete through
UiLogicHelper Interface
The following sequence diagram shows how the AutoComplete operation runs when user keys in "st" into command box.
Alternative 1 |
Alternative 2 |
|
Aspect 1: |
Use java reflections to hold a List of Classes and iterate through them to pick the matching Classes |
Use switches in Parser to match Command Word to create Parser objects if necessary else directly create Command object. |
Why did we choose Alternative 1: |
||
Aspect 2: |
Using a ParserManager to dynamically switch between Parsers based on current state |
Use a single parser |
Why did we choose Alternative 1: |
AppSettings
was a class that was created to be integrated into the Model
of the app. It currently contains these functionalities:
-
difficulty [EASY/MEDIUM/HARD]
to change the difficulty of the game. -
hints [ON/OFF]
to turn hints on or off. -
theme [DARK/LIGHT]
to change the theme of the app. Currently only supporting dark and light themes.
This feature provides the user an interface to make their own changes to the state of the machine. The settings set by the user will also be saved to a .json
file under data/appsettings.json
.
The activity diagram below summarizes what happens in the execution of a settings command:
ℹ️
|
Take note that "mode" as defined in our project is the state in which the application is able to take commands specific to that mode. |
Given below is a step by step walk-through of what happens when a user executes a difficulty command while in settings mode:
Step 1:
Let us assume that the current difficulty of the application is "EASY". The object diagram above shows the current state of AppSettings
.
Step 2:
When the user enters difficulty hard
, the command gets passed into Ui first, which executes AppManager#execute()
, which passes straight to LogicManager#execute()
without any logic conditions to determine its execution path.
Step 3:
At LogicManager#execute()
however, the command gets passed into a parser manager which filters out the DifficultyCommand
as a non-switch command and it creates a DifficultyCommand
to be executed.
Step 4:
Upon execution of the DifficultyCommand
, the state of the model is changed such that the DifficultyEnum
in AppSettings
is now set to HARD
.
Step 5:
Since the main function of the difficulty
command is accomplished and all that is left is to update the ui, the CommandResult
that is produced by the execution of the command goes back to Ui
without much problem.
Step 6:
Assuming that there were no errors thrown during the execution of the difficulty
command, the execution calls updateModularDisplay
in UpdateUi
. In here, the ModeEnum.SETTINGS
is registered and it updates the settings display to properly reflect the change in difficulty.
The state of appSettings is then as follows:
There were a few considerations for implementing an interface that essentially allows users to touch a lot of parts of the application through settings and some of these methods break software design principles. These are the considerations we came across:
Alternative 1 |
Alternative 2 |
|
Aspect 1: |
Effecting the change inside the |
Effecting the change in the part of the architecture that the setting is affecting. E.g, Changing the theme inside Ui or changing the difficulty inside model |
Why did we choose Alternative 2: |
||
Aspect 2: |
Storing it inside the enumerations that make up the choices for the settings |
Storing it inside the classes that implement the settings |
Why did we choose Alternative 1: |
The Timer
component utilizes the java.util.Timer
API to simulate a stopwatch during a Game
. It also relies on
using Functional Interfaces as callbacks to periodically notify other components in the system. Using callbacks
allows the Timer
to enact changes in other components of the system without directly holding a reference to those
components.
Internally, the Timer
works by using the method java.util.Timer.schedule()
that will schedule java.util.TimerTasks
at a fixed rate.
An Observer Pattern is loosly followed between the Timer
and the other components. As opposed to defining an
Observable interface, the AppManager
simply passes in method pointers into the Timer
to callback when an
event is triggered. The AppManager
thus works closely with the Timer
as the main hub to enact changes based on
signals given by the Timer
.
ℹ️
|
To avoid
synchronization issues with the UI component, all
TimerTasks (such as requesting to refresh a component of the UI ) are forced to run on the JavaFX Application Thread using
Platform.runLater() .
|
The three main events that are currently triggered by the Timer
component which require a callback are:
-
Time has elapsed, callback to
AppManager
to update and display the new timestamp on theUI
. -
Time has run out (reached zero), callback to
AppManager
to skip over to nextCard
. -
Time has reached a point where
Hints
are to be given to the User, callback toAppManager
to retrieve a hint and display accordingly on theUI
.
The call-backs for each of these events are implemented as nested Functional Interfaces
within the GameTimer
interface, which is concretized via the GameTimerManager
.
This section describes the sequential flow of events in the life cycle of a GameTimer
object.
The UI
component first registers callbacks with the AppManager
, who then registers callbacks with
the Timer
component. Periodically, the Timer
will notify the AppManager
to perform tasks to notify
the UI
component. This is to provide better abstraction between the UI
and Timer
.
A GameTimer
instance is created by the AppManager
for every Card
of a Game
.
The AppManager
provides information regarding the duration in which the Timer
should run for, and whether
to trigger Hints
at the point when a GameTimer
instance is created.
There were a few considerations for designing the Timer
this way.
Alternative 1 |
Alternative 2 |
|
Aspect 1: |
Holding a reference to Pros: Cons: |
Using Functional Interfaces as Call-backs to notify components indirectly. Pros: Cons: |
Why did we choose Alternative 2: |
Dukemon, a flashcard app, requires a non-trivial implementation of a data structure to contain it’s information.
It comes along with a set of commands that either modifies it’s data, or modify the view.
These commands will then synchronise the data in storage, or update the model for viewing.
Lastly, there is a cool drag and drop feature for word banks, to transfer the files into and out of your computer.
Let’s begin by explaining some key terms:
A Card
contains a word and a unique meaning. (May contain tags)
CardCommands
work on Cards
.
A WordBank
contains multiple Cards
. (May contain tags)
WordBankCommands
work on WordBanks
.
A WordBankList
contains multiple WordBanks
.
Each time a CardCommand
or WordBankCommand
is executed, Storage
data is synchronised and
Model
gets updated automatically for UI
to retrieve updated information for user viewing.
A quick look at Card
and WordBank
as it is displayed through the UI
.
We start from the lowest level - Card
.
A Card
contains a unique id
, a word
, a unique meaning
, a set of tags
.
id
: for statistical tracking
word
: answer to the question
meaning
: the question that will appear in the game
tags
: optional tag to classify cards
ℹ️
|
Cards with the same meaning are duplicates, and is disallowed. |
Now the second level - WordBank
A WordBank
contains a UniqueCardList
and a unique name
.
UniqueCardList
: prevent duplicate cards
name
: unique name of the word bank
ℹ️
|
Internally, the UniqueCardList contains an observable list of Card .
This is so any changes to the cards gets updated in the Model and thus the UI automatically.
|
Now the third level - WordBankList
A WordBankList
contains a UniqueWordBankList
.
UniqueWordBankList
: prevent duplicate word banks
ℹ️
|
Internally, the UniqueWordBankList contains an observable list of WordBank .
This is so any changes to the word banks gets updated in the Model and thus the UI automatically.
|
In Dukemon, there is should only be one WordBankList
, which is created upon Storage
initialisation.
Model
holds a reference to that specific WordBankList
.
Now the integration - How these data structures are stored in Model
and Storage
.
A card command edits the cards within a particular word bank. Therefore it needs to make function calls through the
WordBank data structure.
A word bank command edits the word bank within that particular word bank list. Therefore it needs to make function calls through
the WordBankList data structure.
To have a better understanding of how these commands work, I will show you how these commands are structured in Logic
and then walk you through a Sequence Diagram of executing a particular command.
With the understanding of WordBankList
data structure, and how the Commands
are structured within Logic
,
I will now take you through what happens when a Command
is called.
For instance, CreateCommand
:
WordBankList
using WordBankCommand
through different componentsWe will see the case where the input: "create bank1" is valid.
-
It gets parsed by the ParserManager. Depending on the input, a specific
Command
is returned. In this case, aCreateCommand
object is instantiated. -
Depending on the type of Command object, execute() performs slightly different tasks. In this case, the execute method of
CreateCommand
checks inModel
to see if theWordBank
currently already exist. -
Relevant information is stored in
CreateCommandResult
and is returned back toLogicManager
. -
With the retrieved information and type of
CommandResult
, commandResult updates the storage through it’s method. -
The
Storage
abstracts away details and contains well-written methods, each to handle different cases ofCommandResult
. In this case, createWordBank is called. -
JsonWordBankListStorage
contains the abstracted details of how a commandResult should be handled. For aCreateCommandResult
, addWordBank and saveWordBank is called. -
In addWordBank method, it simply adds to the only WordBankList in the entire app. This
WordBankList
is the same instance as referenced byModel
. -
In saveWordBank method, an even lower level saveJsonFile function is called to write to the disk. This is performed through the common class:
JsonUtil
. -
In addWordBank method, it simply adds to the only WordBankList in the entire app. This
WordBankList
is the same instance as referenced byModel
. -
It returns void all the way back to
LogicManager
, and then success message is then passed back toAppManager
, then to theUI
to notify the user.
As much as a pro CLI user would love to type all the commands, I figured a good old drag and drop feature will save
the user lots of time.
It aims to streamline the process of sharing word banks with friends.
From HOME
mode, you can view WordBank
, then simply drag and drop a WordBank
, out of the application, into say,
your desktop, or chat applications.
From your computer, simply drag and drop a WordBank
json file into Dukemon’s HOME
page.
With the well designed WordBankList
data structure and it’s functions, drag and drop feature is simply an import and export
function call, linked by the JavaFX’s UI drag detection and drag dropped methods.
Alternative 1 |
Alternative 2 |
|
Aspect 1: |
Although WordBankList and WordBank have very similar structures, I made classes for each of them they
each contain a unique list: Pros: Cons: |
Create a generic data structure class, and let both WordBankList and WordBank extend it. Pros: Cons: |
Why did we choose Alternative 1: |
Alternative 1 |
Alternative 2 |
|
Aspect 2: |
One single large json file with word bank names as keys and it’s word bank data as values: Pros: Cons: |
In the default data folder, each word bank is stored as a json file. Pros: Cons: |
Why did we choose Alternative 2: |
Alternative 1 |
Alternative 2 |
|
Aspect 3: |
All types of commands extends a single abstract class Pros: Cons: |
Distinguishing Pros: Cons: |
Why did we choose Alternative 2: |
The work of the Statistics component can be neatly captured and explained using a common series of user actions when operating the app.
User action | Statistics work | UI Statistics updates |
---|---|---|
User opens the app. |
User’s |
User is shown their |
User selects a word bank. |
The selected |
|
User opens the selected word bank. |
In open mode, User is shown the |
|
User plays the game. |
A |
|
User finishes the game. |
|
|
We will discuss each step with its implementation details primarily on the statistics work.
When the user opens the app, their GlobalStatistics
and WordBankStatisticsList
are loaded into Model
by
MainApp
.
When the user selects a word bank, the selected WordBankStatistics
from the WordBankStatisticsList
is loaded
into Model.
It is necessary to set the active WordBankStatistics
in the Model
such that when the user opens the WordBank
, the
WordBankStatistics
can be found in Model
and shown in the UI.
In open mode, the user is shown the WordBankStatistics
of the opened word bank, which is set in Model
at step 2.
A GameStatisticsBuilder
is used to record user actions during the game.
When the user starts the game by calling a StartCommand
, the GameStatisticsBuilder
is initialized.
During the game, the GameStatisticsBuilder
is updated with every GuessCommand
or SkipCommand
made. It receives
the timestamp from the GameTimer
which also resides in AppManager
.
When the user finishes the game, a GameStatistics
is created from the GameStatisticsBuilder
. The GameStatistics
is shown to the user in the game result page.
The GameStatistics
is used to update its corresponding WordBankStatistics
, which is then saved to disk.
Additionally, the GlobalStatistics
is also updated and saved to disk.
There were some design considerations on implementing the statistics.
Alternative 1 |
Alternative 2 |
|
Aspect 1: |
Store in a separate file from the Example: Pros: Cons: |
Store Pros: Cons: |
Why we decided to choose Alternative 1: |
The user profiles could allow multiple users to use the same app and have different statistics tracked. This feature is a work in progress and will be delayed to v2.0.
We are using java.util.logging
package for logging. The LogsCenter
class is used to manage the logging levels and logging destinations.
-
The logging level can be controlled using the
logLevel
setting in the configuration file (See Section 3.9, “Configuration”) -
The
Logger
for a class can be obtained usingLogsCenter.getLogger(Class)
which will log messages according to the specified logging level -
Currently log messages are output through:
Console
and to a.log
file.
Logging Levels
-
SEVERE
: Critical problem detected which may possibly cause the termination of the application -
WARNING
: Can continue, but with caution -
INFO
: Information showing the noteworthy actions by the App -
FINE
: Details that is not usually noteworthy but may be useful in debugging e.g. print the actual list instead of just its size
Refer to the guide here.
Refer to the guide here.
Refer to the guide here.
Target user profile:
-
students
-
wants to learn new English words or definitions
-
can type fast
-
enjoys games
-
is reasonably comfortable using CLI apps
Value proposition: gamify learning experiences
Priorities: High (must have) - * * *
, Medium (nice to have) - * *
, Low (unlikely to have) - *
Priority | As a … | I want to … | So that I can… |
---|---|---|---|
|
teacher |
add, edit, and delete questions in the word banks |
make corrections on what my students are supposed to learn |
|
teacher |
give customised word banks and definitions |
can let my students practice specific problems. |
|
user |
list all my word banks |
|
|
user |
give titles to word banks |
recognise them better |
|
user |
delete word banks |
free up some memory when I don’t need it anymore |
|
user |
see the content of the word bank |
study beforehand/make changes |
|
young student |
trivia questions to be gamified |
enjoy the process |
|
student |
create my own question banks |
tailor fit to my learning |
|
computer science student |
have a manual of the commands available |
refer to them when I am lost |
|
frequent user |
easily access my most recently attempted question sets |
can quickly resume my revision |
|
studious student |
set and complete goals |
have something to work towards |
|
student |
see my test statistics |
track my progress/improvement |
|
student |
choose different kinds of time constraints |
can simulate exam conditions |
|
student |
categorise my question sets |
easily look for relevant materials |
|
student |
mark question sets as important/urgent |
know how to prioritise my revision |
|
module coordinator |
export lessons |
send to their students |
|
student |
share and compare my results with my classmates |
know where I stand |
|
student |
partition the trivia |
attempt questions that I’m comfortable with |
|
weak student |
have the option to see hints |
won’t get stuck all the time |
|
computer science student |
practise typing bash commands into the CLI |
strengthen my bash skills |
|
teacher |
export statistics |
can compare performance across different students |
|
computer science student |
customize my “terminal” |
changing themes/ background/ font size/ font colour, so that I feel comfortable working on it |
|
teacher |
protect tests with passwords |
let my students do them in lessons together when password is released |
|
teacher |
protect the files |
doesn’t get tampered when distributing to students |
|
student |
have smaller sized files |
have more space on my computer |
{More to be added}
(For all use cases below, the System is the Dukemon
and the Actor is the user
, unless specified otherwise)
MSS
-
User requests to list persons
-
Dukemon shows a list of persons
-
User requests to delete a specific person in the list
-
Dukemon deletes the person
Use case ends.
Extensions
-
2a. The list is empty.
Use case ends.
-
3a. The given index is invalid.
-
3a1. Dukemon shows an error message.
Use case resumes at step 2.
-
{More to be added}
-
Should work on any mainstream OS as long as it has Java
11
or above installed. -
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.
-
Users can export and import their word banks or statistics.
{More to be added}
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 sample contacts. 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.
-
{ more test cases … }
-
Deleting a person while all persons are listed
-
Prerequisites: List all persons using the
list
command. Multiple persons in the list. -
Test case:
delete 1
Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. -
Test case:
delete 0
Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. -
Other incorrect delete commands to try:
delete
,delete x
(where x is larger than the list size) {give more}
Expected: Similar to previous.
-
{ more test cases … }