2

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.

4
  • 1
    Sounds like you've re-invented the Command Pattern. Commented Apr 16, 2019 at 15:29
  • When you say "create an adaptor class instead of using a lambda" do you mean something like this: return new ProductManager( new ProductAdapterManagerToService(productService) );? Commented Apr 16, 2019 at 17:02
  • @candied_orange yes, exactly. Commented Apr 16, 2019 at 17:55
  • Who is going to write the adapter? Marry or John? Commented Apr 20, 2019 at 12:35

4 Answers 4

0

The structure of your two approaches is practically the same, but you approached the design process differently – and that difference matters.

In the first solution, the interface describes the full capabilities of the “default” implementation. That makes it look like an almost useless indirection. As you've correctly noted this also makes it easy to accidentally violate the ISP and to add unwanted coupling to the system. However, such approaches have fairly good tooling support.

In your second solution, your interfaces were guided by how the object will be used. That's how it should be and how OOP works best. It's not quite how Java works best, so you have to introduce adapter classes. In your cases these adapters are invisible through the use of lambdas or method references, but that only works for single-method interfaces.

Usage-guided interface definitions occur in a number of places:

  • You've already noticed that in the Hexagonal/Onion/Clean Architecture, dependencies flow inward so that interfaces are defined at the point of use, not the point of implementation. While this is an architecture-level pattern, the same principles hold on a small scale.

  • Programming by Wishful Thinking is a small-scale variant of this: we solve a problem top-down by breaking it down into smaller, easier parts. Let's write the code as if a solution to that smaller problem already exists. Later, somehow implement the required functionality. If we need some object, we can first write the interface to denote what we require from the object, and later see how that object can be provided. This technique was popularized by SICP.

  • TDD also ties in to this. A TDD practicioner iteratively tries to write the simplest possible test that could fail, write the simplest possible code to make it pass, then refactors the solution into a suitable design. At each of these steps, expressing the solution in terms of a yet-to-be-implemented interface can often be the simplest approach. Which I can then flesh out during the next TDD iteration.

  • The object system of some languages is well-suited for adapting existing types to new interfaces, e.g. Go or Rust.

Such interfaces or design approaches help keep each piece of code small and simple, and keep interfaces relevant to the context in which they are used. However, such approaches tend to produce many more objects that each do very little actual work. This can also hurt maintainability, especially in a language like Java where supporting declarations typically get their own file. It is also possible to go overboard and declare an interface for every single method that you would call. That is no longer useful.

So it is probably best to apply such strategies when:

  • dependencies cross domain or system boundaries (e.g. interfaces that represent external systems, functionality provided by third party libraries)
  • the using code needs to be exceptionally clear and simple (usage-guided interfaces make the using code more self-documenting)
  • the software has detailed tests (e.g. is developed with TDD, and wants to mock other functions)
  • the code is not written in Java or C#

But most importantly:

  • the code doesn't want to hard-code certain decisions (e.g. so that business rules can be swapped via the strategy pattern)
4

You should manage dependencies not fight them.

If class A needs to send an email and class B can send emails then class A will depend on class B and no amount of abstraction and redirection is going to make it that relationship go away.

When you write:

In My Idea approach, ProductManager and ProductService are completely independent.

I’m afraid you are mistaken. The relationship is still there you just hide it behind some porcelain.

You will find that building and maintaining those ports and adapters will not be any simpler than the “traditional” solution so you will try to come up with a system that automatically generates those adapters. Maybe even utilize some domain specific language to define the ports? Congratulations, you just invented interfaces. What you will end up with will be a prime example of the inner platform effect:

The inner-platform effect is the tendency of software architects to create a system so customizable as to become a replica, and often a poor replica, of the software development platform they are using. This is generally inefficient and such systems are often considered to be examples of an anti-pattern.

2

You are not using objects right.

The "conventional" design you start from is to use a bunch of "Managers" and "Services" which basically have procedures that work with some data from somewhere else. In this typical design, there is no real (i.e. conceptual, i.e. business-relevant) relation between these things, they are purely technical. So you are not using dependencies at all for anything valuable, therefore (understandably) you want to hide them completely.

Better designs use dependencies as part of their story. Dependencies are just another tool to tell the reader some business-relevant information, some connection or relation to help her better understand the software. In these designs, there is obviously no hurry to hide these dependencies, since they are part of the application.

So the problem is you are not using dependencies, you are just forced to have them. Which leads to your technical solution to hide them even more.

1

As far as the mechanism of dependency injection goes, both of these are pretty much the same thing; the only difference is how you go about it. That is, both of these are a form of dependency injection. But there is (likely) an important difference in semantics.

Generally, you want to combine dependency injection with dependency inversion. There, you have two components1 and an abstraction of some sort that both of the other two components depend on. (BTW, the abstraction doesn't have to be represented by an interface). This abstraction needs to represent some generalization of different possible dependency implementations. There is no general prescription on how to design this abstraction, as this depends on your understanding of the domain (and it may change as the application evolves). But it needs to be based on relatively stable concepts (it should change less often then the components that depend on it), and it needs to be workable (an abstraction is no good if it's not technically feasible - e.g., results in unacceptably poor performance).

Now, the key difference between the two cases that you've shown is how this abstraction is conceptualized. In the first case, the expectation is that different dependency implementations can all be effectively expressed & realized through the bar1() and bar2() methods. In the second case, the abstraction supposes that dependencies can be effectively expressed & realized through the baz() method. That's the important bit; weather you use inheritance or a wrapper is a secondary concern.

Which one of the abstractions is better depends entirely on which one of these two works better for the domain you are trying to model (and within the context of the tools and languages you are using). That's something you and your team will have to think about. You may also want to postpone that decision until your understanding of this part of the domain (and of the change patterns in this part of the code) becomes greater; you don't want to lock yourself into a wrong abstraction.

A side note: In the first approach, the ProductService interface, at least at first glance, doesn't really provide an abstraction of ProductServiceImpl, as both of them have an identical set of methods. I suspect that's part of the reason you dislike it. But consider this; if ProductServiceImpl is your only example of a ProductService implementation so far, you may be at a point where you lack enough information to come up with a useful abstraction, so you may want to wait to see how the code will change (and if it'll change at all), and what will other implementations look like. On the other hand, it could turn out that the all the changes will be happening behind the ProductServiceImpl class (so, in its own internal details, or in its own dependencies), i.e., the software will grow "behind" that class. In that case, you may even decide to scrap the ProductService interface entirely, and use the ProductServiceImpl class as the abstraction instead.


1 I use the term here in a general, loose sense - just some piece of code

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.