By: CS2113-T14-1
Since: Sept 2019
Licence: MIT
Welcome to the Dr. Duke developer guide! This document assumes a familiarity with the User Guide.
In order to be a successful Dr. Duke developer, you need a general understanding of:
-
Dr. Duke's architecture and object model
-
The Java framework which Dr. Duke builds on
-
Mark-up
is used to indicate a code literal. This can be amethod()
, aClass
, anENUM_VALUE
, orliteral input
. It is generally used when discussing concrete implementation details, as opposed to abstract ideas, e.g. "patient" refers to an actual human patient, whilePatient
refers to the Patient class, or an object thereof, in Dr. Duke.-
A method may be specified with its arguments:
method(String strArg, int intArg)
in order to let the reader know what arguments it takes, or to differentiate between two methods with the same name but different arguments. However, unless explicitly stated, amethod()
specified without its arguments does not imply that the method takes no arguments. A method may be referred to without arguments after being introduced with its arguments for brevity.
-
-
Italics are used when introducing a new term or concept.
Note
|
This box draws attention to quirk or caveat that may not be obvious. |
-
JDK
11
or above -
IntelliJ IDE
NoteIntelliJ by default has Gradle and JavaFx plugins installed.
Do not disable them. If you have disabled them, go toFile
>Settings
>Plugins
to re-enable them.
-
Fork this repo, and clone the fork to your computer
-
Open IntelliJ (if you are not in the welcome screen, click
File
>Close Project
to close the existing project dialog first) -
Set up the correct JDK version for Gradle
-
Click
Configure
>Project Defaults
>Project Structure
-
Click
New…
and find the directory of the JDK
-
-
Click
Import Project
-
Locate the
build.gradle
file and select it. ClickOK
-
Click
Open as Project
-
Click
OK
to accept the default settings -
Open a console and run the command
gradlew processResources
(Mac/Linux:./gradlew processResources
). It should finish with theBUILD SUCCESSFUL
message.
This will generate all resources required by the application and tests.
The Architecture Diagram shown above describes the high-level design of Dr. Duke. The application comprises 4 main components:
-
UI: Graphical User Interface (GUI) of the application
-
Parser Logic: Responsible for parsing and executing user commands
-
Model: Stores Dr. Duke’s data in-memory
-
Storage: Reads/writes data from/to persistent storage
This class diagram describes the relationships between the various core classes involved in parsing the user’s input into Command
s. The two highest-level components are the Parser
and the Executor
, both members of the CommandWindow
. They begin parsing when the user enters some input through the CommandWindow
.
The first word (delimited by a space or newline) of the user’s input is the command name. All commands extend the Command
class, which provides enough functionality for basic commands consisting of a single word. The operation of the Command
is specified in the CommandSpec
singleton it is constructed with, via the execute
method.
The mapping from the command name to the CommandSpec
should be created in the Commands
class, which is loaded by the default Parser
constructor. A Parser
can also be constructed with a subclass of Commands
to specify a different set of commands.
The Commands
class has a single function getCommand()
, which takes, as arguments, a String that should uniquely identify the requested CommandSpec
within a particular Context
, and a Context
enum representing the context from which getCommand()
was called. It then returns a new instance of the Command
, constructing it with the required CommandSpec
. The Parser
will supply the command name and the context
field in the DukeCore
instance to the getCommand()
method in its Commands
instance.
If the command requires an argument, the ArgParser
object in the Parser
will parse the rest of the input to determine the argument of the command, the switches supplied to it, and the arguments of the switches, and will set these parameters in the Command
. Finally, after the Command
has been constructed (and loaded with parameters if necessary), it is returned to the CommandWindow
. The Executor
class will then call the execute()
method of the command, supplying the DukeCore
object to the Command
, which will allow it to execute its operations.
This transaction is modelled by the following sequence diagram:
As seen in the class diagram above, Command
has a subclass ArgCommand
, which has a subclass ObjCommand
. Parallel to them are the abstract CommandSpec
, ArgSpec
and ObjSpec
classes, which follow a similar inheritance structure.
If a command has no arguments, it can be represented by a Command
configured with a CommandSpec
object. If it takes any arguments, it requires an ArgCommand
and an ArgSpec
. And finally, in the special case of commands where user input may be ambiguous, an ObjCommand
and ObjSpec
is used. The discussion of ObjCommand
s and how they facilitate disambiguation is left to the respective section.
Each CommandSpec
is a singleton, which defines an abstract getSpec()
method. This method is required to be implemented in its children, providing a means of enforcing the singleton pattern. For an ArgSpec
, the private constructor sets the parameters of the ArgCommand
: cmdArgLevel
(an ArgLevel
enum indicating whether an argument for the command is necessary, optional, or forbidden) and the data structures switchMap
and switchAliases
, generated by the switchInit()
function. The switchInit()
function takes a vararg of Switch
objects, which should specify the switches for the particular ArgSpec
. These parameters will be provided to the ArgParser
, which will use them to parse the user’s input.
switchMap
maps the full name of a switch to a Switch
object, describing its properties, and switchAliases
maps aliases to the full name of the switch they represent. An alias is a string that, when provided by the user as a switch, is recognised as a specific switch. For example, for the switch investigation
(given as -i[nv(x|estigation)]
in the User Guide) has the following aliases:
-
i
-
in
-
inv
-
invx
-
inve
-
inves
-
invest
-
investi
-
investig
-
investiga
-
investigat
-
investigati
-
investigatio
-
investigation
As this would be very tedious to list manually, it is automatically generated by the switchInit()
function, using the data in the Switch
objects provided to it. Observe that almost all these aliases are prefixes of the word investigation
, with the shortest being i
. This follows from the requirement that the switch can be recognised as long as the user has input enough characters for it to be unambiguous. Let i
in this example be the root, the shortest unambiguous part of the full name of the switch. Then, every prefix of the word investigation
starting from the root is an alias of the switch investigation
. All aliases of this form are generated by a loop in switchInit()
, from the root and the full name in the Switch
object. Any additional aliases can be supplied via the aliases
vararg in the Switch
constructor. Refer to the Javadoc of Switch
for further details on its fields.
Switch and argument values identified by ArgParser
are loaded into the ArgCommand
using the initArg()
and initSwitchVal
methods. These values are then accessed by the ArgSpec
from the ArgCommand
with the getSwitchVal()
method, which takes the name of a switch, as a String argument, and returns a String containing the argument supplied for the switch, and getArg()
;
When executing a command, the Command
's execute()
method is called. In a base Command
, this would directly call the execute()
method of the CommandSpec
. For an ArgCommand
, this would instead call executeWithCmd()
on the ArgSpec
, supplying the command to it. This stores a reference to the calling command in the ArgSpec
, allowing it to access its switch values during the execution.
This system is illustrated by the following sequence diagram:
Note that the "Parser Logic" abstraction represents the system of Parser
, ArgParser
, Commands
and Executor
.
This model of having Command
objects configured by configuration objects is somewhat unconventional, but it provides the benefit of enforcing the static initialisation of the switches, and facilitates testing - Command
, ArgCommand
and ObjCommand
are equipped with public constructors that can take in switch values and arguments, hence allowing us to set them up for testing without making the switch setters public, and without copying these constructors across every subclass (as constructors are not inherited).
-
Define a subclass of
CommandSpec
-
Specify its execution in
execute
ofCommandSpec
-
Define the private static field
spec
and the public static methodgetSpec()
to provide singleton behaviour -
Update
Commands
to link the command name to the newCommandSpec
If this command requires arguments, in addition to doing the above for a subclass of ArgSpec
(instead of CommandSpec
):
-
Create a private constructor for the subclass, and within the constructor:
-
Define
cmdArgLevel
-
Construct the switches for the command and supply them as arguments to
switchInit()
-
If there are no switches, call
switchInit()
with no arguments
-
-
Note
|
If there is no argument given for a switch, getSwitchVal(<switch name>) returns null . However, if a switch is not given, getSwitchVal(<switch name>) also returns null . The former case can be distinguished by the fact that switchVals will contain <switch name> as a key.
|
The Parser
object scans through a user-supplied input string. The first word is extracted, and if the corresponding command is an ArgCommand
, it uses a finite state machine (FSM) which switches on the characters in the input. Switches are extracted, using the aliases in switchAliases
to identify the full names of the corresponding switches. The switch arguments are then compared against the requirements of the ArgCommand
, as stored in the switchMap
.
The finite state machine for input parsing has the following states:
-
EMPTY
: parsing whitespace, which has no semantic meaning aside from serving as a separator -
ARG
: parsing an argument that is not quoted, which may be for a switch or for the command itself -
STRING
: parsing an argument that is surrounded by double quotes -
SWITCH
: parsing a switch name
The state transitions are as follows:
-
EMPTY
-
EMPTY
→EMPTY
: <Space> or <Newline> -
EMPTY
→SWITCH
:-
-
EMPTY
→STRING
:"
-
EMPTY
→ARG
: <any other character>
-
-
SWITCH
-
SWITCH
→EMPTY
: <Space> or <Newline> -
SWITCH
→SWITCH
(add current switch and begin processing a new switch):-
-
SWITCH
→STRING
(add current switch and begin parsing a string as an argument):"
-
SWITCH
→SWITCH
(add char to elementBuilder): <any other character>
-
-
STRING
-
STRING
→EMPTY
:"
-
STRING
→STRING
(add char to elementBuilder) : <any other character>
-
-
ARG
-
ARG
→EMPTY
: <Space> or <Newline> -
ARG
→DukeException
: Unescaped"
or-
-
ARG
→ARG
(add char to elementBuilder): <any other character>
-
Preceding any transition character with a backslash \
will escape it, allowing it to be treated as an ordinary character.
While in the ARG
, STRING
or SWITCH
states, each character that is read is added to a StringBuilder elementBuilder
. When exiting the state, the string is processed as a switch via addSwitch()
, or written as an argument to the Command
being constructed by writeElement()
. These functions also check if adding a switch or argument would be valid. This can be an argument for the Command
itself, or a switch argument. elementBuilder
is then cleared, and the parser continues parsing input characters.
These transitions are summarised in the following finite state diagram:
For more details on how switches are processed, see above on Command
objects, and on the Switch Autocorrect feature.
When every character in the input has been consumed, cleanup will be performed based on the state that the ArgParser
is in at that point:
-
EMPTY
: nothing is done -
ARG
: callwriteElement()
to write a command or switch argument -
SWITCH
: calladdSwitch()
to process the switch name -
STRING
: callwriteElement()
, assuming the user simply forgot to close the string
The ArgParser
also checks for the corner case of a switch without an argument at the end, in which case it attempts to write a null
value for the switch.
The Class Diagram shown above describes the relationship among the different data classes invloved in storing
information used in Dr. Duke
. The class is named after the object it represents. All objects extend the DukeObject
abstract class, which stores basic information to identify the object and its parent.
The DukeObject
class specifies several abstract functions crucial for the UI to access. All DukeObjects
also have a
parent
DukeObject which is transient and may be null. This is to facilitate storing in Gson and allow objects to
reference their parent if needed. A String
representation of DukeObjects
can be obtained using the toString
and
toReportString
methods.
The class diagram shown above shows the Patient
class and how it is stored.
Patients entered into our system are stored as Patient
objects in our PatientData
object. This can be converted to
Gson easily after accounting for abstract objects. All patients may have Impressions
associated with them which are
created by the Doctor’s impression of a Patient. This is supported with DukeData
objects as evidences or treatments.
The Patient
object should provide the following functionality:
* Input validation to ensure it stores valid input
* Sorting of Impressions
** Currently, Primary Impressions are also stored at the head of the impressions
list. If a future metric for assessing
importance of impressions are suggested by users, it can be added here as well.
* Filtered list of important critical DukeData
* Filtered list of uncompleted Treatments
which require follow ups
* Quick notes on the Patient
Impressions are what a doctor diagnoses a Patient of. Each impression may be supported by Evidences and associated with Treatments.
The Impression
object should provide the following functionality:
* Input validation
* Sorting of Treatments
High priorities are the first metric
Incomplete status requiring follow up is the second metric
* Sorting of Evidences
** High priorities are the first metric
The diagram above shows the DukeData class and its concrete implementations. The DukeData
objects represent evidence
and treatment recorded by the doctor.
To define new forms of DukeData
representing information on the Patient, extend DukeData
or its abstract subclasses
To define other types of data, extend DukeObject
.
If the class is abstract and needs to be stored, an adaptor implementing JsonSerializer
and JsonDeserializer
for it
needs to be created for Gson storage. Any circular referencing must be stored as transient but must be reinitialised at
launch.
Note
|
By convention, we store invalid values instead of null values to prevent nullptr exceptions. If there are
attributes that may be null, consider returning an empty object instead. E.g. for String , return "" .
|
This class diagram describes the relationship between the Storage class, GsonStorage
, the patient class, Patient
, and the other classes used to describe and handle patient data.
The storage/load mechanism is facilitated by GsonStorage
. GsonStorage
uses the Google-developed Java Library Gson 2.8.6
. Gson
is a library that can be used to convert Java Objects into their JSON
representation. It can also be used to convert JSON
representations back to the equivalent Java` Object. For more information about Gson
refer to the Gson
User Guide at https://github.com/google/gson/blob/master/UserGuide.md.
The JSON
representations of the patients are stored in a JSON
file called patients.json
.
GsonStorage
implements the following operations:
-
HashMap<String, Patient> loadPatientHashMap()
- Loads all the patients inpatients.json
to the hashmappatientObservableMap
-
void writeJsonFile(HashMap<String, Patient> patientMap)
- Creates an array containing the patients inpatientObservableMap
and writes the arraysJSON
representation topatients.json
-
String getFilePath()
- returns the filepath topatients.json
-
PatientMap resetAllData()
- Clearspatients.json
and returns an empty hash map
When the user boots Dr.Duke
a GsonStorage
and a PatientMap
object is created. The method loadPatientHashmap
in GsonStorage
is then executed which extracts all the JSON
representations of the patients in patients.json
as a string. The GSON
method fromJson()
is then executed on the JSON
representation of the patients which creates the equivalent java array contaning Patient
objects. The array is iterated through and every patient is loaded into the patientObservableMap
attribute of the PatientMap
object.
During runtime, every new patient that is created is stored in the patientObservableMap
.
When the user shuts down Dr.Duke
the patientObservableMap
is sent back to the GsonStorage
object by calling the writeJsonFile
method on the GsonSotrage
object. The writeJsonFile
method iterates through the patientObservableMap
and places every Patient
object in a java array. When all the patients are in the array the arrays JSON
representation is created using the Gson
method toJson()
. The context of the patient.son
file is then cleared and the new JSON
representation of the array containing all the patients is written to the patient.json
file which concludes the storage circle.
As can be seen in the class diagram, every individual’s patient’s data in nested from the Patient
object representing that patient. The diagram also displays that there are no circle references. For these two reasons, using Gson
to store all the data about the different patients is very convenient and effective as everything can be stored by simply creating the JSON
representations of each Patient
object and the rest of the nesting will be parsed automatically by the Gson
source code.
If further development of Dr.Duke
requires the storage of other objects that are nested from the patient objects that will be done automatically by the existing storage mechanism as long as there are no circle references. If further development requires storage of objects that are not nested from patient objects the storage mechanism needs to be updated to include two or more arrays instead of one; one containing the JSON
representations of the Patient
objects and the other/s containing the JSON
representation of the other object/s.
The UI
component for Dr. Duke is an abstract model/layer that exists independently in the application. It interacts with the other components (i.e. Model, Logic, Storage) of the application via a simple interface. It is designed to be easily expanded or modified by other developers with its liberal use of abstract classes. Hence, future developments such as the addition of Contexts
can be accomplished with ease.
The UI
component uses the JavaFX UI framework. The layout of these UI elements 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 Parser
component and listens for changes to Model
data so that the UI can be updated with the newly modified data.
The overall UI class diagram shown below is a good starting point to understand how the UI component is designed and constructed.
This overall class diagram aptly describes the relationships between the various core classes and packages of the UI component.
The UI component can be categorised into 3 main parts.
-
UiManager
-
Windows
-
Cards
The UI component is exposed to other components of Dr. Duke via the Ui
interface. The UiManager
implements this interface and acts as the manager of the UI component. UiManager
holds a reference to the MainWindow
(the primary UI window that houses the other UI elements that the application will use).
As mentioned, the MainWindow
houses various UI elements such as the CommandWindow
, ContextWindow
, and HelpWindow
. The MainWindow
holds a reference of the UiContext
object that exposes the current Context
(a core feature) of the application. The Context
of the application determines what UI window the ContextWindow
takes on, i.e. HomeWindow
for Home context, PatientWindow
for Patient context, etc. As Dr. Duke works with a huge number of contexts, the various context windows extend from ContextWindow
. This greatly enables the use of polymorphism when dealing with the context windows. Therefore, when implementing a new context, you, as the developer, should always inherit from ContextWindow
to display the context in GUI format.
The ContextWindow
houses the various cards shown in the figure above (corresponding to their respective context). These cards show an excerpt of the details of the DukeObjects
they represent. All cards extend from UiCard
. Hence, what has been mentioned with regards to polymorphism for ContextWindow
applies to UiCard
as well.
This section describes some noteworthy details on how certain features in Dr. Duke are implemented.
[[Feature-Context Mechanism]]
Dr. Duke aims to assist house officers in quick, accurate, and efficient recording and retrieval of patient data required to provide efficient care. On a day-to-day basis, house officers deal with a lot of information, ranging from the biometrics details of a patient to the investigation results of a blood test. Therefore, it would be really helpful if they are able to view these chunks of information in a very focused setting. This has inspired us to come up with the idea of Contexts
. In Dr. Duke, there are currently four main contexts. They are the HOME
, PATIENT
, IMPRESSION
, and TREATMENT AND EVIDENCE
contexts (listed in hierarchical order). The different contexts allow the house officers to focus on a particular patient or a particular impression of a patient at hand without being overloaded by other irrelevant information.
The Context
mechanism is facilitated by the UiContext
class. It implements the following operations:
-
UiContext#open(DukeObject)
- Opens and displays a context. -
UiContext#moveBackOneContext()
- Moves back one context. -
UiContext#moveUpOneContext()
- Moves up one in the hierarchy of contexts.
Given below is an example usage scenario and how the context mechanism behaves accordingly.
Step 1: The user launches the application. The application (UiContext
) starts out in the HOME
context. The user currently manages 3 patients.
Step 2: The user keys in "open 1" in the text field and presses the Enter key. At this point, the Parser
parses the input and passes a open
command to the Executor
for execution. This command invokes the context mechanism.
Step 3: The Context
mechanism first stores the current context (and the associated DukeObject
) in a stack (so it can still be accessed later when the user wishes to execute the back
command). Then, it updates the context to the PATIENT
context and retrieves the corresponding Patient
object as selected by the user.
Step 4: The UI
component of Dr. Duke listens to changes in the context of the application via an attached PropertyChangeListener
and updates the current context window from HomeContextWindow
to the PatientContextWindow
.
Step 5: The transition to the PATIENT
context is fully completed.
We could possibly adopt the same format used by most existing Electronic Health Record (EHR) system and put all information regarding a patient in a single display screen. This will, without a doubt, significantly simplify the internal workings of Dr. Duke. However, the application will become cluttered and unintuitive to the users.
In order to provide the smoothest experience and least delay to our users, we want to allow them to identify the targets of operations such as reading, updating and deleting with minimal effort. Given a clear, unambiguous identifier like an index in a list, this is straightforward, but if the user wishes to access something by part of its name, or by one of its attributes, and there are multiple objects matching his criterion, he needs some way to disambiguate between them. Having such a disambiguation system in place instead of rejecting ambiguous input (e.g. anything other than an exact name) or preventing it (e.g. access by index only) would improve the user’s experience and input speed by allowing more free-form input, without needing to worry so much if the input is of the correct form.
This system extends and generalises the search feature: instead of only being able to open objects from the search context, we are able to perform any other command on objects identified from a search. This is done by storing the original command before opening the search context. After the user selects a particular object, the system executes the original command again, with the identified object supplied to it. Commands that are capable of such operations are ObjCommand
s, and their behaviour is controlled by ObjSpec
s. This system allows the user to search for objects based on any attribute, to select a result from that search, and perform an arbitrary command on it.
A brief recap of Dr. Duke’s other systems is necessary here. All of Dr. Duke’s components can be accessed from the DukeCore
object. The DukeCore
object is supplied to a command whenever it is executed, as commands may require all these systems to function. In the DukeCore
, the PatientMap
holds all patients being managed. Patient
s, their Impression
s, and the Treatment
s and Evidence
s of the Impression
s are all DukeObject
s. Each DukeObject
can be viewed, and has an associated context which displays its information. "Viewing" null
would open the HOME
context, and would display all Patient
s in the PatientMap
. Searching in Dr. Duke is done by constructing a SearchResults
object, using a search method of the current DukeObject
being viewed. This will only find matching results that are the children of the DukeObject
, and that DukeObject
will be the the parent of the SearchResults
returned. These search methods populate the SearchResults
object through various strategies, such as matching all immediate children whose names contain the search term (findByName()
), matching all immediate children whose fields contain the search term (find()
), and matching any children whose fields contain the search term (searchAll()
). Refer to the individual objects' Javadocs to see what capabilities they offer.
Note
|
findByName() , find() and searchAll() refer generically to these strategies, rather than to specifc methods implementing them (which may have different names).
|
SearchResults
are constructed with a name, which is the search term used to populate it, a List
of DukeObject
s, which are the results of the search, and a parent DukeObject
, which indicates the scope of the search. SearchResults
can be combined using the addAll()
method.
ObjSpec
extends ArgSpec
to provide the method execute(DukeCore core, ObjCommand cmd, DukeObject obj)
, while ObjCommand
extends ArgCommand
to provide the method execute(DukeCore core, DukeObject obj)
, which calls the ObjSpec
execute
method, with itself as the cmd
parameter. Finally, ObjSpec
has an abstract executeWithObj(DukeCore core, DukeObject obj)
method, which specifies the operation of the command once the object in question has been identified.
When an ObjCommand
is executed via the regular execute(DukeCore core)
method, it first attempts to see if the object can be disambiguated without requesting for explicit intervention by the user, via the execute(DukeCore core)
method inherited by ObjSpec
. Although there are no constraints on how this is to be done, the typical ObjCommand
allows user input in either index or string form. If the user did not input an index, the ObjCommand
will typically perform a findByName()
search, as the user likely intends to select an object based on what is visible to him (which is primarily the name of the object). The typical behaviour detailed here is implemented in HomeObjSpec
, PatientObjSpec
, and ImpressionObjSpec
, which provide these behaviours in the specific contexts, using the functions in HomeUtils
, PatientUtils
and ImpressionUtils
respectively. These classes contain helper functions that can assist in the extraction of argument and switch values from typical commands in their respective contexts.
If there is only one result in the returned SearchResults
object (or if a valid index was supplied), then the command can be performed on that object without ambiguity, with a direct call wot executeWithObj
. If none are found, the command fails with an exception. However, if more than one result is found, then disambiguation is required. The ObjCommand
then calls search(SearchResults results, ObjCommand objCmd)
from the DukeCore
, which opens the SearchResults
in a search context, and stores the ObjCommand
, with its ObjSpec
and the switches in the ObjSpec
set, as queuedCmd
.
When viewing a SearchResults
object, the user can only issue one command (whose behaviour is specified by SearchSpec
), by selecting the index of the item he wishes to execute. This command, specified by SearchSpec
, calls executeQueuedCmd(DukeObject obj)
from the DukeCore
on the object identified. This method would then call the execute(DukeCore core, DukeObject obj)
of the stored queuedCmd
, providing the identified DukeObject
as an argument. The ObjCommand
thus gains access to the object selected by the user, clearing up the ambiguity and allowing the user’s desired operation to be executed.
This entire sequence of operations is summarised in this diagram (note that the UI and Parser have been abstracted into the DukeCore
object):
To summarise, in order to use ObjCommand
s:
-
Perform the steps for
ArgSpec
s. but using anObjSpec
instead -
In
execute(DukeCore core)
, if the user’s input is ambiguous as to which object it refers to,construct aSearchResults
object containing the possible candidates, and callsearch(SearchResults results, ObjCommand objCmd)
-
The
processResults()
method inObjSpec
will throw an exception if theSearchResults
object contains no objects, will callexecuteWithObj()
if there is only one object (using that object), and will callsearch()
if there is more than one object.
-
-
Implement the abstract method
executeWithObj(DukeCore core, DukeObject obj)
. All operations that actually affect the system should be inexecuteWithObj()
.
Possible alternatives to this system would be the strict use of indices or the requirement for full names to be provided, as discussed above. However, in addition to failing to provide the flexibility discussed above, this solution does not work as well because our users are likely to think primarily in terms of names when dealing with their data. Being able to access objects by part of a name instead of scrolling through a (potentially large) collection of objects to find an index or trying to remember an exact name would increase the speed at which they navigate through the app and provide input to it.
Another suggestion proposed was the use of switches to differentiate between the use of an index or a name. This was also rejected as differentiating the two is simple enough to do without needing switches to identify the type of input. It is also less natural: when the user wishes to view the details of a patient, for example, open Bob
is closer to a natural-language expression for this than open -n Bob
. Commands that are closer to natural language would allow the user to more quickly and efficiently translate his intentions into input, thereby enabling him to more quickly and fluidly input data.
Note
|
This feature was not completed in time for v1.4, although the logic can be found in the unused folder of our repository. The Damerau-Levenshtein function does not function as expected, and while the disambiguation was supposed to be integrated with the search system, it was later realised that, despite superficial similarities, a single system could not serve both purposes of providing input to a parsing process and providing data objects to a Command .
|
While rapidly adding different types of patient data, it is inevitable that typing mistakes will be made. While short forms of switches are accepted in order to minimise the amount of typing that needs to be done to organise information, and therefore the risk of mistakes being made, we still need to account for the cases where they occur. An automated means of correcting the text would allow these corrections to be made as quickly as possible and with minimal effort required from the user, reducing the disruption to his workflow caused by these mistakes.
If a user-supplied switch is not an alias for any switch, this triggers the disambiguation functions in CommandUtils
. We use a modified Levenshtein-Damerau distance which takes into account the taxicab distance between keys on a standard QWERTY keyboard in weighting the cost of substitutions. Pseudocode for the Levenshtein-Damerau distance computation can be found here and ideas for implementation of keyboard distance analysis are taken from here. This provides a realistic measure of the likelihood that a particular mistake was made, as the likelihood of accidentally pressing an incorrect key is dramatically decreased if the incorrect key in question is a keyboard’s length away from one’s intended key, which is a fact that the basic Levenshtein-Damerau distance algorithm fails to capture.
The distance of the ambiguous string to every alias whose length differs from the string’s by at most 2 is calculated. Basic pruning is implemented, terminating the distance estimation computation if it exceeds the minimum distance found so far.
If there is a switch with a unique lowest distance from the input string, that switch is automatically selected, with a warning shown to the user to indicate that his input was autocorrected. If not, the user is prompted with a screen listing the closest matches, as well as all valid switches for this command. The closest matches are numbered, and the user may select one by entering its corresponding number, or he may enter another valid switch in its full form.
Taxicab distance is used as opposed to Euclidean in order to avoid computing square roots, and only the substitution cost is affected by the keyboard distance, as having missed or accidentally added a character, or typing the characters out of sequence, is not dependent on the distance between two keys.
This function is called by the parser finite state machine whenever a complete switch that does not match any alias is processed, instead of presenting all combinations of possible corrections after the whole input is parsed. This allows mistyped switches to be individually and unambiguously corrected, instead of creating a confusing combinatorical explosion of possible switches if the user makes several mistakes in a complex query, some of which may have more than two close matches for a switch if the user had used their shortened forms.
The above diagram shows the information a search result will store and the SearchContext its displayed in.
The picture above is an example of a find
command.
Dr. Duke aims to assist House Officers in quick, accurate recording and retrieval of patient data required to provide efficient care. When more patients are added to the system and the system grows in size or the user want to directly access a piece of nested data we need a method to directly assess the data. Therefore, it makes sense to have a search function to search through the entire system or a subset of the system. Hence, a find feature is essential for users to quickly locate data or for disambiguation when it is unclear what the user wants to narrow down the possible options based on existing data in the system.
-
Reduce the time taken for the user to enter details of the Patient and navigate in the system.
-
Search a subset of the system or only for data of a certain type.
The search mechanism is facilitated by two main functions, namely contains
and find
.
contains
is a method every concrete component of the data model has. It is specific to the type of information stored
by the class. In our case, this facilitates searching for information by representing relevant attributes in String form
and checking if the search term is contained within.
find
method is included in every class that stores ArrayLists of other objects. It searches if an object contains a
search term by utilising the contains
method. Different flavours of the find
function is post fixed with information
on what its purpose is. For example, findImpressionsByName
searches only the name
field of Impression
objects.
The master find
function is searchAll
which searches through all related information from a particular object down.
Given below is an example usage scenario and how the search mechanism behaves at each step.
Step 1: The user launches the application and navigates to a particular patient context for example, John
. The TextField
in the
CommandWindow
is blank, and the context is Patient:John
. The user wishes to search John
for a particular piece of information
e.g. Fever (a sample valid command syntax is find Fever
).
Step 2: The find method will be called and all data related to the Patient will be searched for Fever
, It will display the results in a new
Context containing all impressions where John
had Fever
in a separate window
Step 3: The user can then select a particular impression and review the information or change the information if desired.
The discharge feature deletes a patient from Dr.Duke
and creates a .txt
report file where all data about the patient at the point of discharge is stored. These report files can be used to manually recreate a patient if a doctor wants to add a discharged patient back to Dr.Duke
. This feature also prevents Dr.Duke
from getting full as new patients come and go from the hospital using the same bed numbers. To be able to discharge a patient that is no longer at the hospital also enables quicker lookup of the patients that are at the hospital.
The discharge mechanism is facilitated by the Command
and ReportSpec
classes. ReportCommand
extends the Command
class and ReportSpec
extends the ArgSpec
class. Like every command, ReportCommand
has an execute
method. The execute
method is called upon when the user enters a "discharge" command followed by a valid bed number. The "discharge" command has the optional switch -sum
that enables the user to input a short discharge summary, for example, the reason why the patient is discharged and the date and time of the discharge. As the reports are stored in a text format the user can also add additional text to the report after the report has been created by simply writing new text to the report file with a text editor. The syntax of the “discharge” command is implemented in ReportSpec
using the Switch
class.
Given below is an example of what a discharge command with a discharge summary that follows the syntax could look like
-
discharge -b A12 -sum "Patient left the hospital, 2019-03-03 08:00"
The execute
method in ReportCommand
creates one report file for each discharged patient and places it in the “report” folder within the “data” folder. Every discharged patient file is named with the patient’s name and bed number separated by a -
. For example, if a patient named “Alexander Smith” with the bed number "A300" was discharged the file name would be AlexanderSmith-A300
.
The execute
method uses the FileWriter
class to write the report to the report file utilizing toReportString
which is a method that every DukeObject
implements. The toReportString
returns a string representation of every attribute that is not a null value and some other strings that make the report more reader-friendly.
A future consideration is to store the reports in PDF files instead of text files. This would be beneficial as it would decrease the risk of the user to accidentally change the reports while reading it. Using PDFs could also make the reports more reader-friendly for the user. A drawback of using PDFs is that it makes it harder for the user to add text to the reports after they have been created. Another future consideration is to automatically include the date and time of when each discharge in the reports.
Target user profile:
House officers, who are typically freshly-graduated medical students, play a vital role in managing hospital patients. They are responsible, among many other things, for collating all information regarding each hospital patient and organising it to provide a clear picture of the patient’s situation, and for presenting that picture to senior doctors who can then make assessments and recommendations based on that picture. As much of this information needs to be exchanged at a rapid pace, Dr. Duke assists in quick, accurate and efficient recording and retrieval of the patient data required to provide effective care.
The house officers we are targeting with this app:
-
need to manage a significant number of patients
-
need to quickly input and organise patient data
-
prefer desktop apps over other types
-
prefer typing over mouse input
-
can type fast
Value proposition:
-
input, organise and access information about patients faster than with 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… |
---|---|---|---|
|
house officer |
check my patients' allergies |
issue them with the appropriate medicine |
|
house officer who has to manage a lot of information |
flag and view the critical issues to follow up for each patient |
complete the follow-up(s) as soon as possible |
|
house officer who has to manage many patients |
view the previous medical history of my patients |
understand what has been done to manage/treat their conditions |
|
house officer who needs to input a lot of data quickly and is prone to mistyping |
be able to make typing errors but still have my input recognised |
avoid having to waste time to retype my command |
|
house officer who needs to input a lot of data quickly and is prone to mistyping |
confirm my input type and modify it quickly if it is incorrect |
avoid having to retype or tediously transfer entries that were input in the wrong place |
|
house officer who needs to upload records into the hospital’s health system |
generate unified reports that are fully compatible with the system |
avoid having to manually input those records |
|
house officer keeping track of information for my consultant |
keep track of whether or not I’ve checked for the results of certain investigations |
make sure the consultant is kept up-to-date |
|
house officer who has to manage a lot of information |
easily link new information and follow-up items to particular conditions |
have a clearer picture of each condition and its corresponding management plan |
|
house officer with a consultant that talks too fast |
differentiate the types of input with just a single control character |
avoid having to waste time switching between windows |
|
house officer who has to manage a lot of information |
easily view and navigate through data associated with particular conditions that particular patients have |
have a clearer view of what that particular condition is |
|
house officer who needs to input a lot of data quickly and is prone to mistyping |
undo my previous commands |
quickly rectify mistakes made when inputting data |
|
house officer who has to manage a lot of information |
search through all of the records of a patient |
find all the details relevant to a particular aspect of his/her care plan |
|
house officer who has to manage many patients |
easily view all critical issues all my patients are facing by level of importance |
address them as soon as possible |
|
house officer who needs to input a lot of data quickly and is prone to mistyping |
have my input automatically checked to ensure it is of the right format |
always be assured that I am inputting the right commands. |
(For all use cases, the System is Dr. Duke
and the Actor is the user
, unless specified otherwise)
MSS
-
User requests to add a patient.
-
Dr. Duke requests for details of the patient.
-
User enters the requested details.
-
Dr. Duke creates a new profile for the patient according to the specified details.
Use case ends.
Extensions
-
3a. Dr. Duke detects an error in the entered details.
-
3a1. Dr. Duke prompts the user with an error message and requests for the correct details.
-
3a2. User enters correct details.
-
Steps 3a1 and 3a2 are repeated until the given details are valid.
-
Use case resumes from Step 4.
-
MSS
-
User searches for the patient (UC-3).
-
Dr. Duke requests for new details of the patient.
-
User enters new details of the patient.
-
Dr. Duke updates the profile for the patient.
Use case ends.
Extensions
-
3a. Dr. Duke detects an error in the entered details.
-
3a1. Dr. Duke prompts the user with an error message and requests for the correct details.
-
3a2. User enters correct details.
-
Steps 3a1 and 3a2 are repeated until the given details are valid.
-
Use case resumes from Step 4.
-
MSS
-
User enters the patient’s name.
-
Dr. Duke returns list of all relevant results.
-
User selects the target patient in the list.
Use case ends.
Extensions
-
2a. The returned list is empty.
Use case ends.
MSS
-
User searches for the patient (UC-3).
-
Dr. Duke shows the detailed records of the patient.
Use case ends.
MSS
-
User searches for the patient (UC-3) and requests to discharge him/her.
-
Dr. Duke shows the details of the patient and requests for a confirmation.
-
User confirms that the patient may be discharged.
-
Dr. Duke generates a discharge report for the patient and delete his/her record from the system.
Use case ends.
Extensions
-
a. At any time, User chooses to cancel the discharge operation.
-
a1. Dr. Duke requests to confirm the cancellation.
-
a2. User confirms the cancellation.
Use case ends.
-
MSS
-
User searches for the patient (UC-3) and requests to generate a report on his/her current health condition.
-
Dr. Duke generates a detailed report for the patient.
Use case ends.
Preconditions: At least 1 command in the command history.
MSS
-
User requests to undo previous command(s).
-
Dr. Duke shows the list of command(s) to be reverted and requests for a confirmation.
-
User reviews the command(s) and confirms the undo operation.
-
Dr. Duke performs the undo operation and returns the system to an older state.
Use case ends.
-
The software should be portable, i.e. work on any mainstream OS as long as the OS has Java
11
or above installed. -
The software should be able to hold up to 500 patients without a noticeable reduction in performance for typical usage.
-
The software should work without internet access.
-
The software should have good user documentation, which details all aspects of the software to assist new users on how to use this software.
-
The software should have good developer documentation to allow developers to understand the design of the software easily so that they can further develop and enhance the software.
-
The software should be easily testable.
-
A user with an above average typing speed for regular English text should be able to accomplish most of his/her intended tasks faster using commands than using the mouse.
Given below are instructions to test the app manually.
Note
|
These instructions only provide a starting point for testers to work on; testers are expected to do more exploratory testing. |
Dr. Duke manages patients, who have impressions (diagnoses), each of which have various data types (treatments and evidence) associated with them. Each type of object is associated with a different context (screen) when viewed, and each context has different commands. The following commands will construct a basic setup for testing Dr. Duke’s capabilities.
-
Start in the
HOME
context. -
new -n Hello -a World -b A123
: create a new patient -
new -name Goodbye -allergies World -bed A321
: create another new patient, using different forms of the switches -
discharge Goodbye
: delete the patient, creating a report indata/reports
-
open Hello
: go to a more detailed view of the remaining patient, thePATIENT
context - the allergy "World" should be visible -
new -n "Test impression" -desc "Testing is fun"
: create a new impression -
new -name "Testing sucks" -description "I hate testing"
: create another new impression, using different forms of the switches -
delete Test
: attempt to delete an impression, will open disambiguation screen -
1
: deletes the impression that was indexed first -
edit -age 22
: edits the age of the patient, as shown in the top left -
open 1
: ambiguous, error message printed and nothing done -
open -im 1
: opens remaining impression in theIMPRESSION
context -
new -obs -n "Test observation" -summary "Just gotta keep testing" -sub -pri 1
: create new observation, which is a type of evidence. Observation card should indicate that it is subjective and critical. -
new -plan -n "Test plan" -summary "The tests do not end" -status "1"
: create new observation, which is a type of evidence. Plan card should indicate that it is in progress.-
Commands to edit data, move it, change its priority/status etc. are available in this context
-
-
open plan
: opens the "Test plan" plan to provide a more detailed view in thePLAN
context-
Use
up
orback
to exit from this level, and continue testing
-
-
Navigate to
PATIENT
context-
Prerequisites: Currently at
HOME
context, no previous contexts, and managing at least 1 patient. -
Test case:
open 1
Expected: Navigate successfully to thePATIENT
context. The GUI display updates from theHomeContextWindow
to thePatientContextWindow
.CommandWindow
prompts a message to notify the user that he/she has sucessfully navigated to thePATIENT
context. -
Test case:
up
Expected: Remains atHOME
context. The GUI display remains atHomeContextWindow
.CommandWindow
prompts a message to notify the user that no context transitions has taken place. -
Test case:
back
Expected: Remains atHOME
context. The GUI display remains atHomeContextWindow
.CommandWindow
prompts a message to notify the user that no context transitions has taken place.
-
-
Open search results
-
Prerequisites: Currently at
HOME
,PATIENT
orIMPRESSION
context, and managing at least 1 patient. -
Test case:
find a
Expected: Navigate successfully to theSEARCH
context, have results which contain the substring 'a'. Note that you will remain in the current context if noDukeObjects
which contain the data exist.CommandWindow
will feedback if the search is successful or not.
-
-
Discharge ambiguous patients
-
Prerequisites: Currently at
HOME
context, with two patients with similar names e.g. "John" and "Joe" in testing data. -
Test case:
discharge jo
Expected: Navigate successfully to theSEARCH
context, have the two similar patients there. -
Test case:
1
Expected: Patient labelled[1]
will be selected and discharged. User will be returned toHOME
context after successful discharge.
-
This functionality works for any command that takes string_or_idx as an input (see User Guide). Refer to Quick Start section to see how to construct other test cases.