DEV Community

Semyon Kirekov
Semyon Kirekov

Posted on

Golang Dependency Inversion — A Java Developer's Point of View

I’ve been writing in Java for some years. But recently I started a new job, and I had to switch to Golang. Of course, there are many differences between these languages. Like error handling, concurrency model, encapsulation mechanism and other stuff. But in this article, I want to focus on one particular thing. And that is the dependency inversion principle.

Meme cover

I consider this pattern the most important in programming. The code is not perfect. Sometimes you have to break rules. Anyway, the dependency inversion principle is the one that keeps your code testable and modularised. If you break it, the entire codebase can quickly become a mess even if you consistently follow the other SOLI patterns.

So, I want to tell you how Golang features can help you push the dependency inversion principle to a new level.

Even if you don't know Golang or Java, it's fine. I tried to keep the examples easy and self-explanatory.

The case

We have three modules (or packages). Those are orders, users and delivery. We create a use-case for putting an existing Order into Delivery department. The steps are:

  1. Find Order by id.
  2. Get the User who posted the Order in the first place.
  3. Check that User is not blocked.
  4. Check that Delivery has the resources to process the Order delivery pipeline.
  5. If everything is fine, put the Order for the Delivery.

Java

Supposing we have three interfaces DeliveryService, UserService, and OrderRepository that provide methods needed for this use case (and probably other ones). Here is a potential declaration.

public interface UserService {
    User getById(long id);
    void create(User user);
    void update(User user);
}

public interface DeliveryService {
    // Some generic payload. The format is not important for the article
    boolean hasResourcesToDeliver(String orderPayload)
    void putOrderForDelivery(long orderId, String orderPayload);
    void cancelDelivery(long orderId);
    Status getDeliveryStatus(long orderId);
}

public interface OrderRepository {
    Order findById(long orderId);
    List<Order> findAll();
    void create(Order order);
    void update(Order order);
}

public interface OrderService {
    void initDelivery(long orderId);
}
Enter fullscreen mode Exit fullscreen mode

Now, let's write the possible OrderService implementation.

public class OrderServiceImpl implements OrderService {
    private final UserService userService;
    private final DeliveryService deliveryService;
    private final OrderRepository orderRepository;

    @Override
    public void initDelivery(long orderId) {
        Order order = orderRepository.findById(orderId);
        User user = userService.getById(order.getPostedById());
        if (user.isBlocked()) {
            throw new InitDeliveryException("User is blocked");
        }
        if (!deliveryService.hasResourcesForDelivery(order.getPayload())) {
            throw new InitDeliveryException("No resources to deliver");
        }
        deliveryService.putOrderForDelivery(orderId, order.getPayload());
    }
}
Enter fullscreen mode Exit fullscreen mode

In real application these calls between different services may put the system into inconsistent state (one call failed and the others didn’t proceed). Let’s omit them problems for the sake of simplicity.

You may notice that the OrderServiceImpl doesn’t require each method in those interfaces. Some of them are untouched. For example, OrderRepository.findAll, DeliveryService.getDeliveryStatus, or UserService.update. Meaning that the module implicitly imports unnecessary methods.

If you’re a Java developer, most likely you would leave it as is (I would definitely). Sure, you can declare another package-private interface that is used specifically by OrderServiceImpl. But that would result in one of these outcomes:

The actual implementations should also implement those specific interfaces

That would look like this:

public class UserServiceImpl implements UserService, LocalUserService {
    ...
}
Enter fullscreen mode Exit fullscreen mode

Where LocalUserService is the specific interface containing only getById method.

Another approach is to create separate implementations of those interfaces and delegate calls to OrderService/DeliveryService/OrderRepository

For example, something like this:

// package-private interface used by OrderServiceImpl
interface LocalUserService { 
    User getById(long id);
}

class LocalUserServiceImpl implements LocalUserService {
    private final UserService delegate;

