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.
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.
Imagine the object
owns the following objects:
In order to change the ImageLayer
, the API requires:
In order to change the TextLayer
, the API requires:
Now before you think this can simply be abstracted out to an id and a replacement value, consider the AudioLayer
:
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.
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.
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.
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
}