1

I am working with an API that is used for registering bookings in a ticket system. Some of the calls I have to make take complex XML objects as arguments. Some data points in these objects have to be retrieved via multiple API calls in-between which a transformation is necessary before this data is combined with existing user data.

This preparation process too complex for one class and therefore distributed over multiple classes with their own, single responsibility.

So far I created a class Booking that holds all data that belongs to one booking request. This class is passed around between modules in the process of preparing the data for the final call.

Doing so is working but couples all modules to the Booking class. If Booking changes, a lot of other classes have to change which is a violation of the single responsibility principle as far as I understand it.

I have considered just passing the data around as arguments between the modules but I definitely see this getting messy and prone to errors. Every method involved would have 4-6 arguments that in some cases might be null or optional.

How do I deal with having to aggregate multiple data points that all have to feed into one API-method call without coupling all modules to one class?

Edit:

These are the data points needed in every step. I considered defining classes for every set of arguments but I think this would be kind of hard to maintain because it becomes very intransparent what object is responsible for what data and how the data flows between the modules.

Availability

  • rates
  • rate quantities
  • date, either from user request or retrieved via additional API call

→ returns BookingComponents

Start Booking

  • booking key
  • promo code
  • BookingComponents
  • email
  • first name
  • last name

→ returns booking id, expected total

Credit Card Payment

  • booking id
  • credit card token
  • total amount

→ returns boolean

PayPal Payment

Commit

  • booking id

SettlePayment

  • total amount
  • paypalNonce
  • paypalOrderId

Record Payment

  • booking id
  • total amount
  • payment method string
  • paypalOrder id

Edit - Added Context for follow-up questions

Modules (Example #1)

Modules involved in booking process

The class AvailabilityManager has the responsibility of getting the availability for tours either in the booking process or in order to display availability on the website. (Relies on very similar calls to the API but has to be done before the booking can be processed)

Is this meant by passing behaviour? (Example #2)

    class InitialBooking{
        protected $date;
        protected $rates;
        protected $quantities;

        //the actual operation is more complex, involves more methods 
        // and will be needed outside of the Booking process as well
        public function verifyAvailability(): BookingComponent
        {
            $availabilityResult = $this->api->getAvailability($date, $rates, $quantities)
            return $this->bookingComponentTransformer->transform($availabilityResult);
        }
    }

1 Answer 1

1

You wrote you already considered passing data around, but that gets messy. Have you considered the opposite, that is passing behavior around?

It is really hard from your description to work out the proper behavior, but here are some thoughts:

  • Don't make a state-machine out of objects. Return different objects for different states, preferably immutable ones. Makes it much easier to think about/test things. Easier for the API user also.
  • Try to avoid surrendering data (that is getters). This leads to the unmaintainable mess you're trying to avoid.
  • In general let the client drive the "flow". Your responsibility is to design the API in a way that the client can not construct an invalid sequence of calls. Basically guide the user to construct the right flow.

You could probably start by imagining what the client will have to write, something like (I'm just making up stuff here):

new Booking("Double Room")     // returns Booking
   .checkAvailability()        // returns AvailableBooking or Exceptions
   .withPromocode("...")       // same
   .withEmail("...")           // same
   .pay(new CreditCard(...))   // returns PayedBooking or whatever

Each of these can be immutable, and responsible for a completely different part of the process. The user drives the flow, but only has options that makes sense. For example she has to call checkAvailability(), because that is the only way to continue with the booking, and she can not call it elsewhere, because it's not available elsewhere.

Does that help?

2
  • thank you! I am working on a checkout system for bookable tours that integrates in an information website. For the booking process I am USING an API, not writing one. By passing behaviour, do you mean that the object holding the data in question also implements the methods for performing the actions? Would that look like example #2? When I use different, immutable state objects in order to create reliable ways of passing data, wouldn't I get the data from the immutable object via getters? Is that unproblematic because it is immutable and can have only one state after being created? Commented Apr 15, 2020 at 21:33
  • You wouldn't get data out of immutable objects either, because that would create coupling the same way getting data out of a mutable object does. The objects, because they contain the behavior for that data can create the new state, therefore the new state doesn't pull data out of the object, the object gives/pushes data to the new state. Commented Apr 16, 2020 at 8:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.