    @Override
    public User getById(long id) {
        return delegate.getById(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

The first option means that the users module would have to depend on orders as well, because it has to import LocalUserService interface. And that is not good for isolation.

The second one does not break the modules’ boundaries. But it requires more boilerplate code (and probably a decrease in the whole test coverage). Technically, the solution is valid, but I think it’s not a real thing for the vast majority of cases.

Golang

Looking ahead, Golang allows us to solve the issue elegantly. But let’s keep things slow and start with the same setup.

type OrderService interface {
    InitDelivery(orderId int64) error
}

type orderServiceImpl struct {
    userService UserService // those are also interface types
    deliveryService DeliveryService
    orderRepository OrderRepository
}
func New(u UserService, d DeliveryService, or OrderRepository) OrderService {
    return &orderServiceImpl {
        userService: u,
        deliveryService: d,
        orderRepository: or
    }
}

// OrderService method's implementation

func (o *orderService) InitDelivery(orderId int64) error {
    order := o.orderRepository.FindById(orderId)
    user := o.userService.GetById(order.PostedById)
    if user.Blocked {
        return errors.New("user is blocked")
    }
    if o.deliveryService.HasResourcesForDelivery(order.Payload) {
        return errors.New("no resources to deliver")
    }
    o.deliveryService.PutOrderForDelivery(orderId, order.Payload)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

For now, there is no difference between Java and Golang approaches. But here comes one interesting Golang’s feature. You see, Java demands explicit interface implementation. But Golang doesn’t. If a structure has all interface methods with the same signature, the compiler thinks that this structure implements the interface implicitly.

This slight difference allows us to refactor OrderService in this way:

  1. Create local private interfaces containing only required methods (that are called inside InitDelivery function).
  2. Accept those interfaces as parameters of the New function.
  3. Pass the actual implementations on New function invocation without changing the code.

Take a look at the diagram below to understand the principle.

Interface impl diagram

And here is the refactored code:

type OrderService interface {
    InitDelivery(orderId int64) error
}

type orderServiceImpl struct {
    userService localUserService 
    deliveryService localDeliveryService
    orderRepository localOrderRepository 
}

type localUserService interface {
    GetById(orderId int64) User
}

type localDeliveryService interface {
    HasResourcesForDelivery(orderPayload string) bool
    PutOrderForDelivery(orderId int64, orderPayload string)
}

type localOrderRepository interface {
    FindById(orderId int64) Order
}

func New(u localUserService, d localDeliveryService, or localOrderRepository) OrderService {
    return &orderServiceImpl {
        userService: u,
        deliveryService: d,
        orderRepository: or
    }
}

func (o *orderService) InitDelivery(orderId int64) error {
    order := o.orderRepository.FindById(orderId)
    user := o.userService.GetById(order.PostedById)
    if user.Blocked {
        return errors.New("user is blocked")
    }
    if o.deliveryService.HasResourcesForDelivery(orderId) {
        return errors.New("no resources to deliver")
    }
    o.deliveryService.putOrderForDelivery(orderId)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

This change may not seem like a big deal. But it actually changes the way you think about software design:

  1. Your module has no direct dependency on other modules. This makes it easier to refactor and enhance external interfaces.
  2. If you have a local interface with dedicated methods, there is no need to mock/stub methods that aren’t called in this use case.
  3. Even if you have to change the signature of the calling external interface, it won’t break the code and tests within the current module. Sure, you won’t be able to pass the structure as a method parameter anymore, which would probably result in a compilation error. But the other modules that use this method are untouched. So, you can decide whether you need to refactor it or if it's better to pass a proxy implementation during initialisation.

Conclusion

At first glance, implicit interface implementation in Golang seemed to me inconvenient (especially in comparison to Java). But when you solve distinct problems, you understand the architectural reason behind this feature. Of course, it doesn’t mean you should create a local interface for every interaction. Sometimes it’s just easier to rely on the external interface. But having the ability to do it differently is a pleasant bonus.

Thank you for reading this article. If you switched from Java to Golang (or maybe vice versa), leave your thoughts down below. It will be interesting to discuss opinions. Have a nice day!

Top comments (0)