6

I'm building a Spring backend. I've got a controller which gets a "search object" - an object with like 10 fields which only one of them should be filled, so the search function (which I did not write but need to make changes to and refactor) is written like this:

if( param1 != null ) user = getUserByParam1(param1);
else if ( param2 != null ) user = getUserByParam2(param2);
.
.
.
else if(lastName != null || lastName != null) user = getUserByName(firstName, lastName);
else user = getUserById(id);

if(user == null) throw costumException;
return user;

Notice the 2 special cases in the end- one of them checks the availability of 2 parameters instead of one and sends them both to same function (which can handle null in one of the fields, but not both), and the default case which assumes an ID is being passed (in case its not- it gets handled by the if (user == null) check after to throw exception).

Is there a way to refactor this piece of code to be more readable/good looking? Is there any design pattern or a known method I can use to do this? Or is it actually the best way to write this kind of functionality?

I've done a lot of thinking but couldn't find a nicer way to do so. I've had an idea to somehow send the field name that is filled and its value to another function which will "switch-case" on the field name and send the value to the appropriate function, but it doesn't really save much code (as I still need to manually iterate on all fields to find the one which is filled) and I'm not so sure it'll more readable.

I'm also quite new to Java so I do not know all the available APIs and interfaces, maybe there is something you can throw at me for help.

Zaki
  • 6,997
  • 6
  • 37
  • 53
Gibor
  • 1,695
  • 6
  • 20
  • 1
    How many different cases of getting a user do you have? – Lino Jan 09 '20 at 08:18
  • Is your search object is some type of Map? – Dakshinamurthy Karra Jan 09 '20 at 08:18
  • check whether can you ue predicate API in Java 8. – Pramod S. Nikam Jan 09 '20 at 08:20
  • 1
    The problem here is that the endpoint is trying to do too much. 10 parameters is way too much to handle in a single endpoint, and since they're mutually exclusive ideally you should have one endpoint handle one parameter. The complex parameter object and single endpoint doesn't seem to give you any advantages, at least based on the code provided. Very much related: https://softwareengineering.stackexchange.com/questions/388026/is-it-bad-practice-to-use-a-single-endpoint-to-do-multiple-similar-tasks – Kayaman Jan 09 '20 at 08:26
  • I am also searching somthing like this – spandey Jan 09 '20 at 08:28
  • 1
    Would this not be better asked in the software engineering or code review exchanges? – Gavin Jan 09 '20 at 08:52
  • Why are you checking if lastName is not NULL or not NULL? – alexherm Jan 23 '20 at 19:48

3 Answers3

7

Note: This is simply a workaround for your quite monstrous method. As mentioned in the comments, your endpoint tries to do too much. It would be much better to create many different endpoints which all only need the required parameters. Makes it a lot easier to understand than just to see 10+ if-else statements


You could create a data-class containing all the possible parameters:

public class UserRequest {
    private String lastName;
    private String firstName;

    // more fields, constructors, getters, setters etc.
}

Then have an interface looking something like this:

public interface UserRequestResolver {
    User resolve(UserRequest request);
}

This interface can then be implemented to depend on a given param, if it exists. Return a found User or simply null.

Next step is, create a List<UserRequestResolver> resolvers, and add the different implementations, with Java8+ you can use lambdas:

resolvers.add(r -> r.getParam1() != null ? getUserByParam1(r.getParam1()) : null);
resolvers.add(r -> {
    if(r.getFirstName() == null && r.getLastName()) return null;
    return getUserByName(r.getFirstName(), r.getLastName());
});
// etc.

Then when receiving a UserRequest you can simply iterate over resolvers and take the first return value which is not null:

for(UserRequestResolver resolver : resolvers) {
    User user = resolver.resolve(request);
    if(user != null) return user;
}
throw costumException;

If you're a fan of Streams, then you can replace the for-loop above with this:

return resolvers.stream()
    .map(resolver -> resolver.resolve(request))
    .filter(Objects::nonNull)
    .findFirst()
    .orElseThrow(() -> costumException);
