Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP: Design pattern for managing types of entity

I have User entity. I would like to have multiple "types" of this entity, with different managers and repositories. All User entities of all types would share only UserInterface. Now, I'm looking for a good method of organizing everything. First thing that came to my mind is to create something like this:

interface UserTypeManagerInterface
{
  public function addUserType($name, RepositoryInterface $repository, ManagerInterface $manager);
  public function hasType($name);
  public function getRepository($type);
  public function getManager($type);
}

Then in places where I would like to manage multiple types of User at once, I would inject this, and in places where I would like to manage a specific type of user, I could inject only specific repository and manager objects, for its type.

Seems like a pretty clean approach, but at the same time, when I would like to create a test for a class using UserTypeManager I would need to mock UserTypeManager, then this mock would need to return other mocks (repository and manager).

This is doable of course, but it made me thinking if this can be avoided. The only other thing that I can think of, which would allow for avoidance of above complexity during testing, would be something like this:

interface UserTypeManagerInterface {
    public function addUserType($name, RepositoryInterface $repository, ManagerInterface $manager);
}

/**
 * My class managing multiple types of user.
 */
class ManageMultipleTypesOfUsers implements UserTypeManagerInterface {
    // ...
}

So I would just add all repositories and managers to all classes implementing UserTypeManagerInterface interface. So objects would use directly what was given to them.

This way testing would be much cleaner, because I would need to mock only the one manager and one repository to test class ManageMultipleTypesOfUsers, but this feels sooo much like over-engineering. ;)

Is any middle-ground here even possible?

like image 545
Łukasz Zaroda Avatar asked Aug 08 '16 11:08

Łukasz Zaroda


People also ask

Which design pattern is used in PHP?

PHP Design patterns is an Object-Oriented Programming (OOP) concept that is now also used in Drupal 9 projects. With Drupal's adoption of modern PHP and OOP concepts since version 8, design patterns can be leveraged for cleaner and more robust programming.

What is factory design pattern with example PHP?

Factory method is a creational design pattern which solves the problem of creating product objects without specifying their concrete classes. The Factory Method defines a method, which should be used for creating objects instead of using a direct constructor call ( new operator).

What is facade design pattern in PHP?

Facade is a structural design pattern that provides a simplified (but limited) interface to a complex system of classes, library or framework. While Facade decreases the overall complexity of the application, it also helps to move unwanted dependencies to one place.

What is manager design pattern?

This design pattern can be applied whenever a system needs to support many entities of same or similar type. The Manager object is designed to keep track of all the entities. In many cases, the Manager will also route messages to individual entities.


1 Answers

I can not give a definitive answer on this as it is a trade-off.

As far as I see User is a pure value object? That's a good start. That means it's trivial to manipulate without side-effects.

Now, consider these variables which shall influence your design:

  • Is the interface just one method? [Or are all its methods trivial wrappers for one method?]
  • Is the interface meant to have the users of your library define custom implementations?
  • Do the implementations just add assertions (e.g. the interface requires a method delete(User $user) - only Admins are allowed, other implementations just throw, basic permission checks) or use vastly different code?
  • How much over-engineered vs untestable mess do we have?

So, how do these variable influence our decision?

  • If you ever only have one [smaller] central method, then interfaces plus classes are likely to be overkill and tend to litter your codebase with many small files. Often passing a Closure around also achieves the same effect.
  • Apart from when simple Closures/callables are enough, public API shall always be interfaces.
  • When implementations just add assertions, you typically are better off with one single class dispatching a request to the access control manager [which reads/caches the permissions from database].
  • over-engineered vs untestable: 50 files with each 3 lines of different code are better merged into a single file with 300 lines.

As I do not know what the requirements will be, I cannot give you an ideal solution. The (second) solution you proposed is fine for the "over-engineered" case.

A more moderate solution would be the functional approach:

function addAdminUser($name, RepositoryInterface $repository, ManagerInterface $manager) { /* ... */ }
function addNormalUser($name, RepositoryInterface $repository, ManagerInterface $manager) { /* ... */ }
// you even can pass this around as a callable ($cb = "addAdminUser"), or (pre-binding):
$cb = function($name) use ($repo, $mgr) { addAdminUser($name, $repo, $mgr); };

Or radically (if normal user is a subset of admin user addition) [as long as the function itself is side-effect free and its callers not going to be too hard to test]:

function addUser($name, $type, RepositoryInterface $repository, ManagerInterface $manager) {
    /* ... common logic ... */
    if ($type == IS_ADMIN_USER) { /* ... */ }
}

If that's too radical, also possible to inject a callback into addUser:

function addUser($name, $cb, RepositoryInterface $repository, ManagerInterface $manager) {
    $user = new User;
    /* ... common logic ... */
    $cb($user, $repository, $manager);
}

Yes, the functional approach is maybe not as testable (i.e. you won't be able to mock the function if you call it directly (without passing as callable)), but it may bring you a huge gain in readability. Saving levels of trivial indirection may make your code easier to analyze. I'm emphasizing the may, as removing too much indirection may achieve the opposite effect. Find the middle ground as applicable to your application.

TL;DR: PHP provides you with a more functional approach, but less testable. Sometimes that's a fine trade-off, sometimes not. It depends on your concrete code where the middle ground lies.

P.s.: The only thing which really smells a bit to me in your second proposal, is having RepositoryInterface $repository, ManagerInterface $manager in the addUserType method signature. Are you sure it's not part of the constructor?

like image 59
bwoebi Avatar answered Oct 07 '22 04:10

bwoebi