Implementing the solution in Java

Implementing the solution in Java

Let's write code for task one.

Introduction

Hello, and welcome back. Thank you for sticking around!

In this chapter, I will put into action the design I created in the previous chapter. The tools I'll be using, as well as their versions, are listed in the series' introductory chapter. First, I'll create a GitHub repository. Then, I will explain my thought process and code all the classes in our design step by step. Let's get started!

The repository

The project source code is found here: igcse-pre-release.

The Carpark class

I will start with the Carpark class because it is the main class through which all operations will be carried out. Let me define the responsibilities of this class.

  • Define the number of available parking spaces, the price of each space and the total number of bookings in the 2 weeks.

  • Register a visitor’s bookings.

  • Validate the booking details.

  • Alert the user if the validation fails.

  • Ask for payment.

  • Save booking details.

Remember, this is our first development iteration, but in line with the first SOLID principle, the single responsibilities principle, I will delegate the payment process to the booking object itself. This is because, according to our iteration 1 design, the booking class is aware of the visitor, who "owns" a payment method.

For the time being, the validations will be performed in the Carpark class since it contains a collection of records that allow us to perform validations 2 and 3. Validation 1 for the selected booking date will also be put here. Let me begin by defining the class constants.

Constants

public class CarPark {
    public static final int PARKING_SPACES = 20;
    public static final float PRICE_PER_PARKING_SPACE = 3.0f;
    public static final String CURRENCY_CODE = "USD";
    public static final int BOOKING_PERIOD = 14;
    public static final int TOTAL_BOOKINGS = BOOKING_PERIOD * PARKING_SPACES;
    // ...

PARKING_SPACES

represents the number of parking places available in the organization's parking lot. The value was specified in the problem description.

BOOKING_PERIOD

the period in which a booking can be made before using the parking space.

TOTAL_BOOKINGS

the total number of booking in the entire booking period (calculated in chapter 2).

PRICE_PER_PARKING_SPACE

the cost of booking a parking space. I just set it to $3.00.

CURRENCY_CODE

the currency code of the currency by the organization. I came across the currency class in the java.util class.

Field initialization

The constructor is straightforward, taking only the name of the organisation and setting it. The collection of bookings will be initialized inline.

private final List<Booking> bookings = new ArrayList<>();
private final String organization;

public CarPark(String organization) {
    this.organization = organization;
}

validateBookingDate()

I wrote the validateBookingDate method to validate the booking dates. At the present, I'm still not very adept at throwing and handling exceptions. Should I return a boolean value? I feel compelled to. Should the validations be separated? Do I have to...? Okay, let me do some research and then return with a suitable answer in iteration 2. For the time being, this is all I have.

public void validateDate(LocalDateTime bookingDate) throws RuntimwException {
    if (bookingDate.toLocalDate().isBefore(LocalDate.now()))
        throw new RuntimeException("Booking date: " + bookingDate
                + " can not be before the current date: "
                + LocalDate.now());

    var dateAfterTwoWeeks =   
          LocalDateTime.now().plusDays(CarPark.BOOKING_PERIOD);
    if (bookingDate.isAfter(dateAfterTwoWeeks))
        throw new RuntimeException("Bookings can only be made"            
                "within a " + CarPark.BOOKING_PERIOD 
                + " day period from the current date: "
                + LocalDate.now());
}

Because I want to record the timestamp of the creation of all objects with the createdOn field, I used the LocalDateTime class instead of the LocalDate class. Notice how I changed the selected booking date and time into a LocalDate in the first if statement so that the comparison with the current date is based on the date rather than the timestamp.

Let me give an example to demonstrate a problem that will most likely occur. Well, If I book a parking space today, say at 09:00:10 a.m., for a parking space I would like to use today and the booking object is created a few microseconds later, then the isBefore function will always return true. The booking date will always be before the creation date of the object.

The isAfter test is okay, it doesn’t have this requirement.

validDateFormat()

The function validates the date format. Looking at this code, I can't help feeling that something is wrong with it. I will fix it in iteration 2.

public void validDateFormat(String date) throws ParseException {
    DateFormat format = new SimpleDateFormat("dd-MM-yyyy");
    format.setLenient(false);
    format.parse(date);
}

addBooking()

Now, let’s code the addBooking method. I will not handle any exceptions in this method. Exceptions will be propagated in the call stack using the throws keyword. According to what I've read, a method doesn't have to handle an exception if it explicitly specifies that it will throw one. The caller of the exception-throwing method must handle the exceptions (or toss them to its caller, and so on) for the program's flow to be preserved.

public void addBooking(Booking booking, LocalDateTime bookingDate)  throws NoParkingSpaceException, PaymentFailedException {
    long count = checkAvailableParkingSpace(bookingDate);

    booking.setBookingDate(bookingDate);

    Money parkingCost = 
    new Money(Currency.getInstance(CURRENCY_CODE),
            BigDecimal.valueOf(PRICE_PER_PARKING_SPACE));
    booking.setParkingSpace(new ParkingSpace((int) count + 1, parkingCost));

    booking.pay();
    this.bookings.add(booking);
}

checkAvailableParkingSpace()

First, the checkAvailableParkingSpace method performs validation 2 and 3, and returns the number of bookings made on the given date (which will be less than 20). Otherwise, a NoParkingSpaceException exception is thrown.

Second, we set the booking date since all three validations were successful. Following that, we create a Money object to represent the cost of parking and then we create a ParkingSpace object inline in the call to setParkingSpace. Take note of the variable count being incremented to establish the parking spot number. What exactly occurred here? The checkAvailableParkingSpace method provided a count of the parking spots that have been reserved. By increasing the count, we get the next open spot for that day.

Lastly, the user is asked to pay for the assigned parking space, and the booking is saved.

Let’s examine the checkAvailableParkingSpace method in great detail. It checks if validations 2 and 3 are successful and throws a NoParkingSpaceException if they are not. Otherwise, the number of parking spots available on that day is returned.

What are your thoughts on my exception messages? Wonderful, aren’t they?

private long checkAvailableParkingSpace(LocalDateTime bookingDate) {
    if (this.bookings.size() == TOTAL_BOOKINGS) {
        throw new NoParkingSpaceException("No free parking space for the next " 
        + BOOKING_PERIOD + "  days. Should we schedule the booking on: "
        + LocalDate.now().plusDays(BOOKING_PERIOD + 1) + "?");
    }

    long count = countBookingsOn(bookingDate);
    if (count == PARKING_SPACES) {
        throw new NoParkingSpaceException(
        "No free parking space on this particular day: " + bookingDate);
    }
    return count;
}

countBookingsOn()

Finally, let’s explore the countBookingsOn method. It uses the Java stream API to perform a filter and reduce transformation to count the number of available slots on a particular date.

private long countBookingsOn(LocalDateTime date) {
    return this.bookings.stream()
            .filter(b -> b.getBookingDate().compareTo(date.toLocalDate) == 0)
            .count();
}

Phew! We have come to the end of the Carpark class.

The Booking class

The visitor's information must be supplied to the constructor to create a booking. Except for the pay function, this class is largely made up of getters and setters. pay just forwards the payment to the visitor's checkout method, which features a payment method.

That's it for this class, not forgetting the output formatting in the toString!!

public class Booking {
    private final UUID bookingID;
    private final Visitor visitor;
    private ParkingSpace parkingSpace;
    private Payment payment;
    private final LocalDateTime createdOn;
    private LocalDateTime bookingDate;

