You’ll have to excuse my ignorance, I’m very new to Java.
Let’s pretend I’m trying to create a repository.
I have an interface such as this:
public interface Repository<T, K>{
T get(K id);
// some more methods
}
And then a few concrete classes that implement this interface
public class RepositoryOne implements Repository<EntityOne, Long> {
// methods
}
public class RepositoryTwo implements Repository<EntityTwo, Long> {
// methods
}
And then some service that uses one of the concrete repository interface implementations.
My question is: what is the correct way to write this service? Here are some examples, all valid from a syntax point of view.
1st – concrete constructor parameter, concrete class member
public class SomeService{
private final RepositoryOne repositoryOne;
public SomeService(RepositoryOne repositoryOne) {
this.repositoryOne = repositoryOne;
}
// methods
}
2nd – concrete constructor parameter, interface class member
public class SomeService{
private final Repository<EntityOne, Long> repositoryOne;
public SomeService(RepositoryOne repositoryOne) {
this.repositoryOne = repositoryOne;
}
// methods
}
3rd – interface constructor parameter, interface class member
public class SomeService{
private final Repository<EntityOne, Long> repositoryOne;
public SomeService(Repository<EntityOne, Long> repositoryOne) {
this.repositoryOne = repositoryOne;
}
// methods
}
4th – interface constructor parameter, concrete class member but cast the interface(this seems funky at best)
public class SomeService{
private final RepositoryOne repositoryOne;
public SomeService(Repository<EntityOne, Long> repositoryOne) {
this.repositoryOne = (RepositoryOne) repositoryOne;
}
// methods
}
Even more so, does it matter?
If you could justify your answer with a "why" that would be awesome. I’m trying to be better as a developer and understanding why is the first step.
>Solution :
1st – Concrete constructor parameter, concrete class member:
public class SomeService {
private final RepositoryOne repositoryOne;
public SomeService(RepositoryOne repositoryOne) {
this.repositoryOne = repositoryOne;
}
// methods
}
In this approach, you are directly using the concrete RepositoryOne class as the constructor parameter and storing it as a concrete class member. While this approach is simple and straightforward, it tightly couples SomeService with the specific implementation RepositoryOne. If you later want to switch to a different implementation, such as RepositoryTwo, you would need to modify the SomeService class. This reduces flexibility and makes the code less maintainable and extensible.
2nd – Concrete constructor parameter, interface class member:
public class SomeService {
private final Repository<EntityOne, Long> repositoryOne;
public SomeService(RepositoryOne repositoryOne) {
this.repositoryOne = repositoryOne;
}
// methods
}
In this approach, you are using the concrete RepositoryOne class as the constructor parameter, but the class member is of the interface type Repository<EntityOne, Long>. This provides some level of flexibility, as you can change the concrete implementation of the repository by passing a different class that implements the Repository<EntityOne, Long> interface. However, you still have a tight coupling with RepositoryOne in the constructor.
3rd – Interface constructor parameter, interface class member:
public class SomeService {
private final Repository<EntityOne, Long> repositoryOne;
public SomeService(Repository<EntityOne, Long> repositoryOne) {
this.repositoryOne = repositoryOne;
}
// methods
}
In this approach, you are using the interface Repository<EntityOne, Long> as the constructor parameter, which is a more flexible approach than the previous ones. Now, you can pass any class that implements the Repository<EntityOne, Long> interface to the SomeService constructor. This allows you to change the concrete repository implementation without modifying the SomeService class. This approach adheres more closely to the Dependency Inversion Principle (DIP) of SOLID principles, making your code more maintainable and easier to extend.
4th – Interface constructor parameter, concrete class member, but cast to the interface:
public class SomeService {
private final RepositoryOne repositoryOne;
public SomeService(Repository<EntityOne, Long> repositoryOne) {
this.repositoryOne = (RepositoryOne) repositoryOne;
}
// methods
}
This approach is generally not recommended because it involves a cast from the interface type to the concrete type (Repository<EntityOne, Long> to RepositoryOne). While it may seem to work if the concrete implementation passed is RepositoryOne, it could lead to ClassCastException if you accidentally pass a different implementation. Using this approach can break the Liskov Substitution Principle (LSP) and is not safe.
Conclusion:
The 3rd approach, using an interface as the constructor parameter and the interface as a class member, is the recommended way to write the SomeService class. This approach promotes loose coupling between classes, adheres to SOLID principles, and allows you to change the repository implementation easily without affecting the SomeService class. It provides better code maintainability, flexibility, and robustness.