How to handle the getting of information from Model

  softwareengineering

I’m doing a Java software engineering team project in school and my team has decided on a Finance Tracker application, which contains the main components of Expense, Budget, Statistics, Suggestions and GUIPanels. Currently, the application only allows actions(add, delete, edit) on an expense to revolve around a primary budget and the statistics will also be tied to this primary budget. This is inspired from the git branching model that is available in the command line.

I’m in charge of the Statistics component and I am facing a problem right now. One of my Statistics commands requires to filter a list of expenses in that budget from a start date to end date, and it is intuitive to think that I will need users to specify both of them before parsing. However, I want to give users more configurations, such as not specifying either or even specifying neither, as I can always refer to the start date and end date of a budget that the command is tied to instead of throwing an error message.

The problem comes in creating the signatures for the classes involved in command execution. The code heavily follows the Command Design Pattern. Currently how a regular command is executed is following the order below:

LogicManager –> Parser –> Command creation –> Model access –> UI.

However, I’m finding difficulty finding a good time to fish the period or any information from the Budget class, which belongs to the Model component.

My current implementation of the Command class requires a start date and end date to be fed into the only constructor of the Command class, i.e Cmd#init(sd,ed) which means the information must be known as the Parser class, the logic component. But it might give the Parser unintended access about the Model.

I explored the option (1):
Making static attributes and static methods on a singleton class of the Model to access a budget objects period, but this also seems to be breaking the abstraction.

I want to know if it is good practice in null into the same Cmd#init(sd,ed), in the event one of them is not specified the user and only wait till the Model is accessed to handle this logic? I’m open to any other feedback for Code Quality as well.

Attached below are the CommandParser and the Command code for the one of the commands showing the current code and how I attempted to resolve getting of period using (1).

Command class

package seedu.address.logic.commands.statistics;

import static java.util.Objects.requireNonNull;
import static seedu.address.logic.parser.CliSyntax.PREFIX_END_DATE;
import static seedu.address.logic.parser.CliSyntax.PREFIX_START_DATE;

import seedu.address.logic.commands.Command;
import seedu.address.logic.commands.CommandGroup;
import seedu.address.logic.commands.CommandResult;
import seedu.address.model.Model;
import seedu.address.model.expense.Timestamp;
import seedu.address.ui.StatsPanel;

/**
 * Calculates statistics for the application
 */
public class StatsCommand extends Command {

    public static final String COMMAND_WORD = "stats" + CommandGroup.GENERAL;

    public static final String MESSAGE_SUCCESS = "Pie Chart calculated!";

    public static final String MESSAGE_USAGE = COMMAND_WORD
            + ": Calculates statistics between the Start Date and End Date "
            + "Parameters: "
            + "[" + PREFIX_START_DATE + "START_DATE] "
            + "[" + PREFIX_END_DATE + "END_DATE] "
            + "nExample: " + COMMAND_WORD + " "
            + PREFIX_START_DATE + "11-11-1111 "
            + PREFIX_END_DATE + "12-12-1212 ";

    private final Timestamp startDate;
    private final Timestamp endDate;

    /**
     * Creates an StatsCommand to calculate statistics between 2 dates {@code Timestamp}
     */
    public StatsCommand(Timestamp startDate, Timestamp endDate) {
        requireNonNull(startDate);
        requireNonNull(endDate);

        this.startDate = startDate;
        this.endDate = endDate;
    }

    @Override
    protected void validate(Model model) {
        requireNonNull(model);
    }

    @Override
    public CommandResult execute(Model model) {
        requireNonNull(model);
        model.calculateStatistics(COMMAND_WORD, startDate, endDate, null, false);
        return new CommandResult(MESSAGE_SUCCESS, false, false, StatsPanel.PANEL_NAME);
    }

    @Override
    public boolean equals(Object other) {
        return other == this //short circuit if same object
                || (other instanceof StatsCommand // instance of handles nulls
                && startDate.equals(((StatsCommand) other).startDate)
                && endDate.equals(((StatsCommand) other).endDate));
    }
}

Command Parser class

package seedu.address.logic.parser;

import static seedu.address.commons.core.Messages.MESSAGE_DISPLAY_STATISTICS_WITHOUT_BUDGET;
import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
import static seedu.address.commons.core.Messages.MESSAGE_REPEATED_PREFIX_COMMAND;
import static seedu.address.logic.parser.CliSyntax.PREFIX_END_DATE;
import static seedu.address.logic.parser.CliSyntax.PREFIX_START_DATE;

import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

import seedu.address.logic.commands.statistics.StatsCommand;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.budget.UniqueBudgetList;
import seedu.address.model.expense.Timestamp;
import seedu.address.model.statistics.Statistics;

/**
 * Parses input arguments and creates a new StatsCommand object
 */
public class StatsCommandParser implements Parser<StatsCommand> {


