With the recent introduction on Java 17 by Oracle, I thought to revise the software principles (like SOLID, KISS, YAGNI etc) which are the building blocks of any software development & the best way to do that is by solving a programming problem.
This solution does not use any framework like Spring boot, DB's etc. so that the solution remains simple. So without wasting any more time lets dive right into the problem.
Problem Statement:
The problem statement is to design the fare calculation engine called as TigerCard. The transport officials from the city of Atlanta want to design a payment system for public metro transport. They have come up with the idea of a prepaid card — TigerCard — which is an NFC enabled card that is to be tapped at entry and exit points of the metro stations.
Detailed excerpt of the problem statement is below :
Explanation:
I have solved the problem by applying various design patterns like Strategy, Chain of Responsibility, Builder etc. I have tried my best to apply principle like SOLID, YAGNI, KISS etc.
The Code is structured according to the SOLID principles into packages,
classes etc. The starting point is
Application.java
whose job is to set/inject the
dependencies before the start of the application and then call the regular
Flow via the FareProcessor.java
which calls the
CalculationService.java
(injected with various Calculation strategies earlier in
FareProcessor.java
)
To keep things simple as iterated earlier, frameworks are avoided even for validations, annotation processor implementations are written For e.g. Date Annotation
@Documented
@Retention(RUNTIME)
@Target({ FIELD, LOCAL_VARIABLE })
@Constraint(validatedBy = DateConstraintValidator.class)
public @interface Date
{
String message() default "Must be of the format dd-MM-yyyy";
Class[] groups() default {};
Class[] payload() default {};
}
And Below is its Validator:
public class DateConstraintValidator implements ConstraintValidator
{
@Override
public boolean isValid(String validDate, ConstraintValidatorContext context)
{
context.disableDefaultConstraintViolation();
var valid = false;
try
{
// dd-MM-yyyy
String[] splitDate = validDate.split("-");
valid = LocalDate.of(Integer.valueOf(splitDate[2]), Integer.valueOf(splitDate[1]), Integer.valueOf(splitDate[0])) != null;
}
catch (Exception e)
{}
return valid;
}
}
So similar way to validate the input I have created Time & Zone Annotations its corresponding validators.
Usage of Chain of Responsibility:
The interface FareStrategy.java
is the one which abstracts away all
the responsibility of the calculation rules into its own functional
units.
Calculation Rules are implemented via the Chain of Responsibility pattern so that the next chain calculates & applies the next rules and are independent with each other. This approach makes them much more maintainable and extendable as when needed. Below are the list of 3 strategies implemented:
- DailyCappedFareStrategy
- TimeBasedFareStrategy
- WeeklyCappedFareStrategy
TimeBasedFareStrategy takes care of the calculating the fare based on the time and weekday constraint's given in the problem, DailyCappedFareStrategy takes care of capping of the fare on the daily basis while the WeeklyCappedFareStrategy does the same thing on week basis.
Link to my github solution is below: