DI in my team

caeus

Alejandro Navas

Posted on November 22, 2024

DI in my team
We will use the Cake Pattern for Dependency Injection, organizing code into components with clearly defined scopes. For each component, we define the following:

1. **Trait**: The interface for the service.


```scala
trait MyService{
  ...
}
```


2. **Binding key**: A `Required` trait in the companion object.


```scala
object MyService{
   trait Required {
      def myService:MyService
   }
}
```



3. **Implementation**: A concrete class for the service.


```scala
final class DefaultMyService extends MyService {
   ...
}
```



4. **Binding**: A `Provided` trait that extends `Required` and implements the method as `lazy val` for singletons.



```scala
object DefaultMyService {
   trait Provided extends Required{
      final override lazy val myService: MyService = new DefaultMyService()
   }
}
```



**Naming conventions**: Avoid the Impl suffix for implementations ([guidelines here](https://stackoverflow.com/questions/2814805/java-interfaces-implementation-naming-convention)).

## Application structure 

1. **Modules**: Group multiple bindings.




```scala
trait MyModule extends DefaultMyService.Provided
with DefaultMyOtherService.Provided
with DefaultMyRepo.Provided
with DefaultMyClient.Provided{
}
```



2. **Containers**: Combine modules into the application context.


```scala
new MyModule with MyOtherModule with YourModule{}
```



## Key Concepts
### Binding keys

Binding keys define associations between components and traits. They must:

* Be a single-method trait named `Required`.
* Use def (not val or lazy val) for initialization order.
* Avoid mixins or self-annotations in the `Required` trait.
scala



```scala
object A{
   // NO EXTRA MIXINS
   trait Required{
      // NO SELF ANNOTATIONS
      def a: A // DEF, NOT VAL, NOT LAZY VAL... DEF!
   }
}
```


### Bindings 

Bindings define dependencies and their constructors.

* Declared as `Provided` traits in the companion object of the implementation.
* Extend only `Required`, implementing the method as `lazy val` or `def`.
* Use self annotations (not mixins) for dependencies. Self annotations should be `Required` traits, not `Provided` traits.
* Kindly add `final` and `override` modifiers for readability


```scala

object DefaultA{
   trait Provided extends Required{
      self: B.Required with C.Required with D.Required =>
      override final lazy val a = new DefaultA(b,c,d)
   }
}
```



## Closing Notes  

1. **Avoid Global Accessibility**  
   Components should not be accessible globally. Global state introduces coupling and makes testing, debugging, and maintaining the system harder. Instead, restrict visibility through scoped dependencies and modular design.

2. **Prefer Composition Over Inheritance**  
   Composition promotes flexibility and reuse by assembling components at runtime rather than inheriting behaviors at compile time. While inheritance has its place, overusing it can lead to rigid and tightly coupled systems. Rely on traits and self-types to compose functionality dynamically.

Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
caeus
Alejandro Navas

Posted on November 22, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related