Search code examples
phpoopencapsulationsoftware-design

Modelling Change in an OO context


Formal semantics of an Object-oriented programming language include encapsulated state. Is there a use-case for encapsulating a potential change, previous to the state change? Although the following examples are in PHP, please also think language-agnostic in your answer.

Background

I have a Client object whose responsibility is to send requests and retrieve responses from a server, and this is used to change state of an object that resides on another server by calling an API. There are a few Url's, one with the endpoint create and another with the endpoint update. The problem is, update can be used to update multiple different elements within the given object, each requiring different parameters.

'Layer' Objects

Imagine the object owns the following objects:

  • ImageLayer
  • BackgroundLayer
  • TextLayer
  • AudioLayer
  • (N)Layer

In order to change the ImageLayer, the API requires:

  • An ID
  • The url of the new image

In order to change the TextLayer, the API requires:

  • An ID
  • What you are changing about it (is it the font, the size, the text content?)
  • The new value

Now before you think this can simply be abstracted out to an id and a replacement value, consider the AudioLayer:

  • An ID
  • A bitrate
  • The url of the new audio file
  • Some other values

My Considerations

I originally considered adding Client::updateImageLayer(), Client::updateTextLayer(), but then realised that the Client object could become exponentially bigger given (N)Layer in the future.

Then I considered adding Client::updateLayer(Layer $layer, array $values) but I didn't think this was good enough.

Finally, here's another option I have been thinking about: a Change object.

Change Object

What if I created a Change object that encapsulated the change to any specific layer, and then this could be passed to the Client, already validated, and ready to be sent off in the request to the API?

interface Change
{
    public function isValid();

    public function getChanges();
}

class ImageLayerChange implements Change
{
    protected $url;

    protected $id;

    public function __construct($id, $url)
    {
        $this->url = $url;
        $this->id  = $id;
    }

    public function isValid()
    {
        // Use a validator object to check url is valid etc
    }

    public function getChanges()
    {
        return array($this->id, $this->url);
    }
}

Using the above, the Client object can loop around a set of Change objects, I could even make a ChangeSet, make sure they're all valid by calling isValid(), and call getChanges() to send the specific array direct to the API as it's all validated.

Questions

  • I have never heard of modelling change before. What do you think about the above option? Am I overcomplicating things? I liked the idea of being able to add / remove changes from a ChangeSet at will, and for everything to still work as expected as they conform to the Change interface.

  • Perhaps I'm not going about this the best way? Is there anything bad about my solution.

  • Are there any patterns I am using, or should be considering, when using either my solution or the one you propose? I am interested in good code.


Solution

  • From what you’ve described it seems to me you need an API Client and Request objects.

    namespace Api;
    
    interface Client {
    
        /**
         * @param string $method
         * @param string $urn
         * @param array $params
         * @return Response
         */
        public function call($method, $urn, array $params = array());
    }
    
    interface Request {
    
        public function isValid();
    
        public function make(Client $client);
    }
    

    And for example implementation something like this. App's ApiClient is responsible for making API calls, knowing which URL to target. I would have the ApiClient know the API's URL and the request would hold the URN portion (resource name). Together they would form the full URI. (This is optional, just anticipating the API versions).

    namespace App;
    
    class ApiClient implements \Api\Client {
    
        private static $url = 'https://api.yourapp.tld';
    
        /**
         * Just an example implementation using json (not completed)
         */
        public function call($method, $uri, array $params = array()) {
            $cUrlHandle = \curl_init(self::$url . '/' . $uri);
            \curl_setopt($cUrlHandle, CURLOPT_CUSTOMREQUEST, $method);
            \curl_setopt($cUrlHandle, CURLOPT_RETURNTRANSFER, true);
    
            if (!empty($params)) {
                \curl_setopt($cUrlHandle, CURLOPT_POSTFIELDS, \json_encode($params));
            }
    
            $response = curl_exec($cUrlHandle);
    
            //...
        }
    
    }
    

    Now we can create different types of requests. Each would be responsible for validating and packaging parameters for the API Client.

    I've noticed that all the update requests require the id. I would use that to handle the create and update requests with one object (if that's not possible you can split them).

    class ImageLayerRequest implements \Api\Request {
    
        private $id;
        private $url;
        private $apiUrn;
        private $params = array();
    
        /**
         * If $id provided it's an update request otherwise it'll be create
         * 
         * @param string $id
         * @param string $imageUrl
         */
        public function __construct($id, $imageUrl) {
            $this->id = $id;
            $this->url = $imageUrl;
        }
    
        public function isValid() {
            if ($this->id === null) {
                //validate create request
                //...
    
                $this->apiUrn = 'image-layer/create';
            } else {
                //validate update request
                //...
                $this->params['id'] = $this->id;
                $this->apiUrn = 'image-layer/update';
            }
    
            $this->params['url'] = $this->url;
        }
    
        public function make(\Api\Client $client) {
            return $client->call('post', $this->apiUrn, $this->params);
        }
    
    }
    

    And for AudioLayer:

    class AudioLayerRequest implements \Api\Request {
    
        private $id;
        private $bitrate;
        private $url;
    
        //the rest is similar, just diferent parameters
    }