Patient Manager Developer Guide
Table of Contents
- Introduction
- Setting up the project in your computer
- Design & Implementation
- Implementation
- Appendix A: Product scope
- Appendix B: User Stories
- Appendix C: Non-Functional Requirements
- Appendix D: Glossary
- Appendix E: Instructions for Manual Testing
- Appendix F: Command Summary
Introduction
Patient Manager is a Command Line Interface (CLI) application for general practitioners (GPs) who work in polyclinics to manage their patient list. This includes managing patient list, recording/retrieval of past record of visit, and some other features listed below.
With the Patient Manager, GPs will be able to reduce paperwork and have a more efficient way to organize the records of their patients.
Setting up the project in your computer
First, fork this repo, and clone the fork into your computer.
If you plan to use IntelliJ IDEA (highly recommended):
- Configure the JDK: Follow the guide IntelliJ IDEA: Configuring the JDK @SE-EDU/guides and ensure IntelliJ is configured to use JDK 11.
- Import the project as a Gradle project: Follow the guide
IntelliJ IDEA: Importing a Gradle project @SE-EDU/guides
to import the project into IDEA.
Note: Importing a Gradle project is slightly different from importing a normal Java project.
- Verify the setup: Run
seedu.duke.PatientManager
and try a few commands. - Run the tests to ensure they all pass.
Application Design
Architecture
The Architecture Diagram shown above gives a high-level explanation of Patient Manager. Given below is a brief overview of each component.
Main
contains the class PatientManager
.
This class is responsible for:
- When the app is launched: Initializing the other components in the correct sequence and connecting them with each other
- When the app exits: Shuts down the components and invokes clean-up methods where necessary
Commons
contains constants and functions that are shared across multiple classes.
UI
is responsible for displaying the all messages generated by Patient Manager to the screen.
Logic
parses and executes commands.
Model
contains the data of Patient Manager in memory.
Storage
reads data from, and writes data to, the hard disk.
Interaction Among Architecture Components
The Sequence Diagram below shows how the components interact with each other for the scenario where the user issues the
command add S1234567D
.
The sections below give more details for each component.
UI Component
API: Ui.java
The UI
component implements methods for:
- reading user input and handling control characters (e.g. End-Of-File) appropriately
- displaying responses from command execution and error messages in a standardized manner
Logic Component
API: Parser.java
and Command.java
-
Logic
uses theParser
class to tokenize and parse the user command. - This creates a
Command
object which is then executed by thePatientManager
class via theexecute()
method. - The command execution can affect the
Model
(e.g. adding a patient). - Within the
execute()
method, theCommand
object can instruct theUi
to perform certain actions, such as displaying the command output to the screen
Detailed explanations of the implementation of each Command
subclass can be found
in Section 4: Implementation.
Model Component
API: Data.java
Data
- stores a
SortedMap<String, Patient>
, which maps the patient’s NRIC/FIN number to their correspondingPatient
instance - implements methods to add new patients and delete existing patients
- implements methods to load an existing patient’s medical records
A Patient
contains:
- the patient’s NRIC/FIN number, which uniquely identifies the patient
- a
TreeMap<LocalDate, Record>
which maps the patient’s consultation dates to the visit records for that date
A Record
contains:
- all the symptoms recorded by a GP during the consultation
- all the diagnoses made by a GP during the consultation
- all the prescriptions made by a GP during the consultation
- the most recently added symptom/diagnosis/prescription, which corresponds to the most recently executed
record
command
Storage Component
API: Storage.java
The Storage
component is responsible for:
- facilitates the saving of application data into a text file
- facilitates the loading of application data from the aforementioned text file
- convert records to string
- converts string to records
-
Storage
is first initialized with theSortedMap<String, Patient>
from the Data class during object creation. - After initialization, the
save(SortedMap<String, Patient> patientData)
method can be called to save the records to a file. The path of the output file is specified by the variable,FILE_PATH
, in theConstants
class. - The reverse process is the
load()
method. This method reads the contents from the file located atFILE_PATH
, and returns aSortedMap<String, Patient>
, which can be loaded into theData
constructor during program initialization.
Exception Component
API: BaseException.java
and its subclasses
BaseException.java
:
- handles all exceptions that occur during the execution of Patient Manager
- can report an error message, prompting the user to provide a syntactically correct command
- may also report the cause of error for debugging purposes
Each subclass of BaseException
:
- has at most one component that is dependent on it (e.g
InvalidInputException
and theUI
component) - contains an enumeration of error messages specific to that component
More details on the specific implementation of each subclass can be found at Section 4.3: Exception Handling.
Common Classes
There are two common classes, Constants
and Common
.
seedu.duke.Constants
class stores constants used by multiple classes. This includes help and exception messages, magic
numbers, delimiter for save file parsing, etc.
seedu.duke.Common
class have a number of static methods shared by multiple command classes. For example, it
includes isValidID()
for checking the validity of an NRIC/FIN number.
Implementation
This section describes some noteworthy details on how certain details are implemented.
Parsing User Input
The parser is one of the core components in charge of parsing all user input commands into program-understandable commands and arguments. For the ease of expansion of this program’s functionality as well as for its testability, reflection is used to invoke commands.
First is the initialization of this parser. A Ui
instance and a Data
instance is passed and stored. This is
important as these two will be passed to logic components (command classes) later.
Then, we can parse a user-input string by passing it to parse()
. We use an example of this:
record 01/05/2021 /s coughing, fever /p panadol Paracetamol 500mg*20
This is broken into a few steps:
- Check whether the command contains any forbidden characters. Currently, they are these characters:
~ ` % # @ !
- Initialize an empty hashmap, called
arguments
. - Tokenize using any number of consecutive white spaces.
- Taken out the first token as command, i.e.
record
. Push it into the hash map using keycommand
. Create a new empty list with default keypayload
. - Check if next token starts with
/
. No, so we add it to the list:list = ['payload']
. - Check if next token starts with
/
. Yes, so we concatenate all tokens in the list to one string use delimiter ` ` (empty whitespace). Put it into the hash map using the keypayload
. Reset the list, and set new key tos
(the part after this/
). - Repeat same process, we have
list = ['coughing,']
- Repeat same process, we have
list = ['coughing,', 'fever']
- Same process,
coughing, fever
is pushed into arguments hash map with keys
. Reset the list, and new key set top
. - …
At the end, we have an argument hashmap like this:
Key | Value |
---|---|
command | record |
payload | 01/05/2021 |
s | coughing, fever |
p | Panadol Paracetamol 500 mg*20 |
Initializing Command Class
Continuing from the command parsing above. Next step is the initialization of a command class. Since we have
command record
, the program finds a class called RecordCommand
under the module seedu.duke.command
(first character being capitalized, then concatenated with ‘Command’).
Since this is a valid command, this class exists. If the class does not exist, it means the command is not yet implemented by this program.
After finding the command class, it is initialized with (ui, data, arguments)
. ui
and data
are the two references
passed in when initializing the parser, and the arguments
is the hash map we just obtained by parsing the input. The
result of the initialization (i.e. the instance of the command class) is returned.
Since all command classes implements the abstract method execute()
, the main loop just need to execute this method to
call out the actual logic of this command.
Note: Since we are tokenizing the user input with any number of white spaces and concatenate all tokens belong to the same key back using single whitespace, the number of white spaces input has no effect on the actual arguments being parsed. For example, the following two input has exactly the same result after being parsed.
record 01/05/2021 /s coughing, fever record 01/05/2021 /s coughing, fever
Adding Patients
Adding of patients is implemented via AddCommand
, which is created by the Parser.parse()
method. As
per Section 4.1: Parsing User Input, the arguments to the command are stored in
a HashMap<String, String>
and passed to the AddCommand
during initialization.
Below is a sequence diagram when the user executes the command add S1234567D
. For clarity, arguments are
excluded from some function invocations.
Internally, the addPatient
method will check if the requested patient exists, and throw an error if the patient
already exists. Otherwise,Data
will create a new Patient
object, and add that to the HashMap<String, Patient>
of
registered patients.
Loading Patients
Loading of patients is implemented via LoadCommand
, which is created by the Parser.parse()
method. As
per Section 4.1: Parsing User Input, the arguments to the command are stored in
a HashMap<String, String>
and passed to the LoadCommand
during initialization.
Below is a sequence diagram when the user executes the command add S1234567D
. For clarity, arguments are
excluded from some function invocations.
Internally, the loadPatient
method will check if the requested patient exists, and throw an error if the patient
does not exist. Otherwise,Data
will update the current loaded patient to the requested patient.
Adding Medical Records to Patients
Adding medical records to a patient is implemented via RecordCommand
, which is created by the Parser.parse()
method.
As per Section 4.1: Parsing User Input, the arguments to the command are stored in
a HashMap<String, String>
and passed to the RecordCommand
during initialization.
Below is a sequence diagram when the user executes the command record /s coughing
. For clarity, arguments are
excluded from some function invocations.
Internally, the addRecord
method will first verify that there is a loaded patient, and it will also verify that at
least one of the three fields (symptoms, diagnosis and prescription) is not null and not blank, before adding the
medical record(s) to the patient.
Retrieving a Patient’s Medical Records
Loading of patients is implemented via the RetrieveCommand
, which is created by the Parser.parse()
method. As
per Section 4.1: Parsing User Input, the arguments to the command are stored in
a HashMap<String, String>
and passed to the RetrieveCommand
during initialization.
Below is a sequence diagram when the user executes the command retrieve
. For clarity, arguments are
excluded from some function invocations.
Internally, the getRecords
method will first verify that there is a loaded patient before trying to load their
records.
Exception Handling
All unexpected behaviour encountered by Patient Manager is signalled and handled with exceptions. Since the generic
Exception
is too broad, we have created a few custom exception classes to relay exception information.
BaseException.java
:
- inherits from the generic
Exception
- base class of all custom exceptions
- overwrites the
toString()
method to make it output messages more meaningfully
InvalidInputException.java
- inherits from
BaseException
- is used to handle all unexpected user input, like invalid commands, wrong NRIC numbers, etc.
- implies that user should re-enter correct command and arguments
- has a member enumerate (
enum
)Type
to give a fixed set of exception messages, which can be passed as the argument for exception initialization (see example below)
StorageException.java
- inherits from
BaseException
- is used to handle expected events occur during loading and saving data from/onto the hard disk
- shows that usual saving/loading action cannot be done, and there might be the case of a data loss after closing the program
UnknownException.java
- inherits from
BaseException
- is used to handle unusual events that should not be trigger by user
- signals an internal error of the program and should be fixed during next iteration or through hotfixes
During invocation of an exception, there are two ways to invoke:
throw new InvalidInputException(InvalidInputException.Type.EMPTY_STRING);
// e is a Throwable, e.g. a captured exception in a try-catch block
// for this UNKNOWN_COMMAND, the e should be of type ClassNotFoundException
throw new InvalidInputException(InvalidInputException.Type.UNKNOWN_COMMAND,e);
If a second argument is passed, it is called the cause of the exception. For example, the user’s wrong input
triggers ClassNotFoundException, and then this exception is captured in Parser
which then causes
InvalidInputException
.
This cause is stored for debugging purposes, and it will not be printed out to the user. The implementation of this facilitates breakpoint debugging during development.
Organization of the Model Component
In the Model
component, the Data
class acts as a Facade for the Patient
and Record
classes. As such, if a
command requires to make some modifications to the Patient
or Record
as part of its execution, it will have to do so
via method(s) implemented in the Data
class. However, since the Patient
and Record
are implemented seperately from
the Data
class, all their methods are still exposed as public methods. In theory, one could still bypass the Data
class and directly interface Patient
and Record
classes.
One alternative solution to prevent this is to implement Patient
and Record
as nested classes within the Data
class, and then making all their methods private. This would allow Data
to access their methods while preventing other
classes from doing the same. However, since Java does not have support for seperately defining and defining classes, all
the definitions would have to be included in the Data
class, making the codebase much larger and harder to read.
As such, we have opted to seperate these three classes individual files, and rely on the developers’ to exercise their
due discretion to not directly interface with the Patient
and Record
classes, but implement and utilize the
necessary methods in the Data
class.
Appendix A: Product scope
Target user profile
The target users for this application are general practitioners (GP) who work in clinics. They are keen to reduce the paperwork that is required of them during consultation sessions, so that they may focus more on the consultation itself. Also, they would like to have a more efficient way to organize the records of their patients.
Value proposition
Through Patient Manager, general practitioners are able to manage patients faster than a typical mouse/GUI driven app. The typical paperwork, such as recording of symptoms, diagnoses and prescriptions, are greatly reduced through digital input.
Appendix B: User Stories
Version | As a … | I want to … | So that I can … |
---|---|---|---|
v1.0 | GP in a polyclinic | add a new patient | record a patient |
v1.0 | GP in a polyclinic | view the list of patients | track the list of patients |
v1.0 | GP in a polyclinic | select a specific patient’s records | access the patient’s records |
v1.0 | GP in a polyclinic | add new record for a patient | refer to them during future consultations |
v1.0 | GP in a polyclinic | retrieve the patient’s past records | refer to them during the current consultation |
v1.0 | new User | view list of available commands | refer to them if I have any problems |
v2.0 | GP in a polyclinic | delete a patient | remove patients are no longer required to be tracked |
v2.0 | GP in a polyclinic | delete a patient’s records | remove records that I no longer need |
v2.0 | GP in a polyclinic | know if I entered an invalid Patient ID | make sure no mistake is made recording the patient’s ID |
v2.0 | GP in a polyclinic | load and save existing data | work on the data on another device |
Appendix C: Non-Functional Requirements
- Should work on any mainstream OS as long as it has Java 11 or above installed.
- Should be able to hold up to 1000 patients without a noticeable sluggishness in performance for typical usage.
- A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse.
- The data should be stored locally and should be in a human editable text file.
- The application should work without requiring an installer.
- The application should be at most 100 MB in size.
- The application should not rely on any remote server, or database management system.
Appendix D: Glossary
- Mainstream OS - Windows, Linux, and OS-X platforms.
- General Practitioner - A doctor based in the community who treats patients with minor or chronic illnesses and refers those with serious conditions to a hospital. Their duties are not confined to specific organs of the body, and they have particular skills in treating people with multiple health issues.
- Visit Record - Details taken down by the doctor during one’s visit. In this case, Patient Manager can record the patient’s symptoms, the diagnosis made by the doctor, and any prescriptions or referrals given.
- NRIC/FIN Number - A unique identification number for all Singaporean citizens, Permanent Residents and Long-term Pass Holders. The number begins with a single character (S, T, F or G), followed by 7 numeric digits, and ends with a checksum letter.
Appendix E: Instructions for Manual Testing
Launch, Help and Shutdown
- Initial launch
- Download
PatientManager.jar
and copy into an empty folder. - Open a terminal/Command Prompt (cmd)/PowerShell. A Windows 10 OS’ screenshot is here:
- Execute
java -jar PatientManager.jar
to start the Patient Manager.
Expected: Shows the welcome message as shown below
- Download
- View help
- Test case:
help
Expected: Application prints out a help message containing a list of valid commands and how to use them. - Test case:
help add
Expected: Application prints out a help message explaining only theadd
command.
- Test case:
- Exiting
- Test case:
exit
Expected: Application prints goodbye message and exits. All data will be saved topm.save
in the same folder asPatientManager.jar
.
- Test case:
Adding and Loading Patients
- Adding a new patient
- Test case:
add S1234567D
Expected: Application adds patient to the list and shows:---------------------------------------------------------------------- Patient S1234567D has been added! ----------------------------------------------------------------------
- Test case:
- Loading a patient’s records
- Prerequisite: Patients have already been added (in this case, S1234567D has already been added).
- Test case:
load S1234567D
Expected: Application loads S1234567D’s records and shows:---------------------------------------------------------------------- Patient S1234567D's data has been found and loaded. ----------------------------------------------------------------------
- Deleting a patient
- Prerequisite: Patients have already been added (in this case, S1234567D has already been added).
- Test case:
delete /p S1234567D
Expected: Application deletes patient S1234567D and shows:---------------------------------------------------------------------- Patient S9841974H has been deleted! ----------------------------------------------------------------------
Adding, Viewing and Deleting a Patient’s Visit Records
- Adding visit records
- Prerequisite: Patient’s records have already been loaded.
- Test case:
record 30/03/2021 /s coughing, runny nose, fever /d flu /p panadol, cetirizine
Expected: Application adds details to patient’s visit record and shows:---------------------------------------------------------------------- Added new record to patient S1234567D: Symptom: coughing, runny nose, fever Diagnosis: flu Prescription: panadol, cetirizine ----------------------------------------------------------------------
- Viewing visit records
- Prerequisite: Patient’s records have already been loaded.
- Test case:
retrieve
Expected: Application shows details of all the patient’s past visits:---------------------------------------------------------------------- Here are S1234567D's records: 30/03/2021: Symptoms: coughing, runny nose, fever Diagnoses: flu Prescriptions: panadol, cetirizine ----------------------------------------------------------------------
- Deleting visit records
- Prerequisite: Patient’s records have already been loaded.
- Test case:
delete /r 30/03/2021
Expected: Application deletes record dates 30/03/2021 and shows:---------------------------------------------------------------------- Record for 30/03/2021 has been deleted! ----------------------------------------------------------------------
Saving Data
- Missing data files
- Delete the file
pm.save
, which should be in the same folder asPatientManager.jar
. - Launch the app with
java -jar PatientManager.jar
. - Expected: Application should start up without any data.
- Delete the file
Appendix F: Command Summary
Listed below are all currently implemented commands in alphabetical order. For a more detailed explanation and input/output samples, please refer to the User Guide.
Command | Usage |
---|---|
add | add IC_NUMBER |
current | current |
delete(patient) | delete [/p IC_NUMBER] |
delete(record) | delete [/r DATE] |
exit | exit |
help | help [OPTIONAL_COMMAND]... |
list | list |
load | load IC_NUMBER |
record | record [DATE] [/s SYMPTOM] [/d DIAGNOSIS] [/p PRESCRIPTION] |
retrieve | retrieve [DATE] |