I just have an idea about dependency management in Spring IOC environment that seems to be better than the typical approach, but I am not sure because I can't find any references or example out there that talk about it, though the idea is very simple. I will explain the idea comparing to the typical approach. Please help me see if the idea is valid and if it has some formal theoretical name or are there some books talking already about it.
Typical Approach
In typical Spring application, when class A
has class B
as a dependency, we would inject the interface of B
into A
. For example, let's say we have class ProductManager
that depends on ProductService
.
class ProductManager {
private ProductService productService;
public ProductManager(ProductService productService) {
this.productService = productService;
}
public void foo() {
... // some logic
// somewhere in this method
String result = productService.bar1();
... // some logic
}
}
interface ProductService {
String bar1();
void bar2();
}
class ProductServiceImpl implements ProductService {
@Override
public String bar1() { ... }
@Override
public void bar2() { ... }
}
@Configuration
public class ApplicationConfiguration {
@Bean
public ProductManager productManager(ProductService productService) {
return new ProductManager(productService);
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
class ProductManagerTest {
@Autowire
private ProductManager classUnderTest;
@MockBean
private ProductService productService;
public void testFoo() {
when(productSerivce.bar1()).thenReturn("bar");
// do the test
}
}
In this approach, ProductManager
directly depends on ProductService
. It has some problems which I will explain in the last section.
My Idea
I want to make it that ProductManager
and ProductService
do not know each other completely. The ProductManager
depends only on its own defined Port
. We plug the ProductService#bar1()
to the port in configuration class.
interface ProductManagerPort {
String baz();
}
class ProductManager {
private ProductManagerPort port;
public ProductManager(ProductManagerPort port) {
this.port = port;
}
public void foo() {
... // some logic
// somewhere in this method
String result = port.baz();
... // some logic
}
}
// Notice we don't use interface anymore
class ProductService {
public String bar1() { ... }
public void bar2() { ... }
}
@Configuration
public class ApplicationConfiguration {
@Bean
public ProductManager productManager(ProductService productService) {
// in more complex case, we can create an adaptor class instead of using a lambda
return new ProductManager(() -> productService.bar1());
}
}
class ProductManagerTest {
@Test
public void testFoo() {
ProductManager classUnderTest = new ProductManager(this::mockBaz);
// do the test
}
private String mockBaz() { return "bar"; }
}
The Differents
To explain why I come up with the idea, I will compare the two approaches in the following topics.
Dependency Decoupling
In the typical approach, ProductManager
is tightly depending on ProductService
. Even though the dependency is bound via an interface, it only helps to hide the implementation detail of ProductService
from ProductManager
, but the ProductManager
still have to concern about how the ProductService
works in general.
Suppose you assign 2 developers to write code for each class: John writes ProductManager
and Marry takes care ProductService
. both persons will have to agree on the interface before doing their own work. John has some assumption in mind how ProductService#bar1()
works, and write the method ProductManager#foo()
based on that assumption. When Marry found something new during implementation that affects the agreement, she notifies John. Then John has to rework to make his code support the new assumption.
In My Idea approach, ProductManager
and ProductService
are completely independent. ProductManager
declares its own Port
. John can keep his assumption in the Port
without having to fear that Mary will find some problem during implementing the ProductService
. Mary, in turn, does not have to fear that her changes of design will bother anyone.
To integrate ProductService
into ProductManager
, we can create ProductServiceToProductManagerPortAdaptor
that implements ProductManagerPort
. If there is some mismatch in John's and Mary's assumption, it will be adjusted in the adaptor. In the example code above, I use lambda for the adaptor to exemplify possible simplification of the adaptor.
Single Implementation Anti-Pattern
The typical approach requires us to make the ProductService
an interface and put the implementation in ProductServiceImpl
. We are forced to do so to avoid wiring dependencies with concrete classes.
In My Idea approach, we are free to that limitation because the wiring is done through the class's own ports. We can write a straight forward code using concrete class and use interfaces where a true abstraction is really needed.
Interface Segregation Principle
The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.
Wiring component to component is easy to violate this principle. For example, ProductManager
only depends on ProductService#bar1()
but it is forced to know about ProductService#bar2()
just because they are packed within the same component. You have to read the implementation detail to be sure if ProductService#bar2()
is used in ProductManager
or not.
When writing test, in the typical approach, you are forced to use a mock framework to avoid unnecessary mocking unused method ProductService#bar2()
. While in My Idea Approach, you are required to mock only as much as the ProductManager
actually needs without using the mock framework.
Question
I want to encourage my team to use this pattern in our next project but I cannot find any references to support my idea. I am afraid that someone has already considered this idea and reject it due to some limitation.
Please share if you have experience using this pattern in your application or if there is a standard concept about it. I have looked into Hexagonal Architecture but it seems to be more about system architecture than a coding pattern.
return new ProductManager( new ProductAdapterManagerToService(productService) );
?