Lino
  • 19,604
  • 6
  • 47
  • 65
  • 1
    Indeed the if/else-ladder can be changed into a different form, for example the more complicated form presented here. I believe the DRY principle may cause some people to see it as unnecessary duplication if the 10 different ways of retrieving a user aren't all sharing "as much code as possible" (and in that case, the if/else would still be the simplest one). Good answer. – Kayaman Jan 09 '20 at 08:45
  • 1
    @Kayaman I do agree, I presented an option which may be appealing to the eye, but introduces a lot of complexity. Namely: 1) introducing a `class` and an `interface`, 2) having to initialize `resolvers`. To note is also that this solution will be alot slower than some simple one-liner if-else statements – Lino Jan 09 '20 at 08:48
  • Thanks for the detailed answer! I really like this way, but do you really think my endpoint is doing too much? after all, it sounds to me that the "single responsibility principle" applies here pretty strongly as all I do is "search"- jsut by different params. Also that would introdue a front-end load, since this is a "seach" form that we require only one field to be inserted. FE would have to do the iteration itself, which would surely be slower and more complicated than in backend. @LinosaysReinstateMonica – Gibor Jan 09 '20 at 08:54
  • 1
    @Gibor it sure does too much, because you provide multiple different search methods which all exclude each other (you can't search by `param1` and `param2` at the same time). I don't know how you receive your search criteria, but the whole thing sounds a bit smelly if you ask me. Regarding your concerns with the frontend load, you have to know that javascript is incredibly fast. Some iterations are no problem for it ;) – Lino Jan 09 '20 at 09:00
  • @LinosaysReinstateMonica yeah I see what you're saying here... I just thought I can tweek your method a bit so like non one-to-one search criterias (like name and family name) will come first, give me list of drivers and then I keep applying non-null resolvers until the list size gets to 1. It can actually really help me since I need to keep current function to support return of a single object and also make another endpoint to support return of multiple objects (can't combine and return size 1 list since this search function is already heavily used in product). I'll try to implement, thank! – Gibor Jan 09 '20 at 09:23
1

Depending on what you use to access the data, you can use getByExample query. You have an example of using it here with Spring data. With this you only have to get the user class from the api (from you api you must receive and object for the search query, this object must be somewhat like the user object as the parameters looks alike).

And then where you have your big if/else you pass the user search object from the api and in the repository do a getByExample.

user = getByExample(userSearchObject)

But this is all assuming you receive in your api only one field filled in your object. This would be the most concise way to do it.

Otherwise I would suggest wrapping all your parameter into a class in the api like this:

public class UserSearchQuery() {
    private String param1;
    private String param2;
    ...
    
    // getters + setters
}

and then use criteria query and criteria query builders (An article about it here).

In your service you would only have to do this:

public class UserService() {

private UserRepository repository;
   public User search(UserSearchQuery query) {
      return repository.search(query);
   }
}

and in the repo :

public class UserRepository() {
   private EntityManager em;
   
   public User search(UserSearchQuery query) {
      CriteriaBuilder builder = em.getCriteriaBuilder();
      CriteriaQuery<User> query = builder.createQuery(User.class);

      Root<User> user = query.from(User.class);
      List<Predicate> predicates = new ArrayList<>();
     
      if (query.getParam1() != null) {
          predicates.add(builder.equal(user.get("param1"), query.getParam1()));
      }
      ...
      // and the other ones goes here
   }
}

Doing so you would only have 1 search method for your User and no longer 10 methods, one for each parameters, and to add a search criteria you would only have to add a new parameter to the UserSearchQuery class and add a new predicate. Also if someday you would like to be able to use multiple parameters as search criteria, it would already be done.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
A Le Dref
  • 422
  • 3
  • 7
  • Thanks for the answer! This sounds amazing, but it requires changing of a WHOLE lot in our backend siince we heavily use those search functions everywhere. I'll keep that API in mind tho for our upcoming huge refactor in a few month- this can really help us reduce number of functions and make everything much muhc more genric. Thanks again! – Gibor Jan 09 '20 at 09:06
  • 2
    Glad I could help you! Then I think for now you do not have lots of options excepts a big if-else ... – A Le Dref Jan 09 '20 at 09:18
0

You can send all the fields in a request parameter and then use command design pattern or strategy design pattern to implement different implementation for different case.

Example for command pattern is as below:

https://sourcemaking.com/design_patterns/command

Strategy design pattern: https://sourcemaking.com/design_patterns/strategy

Ankur Gupta
  • 312
  • 1
  • 3