Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Dependency Injection without classes
Cherif Bouchelaghem
Cherif Bouchelaghem

Posted on • Edited on

     

Dependency Injection without classes

Dependency Injection (DI) is a pattern that enhances the maintainability, scalability, and testability of your code. In TypeScript, we can leverage higher-order functions to implement Dependency Injection seamlessly, promoting a more modular and flexible codebase.

While it may sound like a complex pattern, many tools out there make it look very complicated. However, the reality is that DI is simply passing parameters, mostly to the constructor. As Rúnar Bjarnason casually once said, dependency injection is "really just a pretentious way to say 'taking an argument.'"

To provide a short and easy example, let's imagine we are working on a cab (taxi) Uber-like application. We've received a request to implement a rides pricing feature. For the sake of this article, the only rule is that the price of a ride must be $2 per kilometer for a given route.

Example

  • The distance from my house address to the airport is 20 KM for the available itinerary.
  • The price returned by the application should be 20 * 2 = $40.

We'll follow the domain model:

typeAddress={street:string;city:string;}typeRoute={origin:Address;destination:Address;};typeItinerary={route:Route;distance:number;}typePricedRide={itinerary:Itinerary;price:number;}
Enter fullscreen modeExit fullscreen mode

However, the implementation of the ride pricing feature needs to know the itinerary to calculate the price based on the distance. The route itinerary can be fetched using a third-party service like Google Maps, Google Addresses, or others. Let's assume we haven't made a decision on which service to use.

Implementation using class-based Dependency Injection:

Since the itinerary 3rd-party service provider is not known yet, we can abstract it behind an interface and inject it into the class that calculates the price as a constructor parameter, like the following:

interfaceItineraryService{getItinerary(route:Route):Promise<Itinerary>}classRideService{constructor(privateitineraryService:ItineraryService){}asyncpriceRide(route:Route):Promise<PricedRide>{constitinerary=awaitthis.itineraryService.getItinerary(route)constprice=itinerary.distance*20;return{itinerary,price}}}
Enter fullscreen modeExit fullscreen mode

Let's break down the code above

  • ItineraryService is an interface that exposes thegetItinerary method signature. The implementation details of that interface can be done once a decision is made on what itinerary service provider to use, or even use a fake implementation.
  • RideService class has a constructor that takes an ItineraryService implementation instance. It doesn't matter what the concrete implementation to use is since it respects the interface contract.
  • RideService exposes thepriceRide method that takes a Route object and returns a promise of aPricedRide object after getting an itinerary for the passed route.

I kept the code short by removing some details like error handling.

Towards function-based DI

Now let's try to get rid of that verbose class-based version and replace it with another version that uses just functions. Since DI is basically passing parameters and functions are first-class citizens in JavaScript and TypeScript, we can just use a function to calculate the price and pass the itinerary fetching dependency as a function as well. The class-based code can be refactored to:

constpriceRide=async(route:Route,getItinerary:Function):Promise<PricedRide>=>{constitinerary=awaitgetItinerary(route)constprice=itinerary.distance*20return{itinerary,price}}
Enter fullscreen modeExit fullscreen mode

The code is now shorter and simpler, isn't it?

We have a small issue in the function implementation above:getItinerary function type accepts any function. Let's fix this by adding the following type to our domain model:

typeGetItinerary=(route:Route)=>Promise<Itinerary>
Enter fullscreen modeExit fullscreen mode

Dependencies First, Data Last

If we want to mimic the class-based approach, we can justFLIP the order of thepriceRide function arguments like the following:

constpriceRide=async(getItinerary:GetItinerary,route:Route)=>{constitinerary=awaitgetItinerary(route)constprice=itinerary.distance*20return{itinerary,price}}
Enter fullscreen modeExit fullscreen mode

Flipping the arguments order as above is more of a functional style, where the input data are always set as the last function argument. This will allow us to partially applypriceRide.

Partial application

It is a functional programming concept that can be applied to create a new function by just passing some arguments to a function. For example, we can uselodash.partial to get a new function from ourpriceRide like the following:

constpartialPriceRide=_.partial(priceRide,getItinerary)// create a function with a dependencyasyncpartialPriceRide(route)// -> ride price
Enter fullscreen modeExit fullscreen mode

WheregetItinerary and route are concrete implementations.

This is exactly the steps to make to use the class-based version:

constrideService=newRideService(itineraryService)// create an object with a dependencyasyncrideService.priceRide(route)// -> ride price
Enter fullscreen modeExit fullscreen mode

Great! Now, since partial application is a function that returns a function, instead of usinglodash to create ourpriceRide function, we can define the function like the following:

constpriceRide=(getItinerary:GetItinerary):PriceRide=>async(route:Route):Promise<PricedOrder>=>{constitinerary=awaitgetItinerary(route)constprice=itinerary.distance*20return{itinerary,price}}
Enter fullscreen modeExit fullscreen mode

We need to addPriceRide function definition to the domain model:

typePriceRide=(route:Route)=>Promise<PricedRide>
Enter fullscreen modeExit fullscreen mode
  • The function now returns a function instead of a promise ofPricedRide by taking all dependencies as parameters.
  • The returned function returns a promise ofPricedRide and uses the dependency to fetch an itinerary.
  • SincepriceRide creates a closure, all dependencies are accessible for the returned functions but private for the outside world (like in the class-based version).

Runtime configuration checking

We can take advantage of thepriceRide function and add a runtime check that verifies that the passedGetItinerary parameter is really a function:

constpriceRide=(getItinerary:GetItinerary):PriceRide=>{if(typeofgetItinerary!=="function")thrownewTypeError("Get Itinerary is not a function")returnasync(route:Route):Promise<PricedRide>=>{constitinerary=awaitgetItinerary(route)constprice=itinerary.distance*20return{itinerary,price}}}
Enter fullscreen modeExit fullscreen mode

The major downside of this approach is that the function creates a closure for every call, which means a new object is created every time we call the function, however this can avoided by calling the function once at the composition root level which will be a topic for another article.

Top comments(3)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
djameleddine21 profile image
Sebbagh Djamel Eddine
  • Joined

Thanks for sharing

CollapseExpand
 
islemmedjahdi profile image
Medjahdi Islem
4th year student at ESI Algiers,Software engineering
  • Joined

Interesting topic, Thank you

CollapseExpand
 
maktabadz profile image
Maktaba
  • Joined

Love how you evolve your code step by step.
It show a total master of the subject you are talking about. And every think deeply make sens.
Awsome articale.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software Engineer, Domain-Driven Design practitioner, Tools agnostic
  • Location
    Montreal, QC, Canada
  • Joined

More fromCherif Bouchelaghem

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp