Skip to content

Latest commit

 

History

History
781 lines (529 loc) · 55.8 KB

DeveloperGuide.adoc

File metadata and controls

781 lines (529 loc) · 55.8 KB

Dr. Duke - Developer Guide

1. Introduction

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

1.1. Notation

  • Mark-up is used to indicate a code literal. This can be a method(), a Class, an ENUM_VALUE, or literal input. It is generally used when discussing concrete implementation details, as opposed to abstract ideas, e.g. "patient" refers to an actual human patient, while Patient 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, a method() 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.

2. Setting up

2.1. Prerequisites

  1. JDK 11 or above

  2. IntelliJ IDE

    Note
    IntelliJ by default has Gradle and JavaFx plugins installed.
    Do not disable them. If you have disabled them, go to File > Settings > Plugins to re-enable them.

2.2. Setting up the project in your computer

  1. Fork this repo, and clone the fork to your computer

  2. Open IntelliJ (if you are not in the welcome screen, click File > Close Project to close the existing project dialog first)

  3. Set up the correct JDK version for Gradle

    1. Click Configure > Project Defaults > Project Structure

    2. Click New…​ and find the directory of the JDK

  4. Click Import Project

  5. Locate the build.gradle file and select it. Click OK

  6. Click Open as Project

  7. Click OK to accept the default settings

  8. Open a console and run the command gradlew processResources (Mac/Linux: ./gradlew processResources). It should finish with the BUILD SUCCESSFUL message.

This will generate all resources required by the application and tests.

3. Design

3.1. Architecture

Architecture Diagram
Figure 1. Architecture Diagram

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

3.2. Parser Logic [JOHN CUTHBERT KHOO TENG FONG]

john1

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:

command

3.2.1. Command s and CommandSpec s

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:

argcommand

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

In summary, to define a new command:

  1. Define a subclass of CommandSpec

  2. Specify its execution in execute of CommandSpec

  3. Define the private static field spec and the public static method getSpec() to provide singleton behaviour

  4. Update Commands to link the command name to the new CommandSpec

If this command requires arguments, in addition to doing the above for a subclass of ArgSpec (instead of CommandSpec):

  1. Create a private constructor for the subclass, and within the constructor:

    1. Define cmdArgLevel

    2. Construct the switches for the command and supply them as arguments to switchInit()

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

3.2.2. Parsing

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

    • EMPTYEMPTY: <Space> or <Newline>

    • EMPTYSWITCH: -

    • EMPTYSTRING: "

    • EMPTYARG: <any other character>

  • SWITCH

    • SWITCHEMPTY: <Space> or <Newline>

    • SWITCHSWITCH (add current switch and begin processing a new switch): -

    • SWITCHSTRING (add current switch and begin parsing a string as an argument): "

    • SWITCHSWITCH (add char to elementBuilder): <any other character>

  • STRING

    • STRINGEMPTY : "

    • STRINGSTRING (add char to elementBuilder) : <any other character>

  • ARG

    • ARGEMPTY: <Space> or <Newline>

    • ARGDukeException: Unescaped " or -

    • ARGARG (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:

fsm

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: call writeElement() to write a command or switch argument

  • SWITCH: call addSwitch() to process the switch name

  • STRING: call writeElement(), 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.

3.3. Data Model [KWOK KUIN EK JEREMY]

DataModel
Figure 2. Class Diagram

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.

3.3.1. Patient

PatientModel
Figure 3. Class Diagram

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

3.3.2. Impression

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

3.3.3. DukeData

DukeDataModel
Figure 4. Class Diagram

The diagram above shows the DukeData class and its concrete implementations. The DukeData objects represent evidence and treatment recorded by the doctor.

3.3.4. Extension

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

3.3.5. Design Considerations

We considered how we should store our collections of objects and how we should update our UI when designing our model.

DataModelComparison

3.4. Storage System [JACOB TORESSON]

ClassDiagramData

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 in patients.json to the hashmap patientObservableMap

  • void writeJsonFile(HashMap<String, Patient> patientMap)- Creates an array containing the patients in patientObservableMap and writes the arrays JSON representation to patients.json

  • String getFilePath()- returns the filepath to patients.json

  • PatientMap resetAllData()- Clears patients.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.

3.5. UI [PANG JIA JUN VERNON]

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.

ui overall
Figure 5. Overall UI class diagram

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

ui windows
Figure 6. UI Windows class diagram

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.

ui cards
Figure 7. UI Cards class diagram

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.

4. Feature Implementation

This section describes some noteworthy details on how certain features in Dr. Duke are implemented.

[[Feature-Context Mechanism]]

4.1. Context Mechanism [PANG JIA JUN VERNON]

4.1.1. Rationale

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.

4.1.2. Implementation

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.

Sequence diagram for Context mechanism

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.

4.1.3. Comparison with Alternatives

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.

4.2. Object Disambiguation [JOHN CUTHBERT KHOO TENG FONG]

4.2.1. Rationale

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.

4.2.2. Implementation

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):

