2

I am facing a common situation where I am saving some values into database from a business object. I am using a relational database and usually I only need to save a few items that are part of the object.

To clarify, my business object is composed of other sub-objects, or some core variables (float, string, int). It contains user input, result of various computations, data read from database, some data formatting.

Before saving parts of the object to database, I first need to prepare the data of the object. Preparing can mean:

  • add weights of sub-components of an assembly (i.e. physical counterparts can be motor & pump weights)
  • convert floats to ints
  • format values and add units, i.e. 8.0292542 => 8.03 mm
  • run any more involved computations, which could be parts of other service classes. Extract variables from returned result

I call the above computations, conversions, formatting, etc parts of the "Service Layer". And raw database SQL code as part of the "Repository Layer"

My question is ... should those layers always remain separate, or are there cases where they can be merged?

Arguments for merging Service code into the Repository

One high-level argument I can provide for merging them here is -- I am saving the business object into the database. I can call a single Repository layer method and in that method I can do both the preparation of the data and actual SQL-inserting the data into the database. Any such preparation of data is needed and required before inserting it into the database, and not for any other cause, and hence, it should be part of the Repository Layer.

Arguments for keeping Service code and Repository code separate

Repository layer should concert itself with SQL-level and database-level concerns. It doesn't care, and shouldn't care about how some high-level business object is converted, formatted, computed, and formed. Hence, any of these conversions and object-manipulation before saving it to database are to live and remain in a separate class, separately in a Service Layer.

Then, final results can be passed to the Repository layer, i.e. in array form, ready for the Repository layer to just take them and directly insert them into the prepared SQL statement.

Gray Areas

I could also make a hazy argument that "it depends". Are there service-related tasks that are so removed from preparation of object for insertion into database, that they indeed must be in a separate layer? And are there service-related tasks that are so close that they must be included into the Repository layer? I think here is where I need some help figuring that out as well.

Sample Code

This is a crude example as it doesn't quite capture the complexities of objects I work with, but it illustrates the above. First, suppose you have some business entity class, i.e. Point:

class Point
{
    private float $x;
    
    private float $y;
    
    public function __construct(float $x, float $y)
    {
        $this->x = $x;
        $this->y = $y;
    }
    
    public function getX(): float
    {
        return $this->x;
    }
    
    public function getY(): float
    {
        return $this->y;
    }
}

Exhibit 1: Separate Service and Repository layers

class Service
{
    function getDistance(Point $a, Point $b): float
    {
        return sqrt(pow($b->getX() - $a->getX(), 2) + pow($b->getY() - $a->getY(), 2));
    }
}

class Repository
{
    function saveDistance(float $distance): void
    {
        print "insert $distance into table" . PHP_EOL;
    }
}

$service = new Service();
$repository = new Repository();

$a = new Point(1, 1);
$b = new Point(2, 2);

// Here we have separate layers, and distance is passed to Repository layer to get saved.
$distance = $service->getDistance($a, $b);
$formatted_distance = number_format($distance, 2);
$repository->saveDistance($formatted_distance);

Exhibit 2: Repository layer includes any Service related code

class Repository
{
    function saveDistance(Point $a, Point $b): void
    {
        $distance = sqrt(pow($b->getX() - $a->getX(), 2) + pow($b->getY() - $a->getY(), 2));
        // Alternatively I can reuse the service as part of Repository layer
        // $distance = $this->service->getDistance($a, $b);
        $formatted_distance = number_format($distance, 2);
        print "insert $formatted_distance into table" . PHP_EOL;
    }
}

$a = new Point(1, 1);
$b = new Point(2, 2);

// here we pass business object(s) to Repository layer and do any pre-computation, conversion there
$repository = new Repository();
$repository->saveDistance($a, $b);

Here while $a and $b are different objects, in reality both points can be a part of some larger objects, so any interactions between the two points here, can be seen as interactions within one larger object.

Another way to ask my question could be "Is it okay to have Repository Layer be dependent on Service Layer", i.e. $repository = new Repository(new Service()); where any service related tasks are taken care of inside the repository layer.