    public Booking(Visitor visitor) {
        this.visitor = visitor;
        this.bookingID = UUID.randomUUID();
        this.createdOn = LocalDateTime.now();
    }

    public void setBookingDate(LocalDateTime date) {
        this.bookingDate = date;
    }

    public void pay() throws PaymentException {
        this.visitor.checkout(this);
    }

    public void setPayment(Payment payment) {
        this.payment = payment;
    }

    public LocalDateTime getBookingDate() {
        return bookingDate;
    }

    public Money getBookingPrice() {
        return parkingSpace.getPrice();
    }

    public void setParkingSpace(ParkingSpace parkingSpace) {
        this.parkingSpace = parkingSpace;
    }

    public UUID getBookingID() {
        return bookingID;
    }

    @Override
    public String toString() {
        return "\n\t\tBooking{" +
                "\n\t\t\tbookingID=" + bookingID +
                ", \n\t\t\tvisitor=" + visitor +
                ", \n\t\t\tparkingSpace=" + parkingSpace +
                ", \n\t\t\tpayment=" + payment +
                ", \n\t\t\tcreatedOn=" + createdOn +
                ", \n\t\t\tbookingForDate=" + bookingDate +
                "\n\t\t}";
    }
}

Most of the classes currently have a simple implementation; just a constructor and/or a couple of getters and setters. From now on I will only show snippets of unique code.

The Visitor class

Since the visitor has details about the payment method they will use, I figured the booking will be paid from here. For the many payment methods that the system would allow, I decided to design a functional interface named PaymentMethod. The abstract mkPayment method accepts the bookingID and parking spot fee and returns an optional payment. If a payment fails, the function must return an empty optional variable.

public interface PaymentMethod {
    Optional<Payment> mkPayment(UUID bookingID, Money bookingPrice);
}

The checkout method throws an exception if the payment fails otherwise the payment details are saved in the booking object.

public void checkout(Booking booking) throws PaymentFailedException {
    var payment = paymentMethod.mkPayment(booking.getBookingID(),             booking.getBookingPrice());

    if (payment.isEmpty())
        throw new PaymentFailedException("Payment process failed. Please try again later.");
    booking.setPayment(payment.get());
}

PaymentMethodFactory class

I decided to construct a PaymentMethodFactory class that would generate the payment method depending on the name supplied by the user.

public class PaymentMethodFactory {
    public static PaymentMethod getInstance(PaymentType paymentChoice) {
        PaymentMethod paymentMethod;

        switch (paymentChoice){
            case CREDITCARD:
                paymentMethod = new CreditCard(PaymentType.CREDITCARD.toString());
                break;
            case ECOCASH:
                paymentMethod = new Ecocash(PaymentType.ECOCASH.toString());
                break;
            case ONEMONEY:
                paymentMethod = new Onemoney(PaymentType.ONEMONEY.toString());
                break;
            default:
                throw new RuntimeException("Wrong choice of payment system. Select values from 1 to 3");
        }

        return paymentMethod;
    }
}

PaymentType enum class

The PaymentType enum class defines all the supported payment methods. This enum is required when creating a payment method object in the PaymentMethodFactory class.

public enum PaymentType {
    ECOCASH("ecocash"), CREDITCARD("creditcard"), ONEMONEY("onemoney");