objcommand

To summarise, in order to use ObjCommand s:

  1. Perform the steps for ArgSpec s. but using an ObjSpec instead

  2. In execute(DukeCore core), if the user’s input is ambiguous as to which object it refers to,construct a SearchResults object containing the possible candidates, and call search(SearchResults results, ObjCommand objCmd)

    1. The processResults() method in ObjSpec will throw an exception if the SearchResults object contains no objects, will call executeWithObj() if there is only one object (using that object), and will call search() if there is more than one object.

  3. Implement the abstract method executeWithObj(DukeCore core, DukeObject obj). All operations that actually affect the system should be in executeWithObj().

4.2.3. Comparison with Alternatives

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.

4.3. Switch Autocorrect [JOHN CUTHBERT KHOO TENG FONG] [v2.0]

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.

4.3.1. Rationale

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.

4.3.2. Implementation

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.

4.3.3. Comparison with Alternatives

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.

SearchDiagram

The above diagram shows the information a search result will store and the SearchContext its displayed in.

SearchExample

The picture above is an example of a find command.

4.4.1. Rationale

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.

4.4.2. Proposed Implementation

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.

4.4.3. Alternatives

  • ChainSearching

    • Pros: We can instead use the toString method to search. Simpler to implement and maintain.

    • Cons: However, this may include unnecessary information. Java String have a character limit of 2147483647. If any String is very long, it may have overflow.

4.5. Discharge patients and create discharge reports [JACOB TORESSON]

4.5.1. Rationale

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.

4.5.2. Implementation

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.

4.5.3. Alternatives considerations

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.

Appendix A: Product Scope

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

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…​

* * *

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.

Appendix C: Use Cases

(For all use cases, the System is Dr. Duke and the Actor is the user, unless specified otherwise)

Use case: UC1 - Add a patient

MSS

  1. User requests to add a patient.

  2. Dr. Duke requests for details of the patient.

  3. User enters the requested details.

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

Use case: UC2 - Edit a patient’s details

MSS

  1. User searches for the patient (UC-3).

  2. Dr. Duke requests for new details of the patient.

  3. User enters new details of the patient.

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

Use case: UC3 - Search for a patient

MSS

  1. User enters the patient’s name.

  2. Dr. Duke returns list of all relevant results.

  3. User selects the target patient in the list.

    Use case ends.

Extensions

  • 2a. The returned list is empty.

    Use case ends.

Use case: UC4 - View a patient’s records

MSS

  1. User searches for the patient (UC-3).

  2. Dr. Duke shows the detailed records of the patient.

    Use case ends.

Use case: UC5 - Discharge a patient

MSS

  1. User searches for the patient (UC-3) and requests to discharge him/her.

  2. Dr. Duke shows the details of the patient and requests for a confirmation.

  3. User confirms that the patient may be discharged.

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

