If I have a site design which follows a dependency flow of:
Handler Class <-> Service Class <-> Mapper Class <-> Model Class
Handler receives the request, Service contains the logic, Mapper performs the DB queries and uses the Model to work on the data, while the Model represents a single record in the DB.
Ideally, you would have explicitly defined dependencies on/from the neighboring layers. So if I change a particular layer, only the two immediately neighboring layers might be affected.
However, I realized I may be using some bad practices. In particular, I have the Service class call a method on the Mapper class. The Mapper class would return an instance of the Model class. But if I then call a method of the Model class, from within the Service class, there is now a direct and implicit dependency between the Service class and the Model class.
I have a few thoughts on how to address this, which I'll outline below. But I feel there has got to be a better way. So I'm curious what the general consensus is on avoid these implicit dependency situations while avoiding unnecessary processing.
I've been trying to find out if it's possible to specify the type of object the \Iterable will iterate over, like you can with arrays (IE: int[]
is an array of integers). The best I could come up with was to create a custom Iterator class specific to the Model class. Is this the preferred method of eliminating these implicit dependencies, while avoiding unnecessary loops?
Much appreciation!
Edit based on feedback:
I don't have any code which fails, and this question isn't meant to be specific to any real code. This question is solely about an abstract problem. That is why no code was originally included. But based on feedback I've drafted up the following to help make the purposed situation more clear.
Just ignore the Handler class, it's not needed to illustrate the concept, I was just trying to describe the overall structure with it to make it easier to identify the design pattern.
The Service class:
namespace App\Service;
class Service implements ServiceInterface
{
protected mapperInterface $mapper;
public function __construct(mapperInterface $mapper){
$this->mapper = $mapper;
}
public getAllRows() {
$iterator = $this->mapper->getAllRows();
while($iterator->valid()){
$current = $iterator->current();
// This line creates an implicit dependency on the Model class
$name = $current->getName();
$iterator->next();
}
}
}
The Mapper Class: namespace App\Mapper;
class Mapper implements MapperInterface
{
protected modelInterface $modelPrototype;
public function __construct(modelInterface $model){
$this->modelPrototype = $model;
}
public getAllRows() : Iterator {
// Execute SQL then hydrate into an iterator over multiple "Model" objects into a variable ($resultSet in this case)
return $resultSet; // This is an Iterator instance of which each iteration contains a "Model" object.
}
}
The Model class:
class Model implements ModelInterface
{
protected string $name;
public function getName() : string {
return $this->name;
}
public function setName(string $name) : void {
$this->name = $name;
}
}
It is specifically this line in the Service class which creates an implicit dependency between the Service class and the Model class (bypassing the transitive dependency through the Mapper class):
$name = $current->getName();
There are two ways to correct this, either make the dependency explicit, or eliminate the dependency in the first place. I've listed my ideas on how to achieve those two options. But I am curious what the most common solution to this situation is. Is there some other option or method of addressing this which I have not thought of? Or is one of my proposed ideas the generally preferred method.
Technically i also don't see a dependency. The Service
ask it's neighbour Mapper
for a collection of Model
and it provides one. The Mapper
uses the Model
and hydrates the data from whereever. So communication-wise everything is fine.
If you would access data in your Service
with
$this->mapper->model->someModelMethod();
instead of
$this->mapper->getAllRows();
only then you would violate something at least in the sense of the law of demeter.
The dependency on the level you might think having an issue is probably some kind of tight coupling inside your layers, which is generally okay for this use case.
If you wanna decouple it between layers/modules/units consider introducing another kind of data container to pass around.
For example using some kind of Transfer Object
and map/hydrate the data from your model to it. This transfer object can be used to pass around e.g. on the communication layer like Controllers, Services etc. You only rely on the transfer objects properties but not on the model itself. Only your mapper has to be adapted if something changed on both the Model
and the Transfer Object