    private final String name;

    PaymentType(String name){ this.name = name; }

    @Override
    public String toString() {
        return name;
    }
}

Creditcard payment

The implementation is a dummy. Here, I'm assuming a payment process has a 70% chance of success. I have used Math.random() to make a payment based on this assumption.

public class CreditCard implements PaymentMethod {
    private final String name;

    public CreditCard(String name) {
        this.name = name;
    }

    @Override
    public Optional<Payment> mkPayment(UUID bookingID, Money bookingPrice) {
        return (Math.random() > 0.3) ?
                Optional.of(new Payment(bookingID, bookingPrice)) :
                Optional.empty();
    }

    @Override
    public String toString() {
        return "CreditCard{'"
                + name + '\'' +
                '}';
    }
}

The exception classes

I extended the RunTimeException class because the program should not stop running if there are no free parking spaces. The PaymentFailedException is coded similarly.

public class NoParkingSpaceException extends RuntimeException {
    public NoParkingSpaceException(String message) {
        super(message);
    }
}

Running the code

I created the main method to see if the code works. The constant NUM_OF_VISITORS represents the total number of bookings that can be made. I'm supposed to use the Carpark.TOTAL_BOOKINGS constant but it's too large to test the functionality of the program hence the use of NUM_OF_VISITORS.

The try/catch block looks scary. Not sure I should have a continue statement in the catch block but the program should repeat the data capture process whenever an error occurs.

The two date validation methods, according to the single responsibility principle, are not supposed to be in the Carpark class. I will create a date validation class for that in the next iteration.

public class Main {
    private static final int NUM_OF_VISITORS = 3;

    public static void main(String[] args) {
        CarPark carPark = new CarPark("Motor space");

        for (int i = 0; i < NUM_OF_VISITORS; ) {
            Visitor newVisitor;
            Booking booking;
            LocalDateTime bookingDate;

            try {
                bookingDate = readBookingDate(carPark);
                newVisitor = createVisitor();

                booking = new Booking(newVisitor);
                carPark.addBooking(booking, bookingDate);
            } catch (NoParkingSpaceException | PaymentFailedException | IllegalArgumentException | ParseException | InvalidDateException e) {
                System.err.println("Error: " + e.getMessage());
                continue;
            }

System.out.println("========================================================");
            i++;            
        }

        System.out.println("========================================================");
        System.out.println(carPark);
        System.out.println("========================================================");
    }

    private static Visitor createVisitor() throws IllegalArgumentException {
        Scanner scanner = new Scanner(System.in);

        System.out.println("Enter your name: ");
        String name = scanner.nextLine();

        System.out.println("Enter your id number: ");
        String id = scanner.nextLine();

        System.out.println("Enter your address: ");
        String address = scanner.nextLine();

        System.out.println("Enter your licence number: ");
        String license = scanner.nextLine();

        System.out.println("We only allow 4 payment methods: " +
                "\n1. Credit/Debit card \n2. Ecocash \n3. Onemoney");
        System.out.println("Select your favourable payment method");
        String choice = scanner.next().toUpperCase();

        PaymentMethod paymentMethod = PaymentMethodFactory.getInstance(PaymentType.valueOf(choice));

        var newVisitor = new Visitor(name, id, address);
        Car car = new Car(license);
        newVisitor.setCar(car);
        newVisitor.setPaymentMethod(paymentMethod);

        return newVisitor;
    }