Use case: UC6 - Generate a unified report for a patient

MSS

  1. User searches for the patient (UC-3) and requests to generate a report on his/her current health condition.

  2. Dr. Duke generates a detailed report for the patient.

    Use case ends.

Use case: UC7 - Undo previous command(s)

Preconditions: At least 1 command in the command history.

MSS

  1. User requests to undo previous command(s).

  2. Dr. Duke shows the list of command(s) to be reverted and requests for a confirmation.

  3. User reviews the command(s) and confirms the undo operation.

  4. Dr. Duke performs the undo operation and returns the system to an older state.

    Use case ends.

Appendix D: Non Functional Requirements

  1. The software should be portable, i.e. work on any mainstream OS as long as the OS has Java 11 or above installed.

  2. The software should be able to hold up to 500 patients without a noticeable reduction in performance for typical usage.

  3. The software should work without internet access.

  4. The software should have good user documentation, which details all aspects of the software to assist new users on how to use this software.

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

  6. The software should be easily testable.

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

Appendix E: Glossary

Mainstream OS
  • Windows

  • macOS

  • Linux

Appendix F: Instructions for Manual Testing

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.

F.1. Quick Start

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.

  1. Start in the HOME context.

  2. new -n Hello -a World -b A123: create a new patient

  3. new -name Goodbye -allergies World -bed A321: create another new patient, using different forms of the switches

  4. discharge Goodbye: delete the patient, creating a report in data/reports

  5. open Hello: go to a more detailed view of the remaining patient, the PATIENT context - the allergy "World" should be visible

  6. new -n "Test impression" -desc "Testing is fun": create a new impression

  7. new -name "Testing sucks" -description "I hate testing": create another new impression, using different forms of the switches

  8. delete Test: attempt to delete an impression, will open disambiguation screen

  9. 1: deletes the impression that was indexed first

  10. edit -age 22: edits the age of the patient, as shown in the top left

  11. open 1: ambiguous, error message printed and nothing done

  12. open -im 1: opens remaining impression in the IMPRESSION context

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

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

    1. Commands to edit data, move it, change its priority/status etc. are available in this context

  15. open plan: opens the "Test plan" plan to provide a more detailed view in the PLAN context

    1. Use up or back to exit from this level, and continue testing

F.2. Navigating between contexts

  1. Navigate to PATIENT context

    1. Prerequisites: Currently at HOME context, no previous contexts, and managing at least 1 patient.

    2. Test case: open 1
      Expected: Navigate successfully to the PATIENT context. The GUI display updates from the HomeContextWindow to the PatientContextWindow. CommandWindow prompts a message to notify the user that he/she has sucessfully navigated to the PATIENT context.

    3. Test case: up
      Expected: Remains at HOME context. The GUI display remains at HomeContextWindow. CommandWindow prompts a message to notify the user that no context transitions has taken place.

    4. Test case: back
      Expected: Remains at HOME context. The GUI display remains at HomeContextWindow. CommandWindow prompts a message to notify the user that no context transitions has taken place.

F.3. Searching Dr. Duke

  1. Open search results

    1. Prerequisites: Currently at HOME, PATIENT or IMPRESSION context, and managing at least 1 patient.

    2. Test case: find a
      Expected: Navigate successfully to the SEARCH context, have results which contain the substring 'a'. Note that you will remain in the current context if no DukeObjects which contain the data exist. CommandWindow will feedback if the search is successful or not.

F.4. Disambiguating Objects

  1. Discharge ambiguous patients

    1. Prerequisites: Currently at HOME context, with two patients with similar names e.g. "John" and "Joe" in testing data.

    2. Test case: discharge jo Expected: Navigate successfully to the SEARCH context, have the two similar patients there.

    3. Test case: 1 Expected: Patient labelled [1] will be selected and discharged. User will be returned to HOME 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.