Lazy-loading is a Code Smell
Have you ever seen those huge objects with many attributes? These domain objects where we are using lazy-loading because we do not want to retrieve too much information from the database? I bet you’ve had this doubtful pleasure.
Today I want to share with you my impressions about them – using lazy-loading should be treated as a code smell!
Let me explain myself:
- Lazy-loading means that sometimes you won’t need some attributes of an object. Those attributes will be necessary in a different context. Doesn’t it mean that you are building different objects depending on context?
- The functionality that is using this object knows definitely too much. It knows the API of the object and this API contains also the methods that require attributes which were not loaded. Great, isn’t it?
- You have to remember what is needed in each place and what is not needed …
- … and, what is even worse, you have to remember what you may use and what methods are not supported in a particular place.
In case it is not enough for you, let me elaborate.
How lazy-loading works
In short, lazy-loading allows you to NOT load children when loading the parent. It loads them only when you explicitly ask for it.
How does it work? Let’s take a look at a simple example:
class User { private final Name name; @OneToMany(fetch = FetchType.LAZY) private List<Role> roles; @OneToMany(fetch = FetchType.LAZY) private List<Subscription> subscriptions; // Some more attributes and methods }
What does the definition of this class tell you? What does FetchType.LAZY mean to us? This gives us information that lists which contain user’s roles and subscriptions won’t be filled with data until we explicitly ask for such data.
What is Bounded Context?
Bounded Context is one of the main patterns in Domain-Driven Development. It helps you work with large domain models by dividing them into different contexts. Thanks to this your domain’s objects become smaller and business logic of your application becomes easier to understand.
Code smell? But… why are?
In one of the previous paragraphs I wrote what the definition of the User class tells us. Till now it’s been all about mechanism. Now we may go further.
Let’s take another look at our class:
class User { private final Name name; @OneToMany(fetch = FetchType.LAZY) private List<Role> roles; @OneToMany(fetch = FetchType.LAZY) private List<Subscription> subscriptions; // Some more attributes and methods }
Can you tell me anything more about this object except the things that were already mentioned?
We know that we are working with the class whose objects are used in places where roles may be needed, but don’t have to be. Where subscriptions may be needed, but don’t have to be. The name is always required.
We know that there are functionalities/places in our application/situation where those attributes are required and there are some where those attributes are useless.
But… we have to go through the code to find those places. It takes time and effort. Unfortunately, there’s also a chance that we will miss some places.
The things we know… the things we don’t…
Wouldn’t it be better to know where and what is required? Of course it would! The question is: how to achieve it?
Let’s do a short analysis of our example:
class User { private final Name name; @OneToMany(fetch = FetchType.LAZY) private List<Role> roles; @OneToMany(fetch = FetchType.LAZY) private List<Subscription> subscriptions; // Some more attributes and methods }
We already know a few things:
- The name is always required.
- Sometimes we need roles.
- Sometimes we need subscriptions.
Based on this information we can add a one more thing – we know that we not always need to have all this information. Maybe it sounds like something trivial, but it is also important.
That’s all about information. Now is time for the unknowns:
- Is there any place where we need both roles and subscriptions?
- Are roles and subscriptions needed in different places?
- Is there any place where we don’t need either?
- Does it depend on the context what attributes will be needed?
The problem with unknowns is that we have to go through the code to find answers to them. But that’s not the end of the problems. When you finally find those places, there is no method or variable or anything to rename to NOT lose this information within a time. Next time you will have to repeat the effort.
Let’s improve the code
Because of the unknowns listed in the previous paragraph, it is not so easy to change the existing code, the real one, the one we are working with. That’s why I suggest that you make this change just after the moment when you think about lazy-loading. That’s the right time when the improvement is cheapest.
Ok, but how could we improve the code from the example?
The first thing to do is to find answers to the unknowns. Without those answers we cannot move forward. In our case I will assume that we recognized three different contexts:
- Authentication and authorization are the places where we need user’s name and its roles.
- We need user’s name and its subscriptions in the place where we handling with report sending.
- In other areas of our application we don’t need neither roles nor subscriptions.
Now we can refactor the User class and split it into something easier to understand:
class AuthUser { private final Name name; private List<Role> roles; // Some more attributes and methods } class ReportUser { private final Name name; private List<Subscription> subscriptions; // Some more attributes and methods } class ApplicationUser { private final Name name; // Some more attributes and methods }
Now we have got three classes instead of one, but we also have more information in our code. We won’t need to go through the code to find out what and where is needed. It would be just enough to open the definition of the class
What’s next?
Unfortunately, to achieve presented state in your domain you have to invest a lot of effort. Why? Mostly because of unknowns. The bigger the application, the harder it will be to get all information. That’s why I encourage you to split your classes just after you will think about lazy-loading as a solution.
If you already have lazy-loaded references in your domain you should refactor only the part you already working with. You will minimize both the risk of the change and the effort needed to make it. And the code will become more descriptive anyway.
Good luck!
Reference: | Lazy-loading is a Code Smell from our JCG partner Sebastian Malaca at the Let’s talk about Java blog. |
Hello, Although I do agree that the first class breaks the Single responsibility principle, I believe that it is also not designed correctly. Being a lazy class, the private state should represent something allusive to its content, therefore, should not use List, but something like Supplier<List>. I’ve seen some implementations of lazy values that are self explanatory, like ILazyValue<List>, which for me implies that, on the first get, the value must be initialized. This brings me to my second point, in which I agree that lazy values can lead to bad code. The value initializer will hold references to potentially… Read more »