    private static LocalDateTime readBookingDate(CarPark carPark) throws ParseException, InvalidDateException {
        Scanner scanner = new Scanner(System.in);
        LocalDateTime bookingDate = null;
        String pattern = "dd-MM-yyyy";

        System.out.println("Enter your most favourable booking date: ");
        String date = scanner.nextLine();

        carPark.validDateFormat(date);

        bookingDate = LocalDateTime.of(
                LocalDate.parse(date, DateTimeFormatter.ofPattern(pattern)),
                LocalTime.now());

        carPark.validateBookingDate(bookingDate);

        return bookingDate;
    }

Now let me run the program!!

Enter your most favourable booking date: 
20-01-2023
Enter your name: 
Wilfred
Enter your id number: 
09876-54W
Enter your address: 
1234 Nkulumane, Bulawayo
Enter your licence number: 
7654-TDF
We only allow 4 payment methods: 
1. Credit/Debit card 
2. Ecocash 
3. Onemoney
Select your favourable payment method
ecocash
========================================================
Enter your most favourable booking date: 
20-01-2023
Enter your name: 
Someone Else
Enter your id number: 
012345-09S
Enter your address: 
0987 Somewhere far, Harare
Enter your licence number: 
8721-WER
We only allow 4 payment methods: 
1. Credit/Debit card 
2. Ecocash 
3. Onemoney
Select your favourable payment method
creditcard
========================================================
Enter your most favourable booking date: 
23-01-2023
Enter your name: 
Another Person
Enter your id number: 
876543-87D
Enter your address: 
654 Pumula, Bulawayo
Enter your licence number: 
4321-GHJ
We only allow 4 payment methods: 
1. Credit/Debit card 
2. Ecocash 
3. Onemoney
Select your favourable payment method
onemoney
========================================================
CarPark{
    Organization="Motor space"
    bookings=[
        Booking{
            bookingID=bf0c31e9-1ad7-4ed1-8870-2ddfeaa46ee0, 
            visitor=Visitor{name='Wilfred', id='09876-54W', address='1234 Nkulumane, Bulawayo', car=Car{licenceNumber='7654-TDF'}, paymentMethod=Ecocash{name='ecocash'}}, 
            parkingSpace=ParkingSpace{spaceNumber=1, price=$3.0}, 
            payment=payment.Payment{id=3fee7c8f-a3f8-409b-980f-fe7c5c46399c, bookingID=bf0c31e9-1ad7-4ed1-8870-2ddfeaa46ee0, cost=$3.0, createdOn=2023-01-15T00:32:51.362420}, 
            createdOn=2023-01-15T00:32:51.355898, 
            bookingForDate=2023-01-20T00:31:55.690777
        }, 
        Booking{
            bookingID=d5863adb-f609-4b0d-a118-c688fa42cfc5, 
            visitor=Visitor{name='Sipho', id='012345-09S', address='0987 Mbare, Harare', car=Car{licenceNumber='8721-WER'}, paymentMethod=CreditCard{name='creditcard'}}, 
            parkingSpace=ParkingSpace{spaceNumber=2, price=$3.0}, 
            payment=payment.Payment{id=b12c4e29-b04e-4e5c-ac03-75658408242d, bookingID=d5863adb-f609-4b0d-a118-c688fa42cfc5, cost=$3.0, createdOn=2023-01-15T00:34:25.950353}, 
            createdOn=2023-01-15T00:34:25.949725, 
            bookingForDate=2023-01-20T00:33:03.833892
        }, 
        Booking{
            bookingID=bb380089-075c-41f0-928c-5f0316bc3b63, 
            visitor=Visitor{name='Musa', id='876543-87D', address='654 Pumula, Bulawayo', car=Car{licenceNumber='4321-GHJ'}, paymentMethod=Onemoney{name='onemoney'}}, 
            parkingSpace=ParkingSpace{spaceNumber=1, price=$3.0}, 
            payment=payment.Payment{id=73e16d64-641f-49c4-928c-bba7bf155510, bookingID=bb380089-075c-41f0-928c-5f0316bc3b63, cost=$3.0, createdOn=2023-01-15T00:36:06.100821}, 
            createdOn=2023-01-15T00:36:06.100549, 
            bookingForDate=2023-01-23T00:34:47.716319
        }]
}
========================================================

Process finished with exit code 0

Awesome! So far the program works!

I successfully booked three visitors. Notice that each visitor paid with a different payment method and two of them choose the same booking day. The program was also able to allocate two consecutive spaces for the visitors on that day.

Conclusion

Iteration 1 of development has come to an end. The source code is available here: igcse-pre-release. The program works as expected but we need to write JUnit tests to be completely certain. I will write the unit tests in the next chapter.

Remember: I would greatly appreciate any feedback on my work so that both I and other readers can improve. If you have suggestions please let me know, if possible contribute to the code base. Also, every time I learn new facts that I would have missed while coding the system, I will update this post.

NEXT: Refactoring code for task 1