Exploring Service Locator Pattern

Recently I was asked to look at Webhook implementation in an application by a client which had grown in size and had become hard to maintain. As I sought the ideal implementation, I delved into various design patterns. The Service Locator pattern emerged as the perfect fit.

Service Locator Pattern

The Service Locator pattern is a design pattern used in software development to encapsulate the processes involved in obtaining a service with a strong abstraction layer. This pattern uses a central registry known as the “Service Locator” which on request returns the information necessary to perform a certain task.

Remember old movies where someone wanted to make a call, and an operator manually connected them to their desired line using a switchboard? That operator is like the Service Locator.

Service Locator Pattern, Simplified with a Phone Exchange:

Caller (You): You want to call your friend, so you pick up the phone.

Operator (Service Locator): Instead of dialling directly, you tell the operator whom you’d like to speak with.

Connection: Knowing which line corresponds to which person, the operator connects you to your friend.

When a part of your program needs a service, the software asks the Service Locator (our operator). The Service Locator then provides the correct service (connects the call) based on the request.

Within the context of this application, the main function to send webhook data had quite a few event types. Some of the events were as follow;

  • Room becomes available
  • Room is updated
  • Property is updated
  • New booking period was added

Here is how it was implemented on a high level


  function sendWebhookData($dataType, $id) {
      if ($dataType == 'room_updated') {
          $data = getRoomUpdatedData($id);
      } elseif ($dataType == 'property_updated') {
          $data = getPropertyUpdatedData($id);
      } elseif ($dataType == 'booking_period_added') {
          $data = getBookingPeriodAddedData($id);
      } elseif ($dataType == 'room_available') {
          $data = getRoomAvailableData($id);
      } else {
          throw new Exception("Unknown data type");
      }

      // Logic to send $data to external applications.
      echo "Sending: {$data}\n";
  }

  function getRoomUpdatedData($id) {
      return "Room {$id} has been updated";
  }

  function getPropertyUpdatedData($id) {
      return "Property {$id} has been updated";
  }

  function getBookingPeriodAddedData($id) {
      return "A new booking period has been added for Room {$id}";
  }

  function getRoomAvailableData($id) {
      return "Room {$id} is now available";
  }

  // Example Usage:
  sendWebhookData('room_updated', 101);

These are just some of the events among others. While the above approach might seem straightforward and easier to implement, as more events were added, the primary function had grown, becoming cluttered, and harder to maintain.

Service locator pattern to the rescue

Here is how we refactored the above logic

Defining the Service interface

A simple and common interface that will be adhered to by all the services, ensuring a consistent data retrieval mechanism

interface WebhookDataService {
    public function getData($dataType, $id);
}

Different services for each event

We created an individual class for each type of event to fetch the respective data.

class RoomUpdatedService implements WebhookDataService {
    public function getData($dataType, $id) {
        // Fetch the updated room data by ID from the database.
        return "Room {$id} has been updated";
    }
}

class PropertyUpdatedService implements WebhookDataService {
    public function getData($dataType, $id) {
        // Fetch the updated property data by ID.
        return "Property {$id} has been updated";
    }
}

class BookingPeriodAddedService implements WebhookDataService {
    public function getData($dataType, $id) {
        // Fetch the new booking period by ID.
        return "A new booking period has been added for Room {$id}";
    }
}

class RoomAvailableService implements WebhookDataService {
    public function getData($dataType, $id) {
        // Fetch room availability status by ID.
        return "Room {$id} is now available";
    }
}

The service locator

The main class to glue all the pieces together where we would register and fetch services

class WebhookServiceLocator {
    private static $services = array();

    public static function registerService($name, WebhookDataService $service) {
        self::$services[$name] = $service;
    }

    public static function getService($name): WebhookDataService {
        if (isset(self::$services[$name])) {
            return self::$services[$name];
        }
        throw new Exception("Service not found");
    }
}

Using the service locator

We then registered the services and use the locator to fetch the right service based on the event type.

// Register services
WebhookServiceLocator::registerService('room_updated', new RoomUpdatedService());
WebhookServiceLocator::registerService('property_updated', new PropertyUpdatedService());
WebhookServiceLocator::registerService('booking_period_added', new BookingPeriodAddedService());
WebhookServiceLocator::registerService('room_available', new RoomAvailableService());

// Send data using the appropriate service
function sendWebhookData($dataType, $id) {
    $service = WebhookServiceLocator::getService($dataType);
    $data = $service->getData($dataType, $id);

    // Logic to send $data to registered external applications.
    echo "Sending: {$data}\n";
}

// Example Usage:
sendWebhookData('room_updated', 101);

This pattern allowed us to easily extend the system for other event types or modify existing ones without affecting the core logic. When a new event type would be introduced, we’ll create its service, register it, and the rest of the application remains unchanged. This also makes sure that it conforms to Open and Closed design principle

Happy coding