7

I'm just a junior developer but in about half a dozen projects I worked on there's always this situation:

Service methods that take big objects as parameters, but often use just a little portion of them. Let me try to clarify with a little bit of code:

class WorkforceService {
    public void assignEmployeeToWorkplace(Workplace wp, Employee emp) {
        //this method just checks couple things that 
        //may prevent assigning that employee to that workplace
        //if everything's ok employee is assigned
    }
}

Difficulties that I have with the situation:

  • If I try to unit test this method, I need to create a full fledged workplace and employee object. These contain many other members and data, which may or may not be relevant to this method.

  • Auditing. Somewhere down the line I will be requested to audit these method calls. Those big parameters make it difficult to automate auditing, I can't just serialize and write them to a log because they are big and contain irrelevant data to the method. I need to hand pick the data I need to save.

I'm wondering these:

1) Is it supposed to be like this?

2) If not, where is the problem? Are those objects unnecessarily big and service method is ok, or is it service method's fault and somehow it should request only whatever it actually needs?

2
  • First of all I would consider if I really need a service class: medium.com/@wrong.about/… Nov 20, 2017 at 13:42
  • On the topic of testing being hard due to having to construct a large, complex object, have you been exposed to mocking yet? You could construct a lightweight mock that has a minimalist amount of information available. Practically a necessity when the objects being passed in are complex enough or depend on a lot of externals (eg, you'd typically want to mock a DB connection).
    – Kat
    Nov 21, 2017 at 19:31

5 Answers 5

5

Service methods are OK. And sometimes your domain model results in large objects, which is also OK. What you might need is some Interface Segregation, and possibly a context boundary.

Your Workplace and Employee objects have a large API surface that's mostly irrelevant to this service method. So why should you service have to depend on that?

If this is a problem (it not always is), then you may want to introduce interfaces that describes what functionality is actually required by your service. Now, the service no longer depends directly on your domain model.

To connect the domain model classes to your interfaces, either:

  • have the domain model classes inherit from the service interfaces, or
  • introduce adapter objects that translate calls through your interface into calls to the model class.

In this case, it would seem dubious to have your domain model classes inherit from service interfaces. That would lead to the domain model depending on the services, which is the wrong direction for these dependencies. More immediately, this would mean that all model classes would inherit from interfaces for each service, which makes the class definitions unwieldy and more difficult to extend. It also increases the likelihood of name clashes. So adapter objects would be preferable.

Such approaches are most valuable when these interfaces are used multiple times, not just for a single method. Otherwise, you are adding a lot of complexity with all these interfaces without much value in return. You have to weigh the difficulty of dealing with all those interfaces against the difficulty of using large objects.

It may also be the case that the service needs different data because it is part of a different problem domain. The service's idea of an Employee may differ from that used in the rest of your domain model. Then, it may be better to define its own bounded context for that service. The service then has its own domain model. Now you have to translate between the two domain models when you use that service, but to the caller this is not fundamentally different from wrapping an object in an adapter.

In this particular example it does not seem like the service should be in a separate context. However, thinking about different contexts in your application can be a way to determine why your current objects are so unwieldy. E.g. for a workforce management software, time tracking might be a different concern from salary information. So if your Employee class is meant for time tracking it should not also contain that employee's pay grade, payment information, or their home address.

1
  • 1
    It also fits ideas from capability-based security well. The service methods that performs these checks neither needs (nor should get) the full capabilities of the Employee object. An interface that narrows down the set of capabilities to just those needed is an excellent idea. Of course there's a tradeoff here. Lots of little interface adapter classes like that can create their own maintenance burden. Nov 18, 2017 at 20:15
3

1) Yes, it is supposed to be like this. Your example already contains the reason why I think so: "This method just checks couple things that may prevent assigning that employee to that workplace". This consistency logic right now needs some fields of the Employee and the Workplace class. And of course, refactoring that method to use the plain field values or more compact EmployeeSubset or WorkplaceSubset classes would give the same behaviour.

But you are lost if the domain rules change later, so they need some other fields. Then you face a huge refactoring of the callers for something that should be a local change.

2) Focussed on the assignEmployeeToWorkplace() method, the objects aren't unnecessarily big. It's only references passed to the method, and the caller surely already has the Employee and Workplace instances for some other aspects of its task.

And a core concept of Domain Driven Design is to use the domain objects wherever feasible. And, for assigning an employee to a workplace, the natural parameters should be Employee and Workplace.

