18

this question did a very good job of clearing my confusion a bit on the matter, but I'm having a hard time finding reliable sources on what the exact limitations of the service layer should be.

For this example, assume we're dealing with books, and we want to get books by author. The BookDataMapper could have a generic get() method that accepts condition(s) such as the book's unique identifier, author name, etc. This implementation is fairly trivial (logically), but what if we want to have multiple conditions that require a more complex query?

Lets say we want to get all book written by a certain author under a specific publisher. We could expand the BookDataMapper->get() method to parse out multiple conditions, or we could write a new method such as BookDataMapper->getByAuthorAndPublisher().

Is it preferable to have the service layer call these [more specific] methods directly, or have the conditions parsed before calling the more generic BookDataMapper->get() method with multiple conditions passed? In the latter scenario, the service layer would do more of the logical "heavy lifting," leaving the data mapper fairly simple. The former option would reduce the service layer almost entirely to just a middle-man, leaving conditional logic to the data mapper in methods like BookDataMapper->getByAuthorAndPublisher().

The obvious concern with letting the service layer parse the conditions is that some of the domain logic leaks out of the data mapper. (this is explained in the linked question here. However if the service layer was to handle the conditions, the logic wouldn't make it out of the model layer; The controller would call $book_service->getByAuthorAndPublisher() regardless.

Community
  • 1
  • 1
orourkek
  • 2,071
  • 14
  • 22

2 Answers2

24

The data mapper pattern only tells you, what it is supposed to do, not how it should be implemented.
Therefore all the answers in this topic should be treated as subjective, because they reflect each authors personal preferences.

I usually try to keep mapper's interface as simple as possible:

  • fetch(), retrieves data in the domain object or collection,
  • save(), saves (updates existing or inserts new) the domain object or collection
  • remove(), deletes the domain object or collection from storage medium

I keep the condition in the domain object itself:

$user = new User;
$user->setName( 'Jedediah' );

$mapper = new UserMapper;
$mapper->fetch( $user );

if ( $user->getFlags() > 5  )
{
    $user->setStatus( User::STATUS_LOCKED );
}

$mapper->save( $user );

This way you can have multiple conditions for the retrieval, while keeping the interface clean.

The downside to this would be that you need a public method for retrieving information from the domain object to have such fetch() method, but you will need it anyway to perform save().

There is no real way to implement the "Tell Don't Ask" rule-of-thumb for mapper and domain object interaction.

As for "How to make sure that you really need to save the domain object?", which might occur to you, it has been covered here, with extensive code examples and some useful bits in the comments.

Update

I case if you expect to deal with groups of objects, you should be dealing with different structures, instead of simple Domain Objects.

$category = new Category;
$category->setTitle( 'privacy' );

$list = new ArticleCollection;

$list->setCondition( $category );
$list->setDateRange( mktime( 0, 0, 0, 12, 9, 2001) );
// it would make sense, if unset second value for range of dates 
// would default to NOW() in mapper

$mapper = new ArticleCollectionMapper;
$mapper->fetch( $list );

foreach ( $list as $article )
{
    $article->setFlag( Article::STATUS_REMOVED );
}

$mapper->store( $list );

In this case the collection is glorified array, with ability to accept different parameters, which then are used as conditions for the mapper. It also should let the mapper to acquired list changed domain objects from this collection, when mapper is attempting to store the collection.

The mapper in this case should be capable of building (or using preset ones) queries with all the possible conditions (as a developer you will know all of those conditions, therefore you do not need to make it work with infinite set of conditions) and update or create new entries for all the unsaved domain object, that collection contains.

Note: In some aspect you could say, that the mapper are related to builder/factory patterns. The goal is different, but the approach to solving the problems is very similar.

Community
  • 1
  • 1
tereško
  • 56,151
  • 24
  • 92
  • 147
  • How would such a simple DM handle something like getting all users that registered between two dates, for example? Where would the `WHERE` DB clauses get generated? – orourkek Aug 13 '12 at 22:20
  • In such situation you are not mapping a single domain object, but a collection of them. An the collection would naturally have different setters, then simple domain object, because it is designed to contain a group of them. – tereško Aug 13 '12 at 22:29
  • Well a similar example would be getting a single user with the same conditions, with the same question attached – orourkek Aug 13 '12 at 22:31
  • When you are dealing with a range, you cannot assume that it will return a single entry. That just asking for trouble. – tereško Aug 13 '12 at 22:37
  • very true, the [non-programmatic] abstraction of the example got away from me :P – orourkek Aug 13 '12 at 22:41
  • @tereško In case of collections, do you create an user collection class to specify the criteria? Like the date range. Mind add examples? – Keyne Viana Aug 24 '12 at 14:58
  • For simple project scene, merge Collection into Mapper maybe easier, like @hakre 's answer, because Collection is only act as pre-filter of parameters in Mapper here. – Fwolf Nov 28 '13 at 16:48
  • I guess you have not heard about [SRP](http://en.wikipedia.org/wiki/Single_responsibility_principle). Or at least, have not understood the point of it. – tereško Nov 28 '13 at 16:51
  • i think no matter how much condition in mapper layer,mapper layer responsibility is get object according mapper,these conditions are just query conditions,not business.and the service layer,should put logic like if some occur we should use getByAuthorAndPublisher method,otherswise use others get method,it is real logic. – Amitābha Aug 12 '15 at 02:00
5

I normally prefer this to be more concrete, like:

BookDataMapper->getByAuthorAndPublisher($author, $publisher)

That is because I do not need to re-invent SQL. The database is better for that and the data-mapper takes care here that the rest of the application does not need to know anything about how things are stored or queried in concrete either.

If you make that more dynamic you can easily have the tendency to offer too much functionality via the interface. Not good.

And take a look at your application. You'll see that there is not that much going to be queried differently. For the main part of data that are normally about 5-10 routines if at all. It's written much faster than to even think about some dynamic system that actually would belong into it's own layer anyway.

hakre
  • 178,314
  • 47
  • 389
  • 754
  • Interesting - is this just preference, or is there some best practices and/or documentation to support it? Obviously the leaking of logic/functionality can be evaluated under best practices, but it could also be controlled and refined as to not leak out of the model layer at all, right? – orourkek Aug 13 '12 at 22:08
  • Puh, I'm not a kind of academic developer, no idea if there are documented best practices behind. When I'm in the role of a consumer of some layer - e.g. the data-mapper - I like to have a clean and concise interface and that actually is concrete. No hidden contract via more or less dynamic value or parameter objects. I mean otherwise I can just straight away exectue some SQL if I need more flexibility. But that would destroy the layer. So draw a line and be fine with it. – hakre Aug 13 '12 at 22:20
  • I might add: Some implementation have finders next to mappers. Those take care to find objects, mappers still take care to map the data. Sometimes you can put these together, sometimes it's better to have them apart so you can extend those if the number of finders grow. – hakre Aug 13 '12 at 22:24
  • I prefer this answer over the accepted one. It fits more with the repository approach which makes sure that such search logic which is relevant in the business domain - e.g. searching for books by author and publisher as in the shown example. Such methods should be provided by the repository API (or data mapper API if you will). This makes the search logic more obvious and prevents it from being scattered all over different other classes (such as controllers). – afh Aug 25 '20 at 06:59