Search code examples
javascripttypescriptoopsolid-principles

Are interfaces required for the Dependency Inversion Principle?


I have read many examples about Dependency Inversion from classic SOLID in different coding languages.

// A is dependent on B
class A {
  property;

  constructor() {
    this.property = (new B()).property;
  }
}

new A();

Nearly all examples use the equivalent of an interface to achieve this.

From my understanding, the primary use of interfaces (or an abstract class in other languages) is essentially to help with testing. It seems that many languages need the equivalent of an interface to allow testing frameworks to inject a mocked version for testing, rather than the original.

interface Dependency {
  property: string
}

class B implements Dependency { ... }

// A is not dependent on B
class A {
  property;

  constructor(interface: Dependency) {
    this.property = interface.property;
  }
}

new A(new B());

Now obviously using an interface also allows different implementations to be used at runtime, but in my experience, 99% of code I write, most classes expect an exact dependency of a certain structure. I won't write a another which implements the same interface but does different functionality. That fact rarely changes with time.

When I write unit tests in TypeScript, I can mock dependencies without interfaces (using varying means). Since I will only ever have a single runtime implementation (as explained above), are interfaces even necessary for the Dependency Inversion principle?

More over, is the Dependency Inversion principle in TypeScript (and JavaScript minus the types), really as basic as the following?

// A is not dependent on B?
class A {
  property;

  constructor(b: B) {
    this.property = b.property;
  }
}

new A(new B()));

Solution

  • It seems as though you are confusing the Dependency Inversion Principle (DIP) with Dependency Injection. Despite some overlap, they are not the same.

    The DIP, succinctly put, says that

    "A. High-level modules should not depend on low-level modules. Both should depend on abstractions.

    "B. Abstractions should not depend upon details. Details should depend upon abstractions."

    - APPP, Robert C. Martin, Prentice Hall, 2006

    Here, abstraction implies encapsulation rather than any particular language feature (for example, an abstraction doesn't have to be an abstract class).

    I was just writing some other content that discuss the DIP. Here's an example where the DIP dictates architecture, even though it has nothing to do with interfaces or testing.

    A common scenario in much application architecture is to have a Data Transfer Object (DTO) that, say, models how JSON is serialized and deserialized, while also having a Domain Model that implements business logic.

    In a Ports and Adapters architecture, we'd typically consider the Domain Model the most abstract part, and the DTO an implementation detail.

    You need mappings between such two representations, and according to the DIP, these mappings should be defined on the DTO. This is because a mapping needs to know about both source and destination, and depending on the direction of the mapping, either the source or the destination is the DTO. Since the DTO is an implementation detail, you can't make the mapping methods part of the Domain Model, because if you do that, the abstraction (the Domain Model) would depend on an implementation detail (the DTO). Thus, according to the DIP, the translation methods must be defined on the DTO.

    This is an example of applying the DIP to software architecture. Mapping methods are typically pure functions, and there's no polymorphism in sight.

    Dependency Injection, on the other hand, usually makes use of polymorphism in order to enable client code to replace one implementation with another. Such polymorphism can be implemented with interfaces, abstract base classes, polymorphic functions or delegates, or even (depending on the language) function pointers.

    I recently wrote another answer that elaborates on that point, and I recommend you consult that one too.

    When it comes to defining interfaces for the sole purpose of unit testing, I suggest that you constrain yourself to doing so only for 'real' application dependencies.