As for your difficulties:

Yes I see them, but compromising the core architecture to make secondary tasks easier isn't a good idea.

2

Yes, this is normal. However, this shouldn't be a problem.

Testing

The challenge of integration testing is to make sure that there is no unexpected effect. If you test only expected behavior, on the parts of the object that you expect to be affected, there are big chances that you miss some surprises. Because unfortunately, even if your code uses only a part of an object, you can't be for sure that this part doesn't impact other parts or isn't impacted by the other parts.

For unit tests the situation is different. Especially if you're using TDD where test cases are implemented before the functionality itself. You can here rely on some techniques such as test doubles to cope with the unnecessary complexity.

Auditing

I assume that you mean logging of data (auditing is performed by auditors, and they'll use the logs that you've implemented).

Indeed, either you put all the object in your log, or you hand pick data. But the latter is somewhat time consuming. A way out of this mess can be to use multilevel logging:

  • The basic logging, would log just core events in your function (entry, some checkpoints, result, and exit), by using just a short description of your object (e.g. provided by a function that logs the id, and some minimalistic data that would be used in all the logs), just to document that the process took place with specific objects.
  • If the logging level is higher, you'd dump the full objects (either in the same log file, or in a different one).

Another approach could be to work with a strategy pattern. The idea is that the data that is logged along with the object ID is no longer either a short summary or a full dump as explained above, but a tailored list of fields. This could be implemented using a strategy pattern. So instead of asking yourself what to log at each step of your function, you would define the set of fields to be used in the log for your specific function, and systematically log all those fields.

It's a little more work. But instead of doing it for every log step, you'd write it only once for the function.

1

Is it supposed to be like this?

Not really, no.

If not, where is the problem?

It's a violation of the Interface Segregation Principle, which is the I in SOLID.

An alternative approach is to introduce small role interfaces that describe the contract that the client needs to complete its work.

That gives you many fine-grained interfaces that describe behavior, rather than one single course grained interface that gives you access to everything and the kitchen sink.

Upside: accepting a finer grain argument makes more explicit what you actually need. It's easier to implement a fine grain interface, which means that re-use is easier, as is creating the test doubles you might want to have.

Downside: more interfaces to think about. You need to make extra changes if you later discover that you need access to other parts of the implementation. Combining interfaces for a single argument can be clumsy. It's more work. It's also more names, and naming things is hard.

Are those objects unnecessarily big and service method is ok, or is it service method's fault and somehow it should request only whatever it actually needs?

The primary problem is that the service method isn't precisely describing what it needs. It may also be the case that the object has too many unrelated responsibilities, making it unnecessarily big.

One of the motivations for is the idea that "hard to test" often indicates that there are opportunities to improve the design.

0

Let me put it this way, it's normal, but it's not OK. A lot of projects work this way, but not many stop to think about this problem. Your instincts are correct, it's a smell.

The problem is simply a wrong domain model. Unfortunately it is not always simple to fix it.

First, the domain model is not a data model. Second, the problem is not technical, so it's not about adapters, interfaces, services, contexts or things like that.

The Blue Book has a quite extensive description of what DDD should be in the first 150-200 pages. It is a Domain Model (again, not Data Model), that fits the actual problem domain. You have to exercise your Model, talk and think with it, until the language fits.

Ok, how does that help you? Try to talk about the problem you are representing in your domain! Can you do that? For example: "An employee is assigned to a workplace." You probably can't, because the word "assign" is missing from your vocabulary. It's in a "Service", which is not really part of the domain. You can't say "WorkforceService" to a business person, can you? So you try this for example:

public interface Workplace {
    void assign(Employee employee);
}

Done. Ok, now you do to the next requirement. "Some workplaces only accept senior employees employed before a given date." Can your model do this? No it can't. Workplaces can decide to check anything, because the "assignment" logic is there, but the "employed before a given date" is not part of our vocabulary yet. So let's add it to the Employee:

public interface Employee {
    boolean wasEmployedBefore(Date date);
}

Now I can say "a specific Workplace can decide to assign only Employees who were employed before a specific date", and it all makes sense in my Domain.

And so on for all requirements. Obviously this is not always easy, and not always unambiguous. The book from Evans describes this process of breakthroughs and knowledge gathering quite well. This should be at the heart of DDD, not a Data Model as I suspect you have right now. A problem like what you are having is a good opportunity to learn more about the domain and adjust it accordingly.

0

Your Answer

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

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