5
  • 1
    I'm confused. Why are you converting floats to ints? Who is providing the floats? Is it the database that needs integers? Sep 1, 2022 at 18:46
  • Looking at my code I misspoke a bit. I have some code like this: $frameid = intval($row['id']);, where $row['id'] is a string, which later must be fed to a class method that expects an int as a parameter. Leaving it a string would cause a PHP type error. So that is the reason for which some values need to be converted from one type to another. It looks like my service-layer code reads some database values itself which come in as strings. With PHP strict_types turned on, the strings must be converted to ints or floats to keep PHP parameter-passing happy
    – Dennis
    Sep 1, 2022 at 18:54
  • 1
    Bob Martin often tries to emphasise that SOLID is context-dependent; i.e. the principles depend upon requirements and aren't a definitive guide on how to structure code. So a responsibility usually isn't something you'd identify in the code structure; responsibilities are typically in the minds of users/stakeholders who decide what behaviours the software must support, and what developers need to deliver. With that in mind the most reliable way to measure 'SOLID' is to try writing automated tests for each of your individual functional and behavioural requirements in isolation. Sep 1, 2022 at 19:12
  • Thanks, can you help clarify individual functional and behavioral requirements? In my case it looks like I am struggling with "converting object" and "saving object". Are you saying that I can define requirements as "saving a converted object", aka one use case. But I can also define them as "convert object" and "save (prepared & converted) object" (aka two use cases).
    – Dennis
    Sep 1, 2022 at 19:23
  • @Dennis Consider whether any other parties than just your code are interested in the data format. For example, if the conversion totally changed (but all existing data migrated to the new format) then would anybody else have a bad day because they depend upon the old conversion? Or is it enough that your own users are able to save/load their data? If the conversion is strictly just a necessary step for saving the objects in a persistent store then it seems only your program cares about the conversion, and that it's merely an implementation detail for the 'save' requirement. Sep 1, 2022 at 20:16

2 Answers 2

6

Whether your repo and your service layer should be separated or not is nothing you decide by just looking at the code and what is does. You decide this by asking yourself if the layers are ever going to be maintained, tested, evolved, used, or generated separately - or if it is quite unlikely that this will happen.

Some examples:

  • when you expect the service functions to be maintained by one team, whilst the repo functions are maintained by another, or

  • when your repository functions can be generated from some high level metadata, but your service layer must be written manually

  • when you expect different repositories for different types of storages, but all of them will use the same service functions

  • when your services are so complex that it makes sense to test them in isolation, without the repo involved, or vice versa

  • when you want to use / reuse service functions (for example, getDistance) without producing a dependency on the repository functions

then it is better to separate the services and repos into different layers. If not, you can most probably save the hassle and leave everything in one layer. Note this is not necessarily an either-or decision: maybe after looking at the list above, you decide to put parts of your current service functions into a separate layer, and other parts into the repository, since those are exclusively required by the persistence mechanism and tightly couple to it.

Bob Martin himself once wrote

And this gets to the crux of the Single Responsibility Principle. This principle is about people.

This is essentially what I am trying to tell here: separating responsibilities (for example, by layering) is not an end in itself, it is a means to an end. You separate two things if you need separate maintainability or evolvability. But if functionality is so much coupled together that separation makes changes harder and error prone, then it will probably be better not to separate.

2

I think you have misunderstood "repository".

A repository is meant to contain the code required to persist an object and retrieve it from the persistent storage.

So in your example you shouldn't be saving the distance, you should save (and retrieve) x, y and any other properties of the point object.

eg

class PointRepo
{
    void Add(Point p) {
       var sql = "insert into point (id,x,y) values (@p.id,@p.x,@p.y)";
       ...
    }

    Point Get(string id) {
        var sql = "select * from point where id=@id";
        ...
        return p;
}

If you need some report on calculated properties of Points, this should be done in code

class PointsReportGenerator
{
    Report Generate(Point[] points)
    {
        //whatever calc
        Return report; //save with a separate ReportRepo?
    }

}
6
  • hm good point ... perhaps I'd design it this way if I started from scratch. In my current production code I have a "god object" where I only save parts of it that are needed, some parts are derived (computed) from other properties of the god objects. in my case saving god object as-is is prohibitive for multiple reasons - it's huge, I will need to rewrite a lot of the Generate() methods to accommodate for that change ... so in short, in my example save the distance value to database, and then retrieve it and show that to end user, without having to derive it.
    – Dennis
    Sep 1, 2022 at 22:08
  • but regardless... I think you are suggesting to move the computation out of the save() repository layer, and instead move it to the view layer, yes?
    – Dennis
    Sep 1, 2022 at 22:09
  • @Dennis, to stay with your distance example: The Repository layer should not know how to calculate a distance, but it should be responsible for massaging a floating-point distance value into the format that the database schema expects (e.g. a string representation of a number with 2 decimals). Sep 2, 2022 at 5:49
  • 1
    its good to separate the calc and saving code, but you complicate the issue with the formatting which could be considered a responsibly of the datalayer as obviously you need to be able to read it back later and understand the formatting.
    – Ewan
    Sep 2, 2022 at 8:41
  • Good point, I am working with legacy code/database where some data in the database fields is saved like 8.23 mm +/- 0.05 mm, which is TERRIBLE. One one hand, it was all precomputed and saved, so all you have to do now is display the data as-is. But I ran into issues later when changes had to be made to the displayed data. In one case I reformatted the database to raw numbers & then added code to add the units and any other value decorations later. That said .... not sure what you mean by complicating issue with formatting. In my case end-user does want some values displayed in this format
    – Dennis
    Sep 2, 2022 at 15:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.