Abstracting your dependencies

Part 3 of 3 in our Modular Vue.js Applications series.
Written by Filip Rakowski

In the previous parts of the series we discussed the concept of a domain and how it could correlate to building independent and easily maintainable modules in your application. Most of the introduced concepts concentrated on separating parts of your app and avoiding tight coupling between them. Even if you make your modules independent there are still things that will lead to significant and time-consuming rewrites once you have to replace them - third parties.

In this article you will learn how to make your app independent from them as well!

Why modular architecture is not enough?

Imagine the following scenario: You started your company 4 years ago and decided to use headless WooCommerce as your backend platform and Nuxt for the frontend because back then it was the easiest setup for you as self-thought web developer. Over time your business grows and WooCommerce is no longer a viable choice for you. You decide to replace it with a more sophisticated platform like Commercetools. Even though you have your frontend decoupled from the backend and the frontend functionalities are staying the same you still have to write your shop business logic (almost) from scratch for the new platform because all your code is depending on WooCommerce data formats and it’s APIs.

The above example perfectly illustrates that making your app modular is not enough to make sure it won’t suffer from heavy rewrites in the future. In fact nothing will, but there is still a lot we can do to make it less likely to happen. Making your modules not coupled with each other is one thing, but every app relies heavily on different third-party services and libraries.

Solving the above problem is one of the main reasons why we created Vue Storefront. The eCommerce tech landscape is changing so fast that changing parts of the tech stack is something that clients do regularily. Combine it with API-first approach where instead of using a single eCommerce platform to manage the whole shop you use a combination of specialized services, each focusing on a small subdomain of eCommerce space like PIM (Product Information Management), OMS (Order Management Software), Taxes, Carts etc.

If it sounds to you like a never-ending rewrite and endless investments - you’re absolutely right and the eCommerce industry is not the only one that shifts towards this direction. The API-first approach is taking the web development world by storm!

So, what we can do to avoid time-consuming and expensive rewrites every time we decide to change some third-party in our app?

Use abstractions

A common solution to this problem that we can learn from Object-Oriented Design Patterns like Adapter or Bridge is to use abstractions. Instead of using third-party service (like eCommerce platform or a CMS) APIs directly we create an interface that connects it with our application and this interface is the only piece that connects the service with the rest of app.

A good mental model that helps understanding this concept is thinking about this abstraction as an adapter. If your computers only port is USB-C it is compatible only with USB-C cables unless we find an adapter than can be used by HDMI or Display Port. This is exactly what we will be doing for our third-parties.

Creating the right abstraction

To find a good abstraction you have to find similarities between all the services of a given type and create a general-purpose interface that all of them could fit into. Writing abstractions is not easy and you will usually do it wrong the first couple of times until you will connect enough services to find the similarities in how they work. Don’t get discouraged -it’s still much less work than rewriting half of your codebase when you have to change an important backend service.

When I’m thinking about the right abstractions I usually research few services of a given type and try to achieve a few use cases that I know I’ll be using extensively in my app. Once I have a few code samples I write down their differences and similarities. Based on them I create the simplest and most general possible API and try to write adapters for each service to fulfill it.

Of course your process could be completely different. Everyone are figuring out these things in their own way. What I’m sharing is a pattern that so far have worked best for me. It could look time-consuming but the more you use it the more things you will be able to catch and figure out in your head without even writing the code! Think of it as “measure twice, cut once” for our code.

Learn Vue.js 3 With Vue School

Side Note: I highly encourage you to read this article about abstractions by Eric Elliot! He has a great gift of explaining complex terms in easy words.

Abstracting Payment Methods

Let me show you a (purposely simplified) example that illustrates this process. Let’s imagine we have two payment providers in our application. One of them redirects the user to the other page where he/she can make the payment and other opens the modal window within the existing URL. In both cases we want to end up in a Thank You page or Error page after payment.

// redirects to the payment page (like redirectpayment.com/payment)
// once payment is accepted user is redirected to "returnUrl?status={paymentStatus}"
RedirectPayment.makePayment(cartId, returnUrl)

// opens the modal window with payment
// once the payment is done modal window is being closed and "afterPayment" called
ModalPayment.pay(cartId, afterPayment(paymentStatusCode))

Let’s sum up the differences:

  • The payment function names are different
  • RedirectPayment accepts the URL while ModalPayment accepts a callback function
  • For RedirectPayment we can’t rely on apps state while conveying the data from before to after the payment state.
  • From the documentation we also know that they send different status codes and payloads

Seems like we’re dealing with completely different things right? If you take a closer look though you will see a lot of similarities as well!

  • Both payment methods use only one function to perform the payment
  • Both payment methods return the payment status code
  • Both need a cartId to know about the payment details

Let’s use the similarities to create our abstraction and then see if we can write an adapter for each of them that will have the differing parts inside of it.

According to our list what we need is a single method that accepts a cartId and returns the status code:

AbstractedPayment.pay(cartId: string): status: string

Once we have the interface let’s see if we can write an adapter for each of our payment methods!

With ModalPayment it’s a piece of cake:

AbstractedModalPayment.pay = function (cartId) {
  function afterPayment(paymentStatusCode) {
    if (paymentStatusCode === 'ACCEPTED') {
      window.location.href = " ";
    } else {
      window.location.href = "http://www.myapp.com/payment-error";
    }
  }
  ModalPayment.pay(cartId, afterPayment)
}

RedirectPayment turns out to be a more challenging one. Since there are two URLs that we’re redirecting our users to and we can provide only one of them to RedirectPayment.pay()method we clearly can’t handle its payments without writing code outside the AbstractedModalPayment.pay() function. We need to figure out how our abstraction should change to fulfill this scenario.

Adjusting the abstraction

Because we’re doing the redirect based on the returned status code it means that we can’t rely only on the abstracted pay method. We need another function that will be invoked after the payment and, depending on the status code redirect to the proper page.

Let’s see if we can achieve this use case by adjusting our abstraction to expose additional afterPayment(statusCode) function. We already know that it will work for AbstractedModalPayment since ModalPayment.pay() needs the same function so I will focus on abstracting RedirectPayment only.

AbstractedRedirectPayment = {
   pay: function (cartId) {
     // we will run "afterPayment" on "after-payment" page
     RedirectPayment.pay('http://www.myapp.com/after-payment')
   },
   afterPayment: function(paymentStatusCode) {
     if (paymentStatusCode === 'SUCCESS') {
       window.location.href = "http://www.myapp.com/thank-you";
     } else {
       window.location.href = "http://www.myapp.com/payment-error";
     }
   }
}

It seems that we finally have the right abstraction for our payment methods! Once we figure it out it’s always worth thinking for a while if there are any things that are specific only to these two payment methods (and not a whole family of them) and if we could generalize them to be useful for all of them. The more interfaces you challenge your abstraction with the better, but don’t get obsessed about it.

Summary

One of the easiest ways to keep your apps maintainable and scalable is making its parts independent from each other. During this mini-series I wanted to share with you some ideas on how it could be achieved but this is just the top of the iceberg. From my experience making a modular app with a good communication between modules and properly abstracted third-parties is extremely hard. It may seem as one simple example but with real-world applications you will have to integrate many services, make many mistakes, and adjust your approach multiple times to finally learn how to make things right. And this is great! To me, the fact that we can constantly learn new things that will improve our future choices is what makes our jobs most exciting!

Learn Vue.js 3 With Vue School

Leave a Reply

Your email address will not be published. Required fields are marked *

Up Next:

Teleport - a new feature in Vue 3

Teleport - a new feature in Vue 3