    public static final List<Prefix> REQUIRED_PREFIXES = Collections.unmodifiableList(List.of(
            PREFIX_START_DATE, PREFIX_END_DATE
    ));
    public static final List<Prefix> OPTIONAL_PREFIXES = Collections.unmodifiableList(List.of());

    /**
     * Parses the given {@code String} of arguments in the context of the StatsCommand
     * and returns an StatsCommand object for execution.
     * @throws ParseException if the user input does not conform the expected format
     */
    public StatsCommand parse(String args) throws ParseException {
        if (UniqueBudgetList.staticIsEmpty()) {
            throw new ParseException(MESSAGE_DISPLAY_STATISTICS_WITHOUT_BUDGET);
        }

        ArgumentMultimap argMultimap =
                ArgumentTokenizer.tokenize(args, PREFIX_START_DATE, PREFIX_END_DATE);
        if (!argMultimap.getPreamble().isEmpty()) {
            throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatsCommand.MESSAGE_USAGE));
        }
        if (hasRepeatedPrefixes(argMultimap, PREFIX_START_DATE, PREFIX_END_DATE)) {
            throw new ParseException(MESSAGE_REPEATED_PREFIX_COMMAND);
        }

        Timestamp startDate = null;
        Timestamp endDate = null;

        boolean isStartPresent = argMultimap.getValue(PREFIX_START_DATE).isPresent();
        boolean isEndPresent = argMultimap.getValue(PREFIX_END_DATE).isPresent();
        if (isStartPresent && isEndPresent) {
            checkStartBeforeEnd(argMultimap);
            startDate = ParserUtil.parseTimestamp(argMultimap.getValue(PREFIX_START_DATE).get());
            endDate = ParserUtil.parseTimestamp(argMultimap.getValue(PREFIX_END_DATE).get());
        } else if (isStartPresent) {
            startDate = ParserUtil.parseTimestamp(argMultimap.getValue(PREFIX_START_DATE).get());
            endDate = startDate.createForwardTimestamp(UniqueBudgetList.getPrimaryBudgetPeriod());
        } else if (isEndPresent) {
            endDate = ParserUtil.parseTimestamp(argMultimap.getValue(PREFIX_END_DATE).get());
            startDate = endDate.createBackwardTimestamp(UniqueBudgetList.getPrimaryBudgetPeriod());
        } else {
            //possibly further develop on the BudgetWindow structure, which uses the current window
            Timestamp centreDate = Timestamp.getCurrentTimestamp();
            endDate = centreDate.createForwardTimestamp(UniqueBudgetList.getPrimaryBudgetPeriod());
            startDate = centreDate.createBackwardTimestamp(UniqueBudgetList.getPrimaryBudgetPeriod());
        }

        return new StatsCommand(startDate, endDate);
    }

    /**
     * Parses the given {@code String} of arguments in the context of the StatsCommand
     * Checks that start date is before the end date of the given {@code ArgumentMultimap}
     *
     * @throws ParseException if the detected start date is after the end date
     */
    private void checkStartBeforeEnd(ArgumentMultimap argMultimap) throws ParseException {
        Timestamp startDate = ParserUtil.parseTimestamp(argMultimap.getValue(PREFIX_START_DATE).get());
        Timestamp endDate = ParserUtil.parseTimestamp(argMultimap.getValue(PREFIX_END_DATE).get());
        if (endDate.isBefore(startDate)) {
            throw new ParseException(Statistics.MESSAGE_CONSTRAINTS_END_DATE);
        }
    }

    /**
     * Returns true if none of the prefixes are repeated
     * {@code ArgumentMultimap}.
     */
    private static boolean hasRepeatedPrefixes(ArgumentMultimap argumentMultimap, Prefix... prefixes) {
        return !(Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getAllValues(prefix).size() <= 1));
    }
}

Your Parser exists to take the input and turn it into a new Command. Your Parser should be able to create a Command that has a start date, end date, both, or neither.

The Command is responsible for updating the Model and returning some result, which might be some sort of successful result or some sort of failed result. The execution of the Command might involve determining start and/or end date if one has not been provided, and it can do that by asking the Model.

Some java-like pseudocode:

class Parser {

    public Command parse(String[] args) {
        Command c = new Command();
        if (args.size() > 0) {
            c.setStartDate(args[0]);
            if (args.size() > 1) {
                c.setEndDate(args[1]);            
            }
        }
        return c;
    }

    public static void main(String[] args) {
        Command c = new Parser().parse(args);
        Result r = c.execute();
        UI.render(r);
    }

}
class Command {

    private Date startDate = null;
    private Date endDate = null;

    public Command() {
        //no op
    }

    public void setStartDate(Date d) {
        this.startDate = d;
    }

    public void setEndDate(Date d) {
        this.endDate = d;
    }

    public Result execute() {
        Result r = new Result();
        r.add(ExpenseFinder.find(startDate == null ? Budget.DEFAULT_START_DATE : startDate,
                endDate == null ? Budget.DEFAULT_START_DATE : endDate));
        return r;
    }

}

1

LEAVE A COMMENT