DIP & IoC & DI

Basic Basic Basic Introduction

Dependency Inversion

High-level modules should not depend on low-level modules.

Both should depend on abstractions.

Abstractions should not depend on details.

Details should depend on abstractions.

Inversion of Control

This is a design pattern created so that code can comply with the Dependency Inversion Principle.

Dependency ?

class Hero {
  weapon: Weapon;

  constructor() {
    this.weapon = new Weapon();
  }
  
  attack() {
    this.weapon.attack();
  }
}

Hero depends on Weapon.

Weapon is dependency of Hero

Hero

Weapon

Hero is high-level module.

Weapon is low-level module.

If we want other weapons?

class Hero {
  weapon: Weapon;

  constructor(type: WeaponType) {
    this.weapon = new Weapon(type);
  }
  
  attack() {
    this.weapon.attack();
  }
}

If complication of weapon ↑ ?

class Hero {
  weapon: Weapon;

  constructor(options?: WeaponOptions) {
    this.weapon = new Weapon(options);
  }
  
  attack() {
    this.weapon.attack();
  }
}

Hero always need changed while Weapon changed

class Hero {
  weapon: Weapon;

  constructor(weapon: Weapon) {
    this.weapon = weapon;
  }
  
  attack() {
    this.weapon.attack();
  }
}

Dependency Injection

Constructor Injection

class Hero {
  weapon: Weapon;

  constructor(weapon: Weapon) {
    this.weapon = weapon;
  }
  
  attack() {
    this.weapon.attack();
  }
}

Setter Injection

class Hero {
  weapon: Weapon;
  
  setWeapon(weapon: Weapon) {
    this.weapon = weapon;
  }

  attack() {
    this.weapon.attack();
  }
}

Field Injection

class Hero {
  @Inject
  weapon: Weapon;

  attack() {
    this.weapon.attack();
  }
}

Someday, pm: we want hero can use tree branch to attack

High-level modules should not depend on low-level modules.

Both should depend on abstractions.

Abstractions should not depend on details.

Details should depend on abstractions.

interface Attackable {
  attack(): void;
}

class TreeBranch implements Attackable {
  attack() {
    ...
  }
}

class Hero {
  attackable: Attackable;

  constructor(attackable: Attackable) {
    this.attackable = attackable;
  }
  
  attack() {
    this.attackable.attack();
  }
}

Hero hold a unknown item but attackable, just attack.

Hero

Attackable

Sword

Katana

Tree Branch

Component Dependency

function Hero(props: HeroProps) {
  const { a, b, ...weaponProps } = props;

  return (
    <div>
      <Weapon {...weaponProps} />
    </div>
  );
}
class Hero {
  weapon: Weapon;

  constructor(props?: WeaponProps) {
    this.weapon = new Weapon(props);
  }
}
class Hero {
  weapon: Weapon;

  constructor(weapon: Weapon) {
    this.weapon = weapon;
  }
}
function Weapon(props: weaponProps) {
  return (
    <div>
      ...
    </div>
  );
}

function Hero(props: HeroProps) {
  const { a, b, weapon } = props;

  return (
    <div>
      {weapon}
    </div>
  );
}
function Attackable(props: AttackableProps) {
  return (
    <div>
      ...
    </div>
  );
}

function TreeBranch(props: TreeBranchProps) {
  const { c, d, ...attackableProps }

  return (
    <Attackable {...attackableProps} someProps={c + d} />
  );
}

function Hero(props: HeroProps) {
  const { a, b, attackable } = props;

  return (
    <div>
      {attackable}
    </div>
  );
}
interface Attackable {
  attack(): void;
}

class TreeBranch implements Attackable {
  attack() {
    ...
  }
}

class Hero {
  attackable: Attackable;

  constructor(attackable: Attackable) {
    this.attackable = attackable;
  }
  
  attack() {
    this.attackable.attack();
  }
}
interface Attackable {
  attack(): void;
}

const AttackableContext = createContext<Attackable>({
  attack: () => {}
});


function Parent() {
  // ...

  return (
    <AttackableContext.Provider value={sword}>
       <Child />
    </AttackableContext.Provider>
  );
}

function Child() {
  const attackable = useContext(AttackableContext);
  
  return (
    <button onClick={() => attackable.attack()}>attack</button>
  );
}

Props

- Dependency(coupling) between modules 

- Easy to maintain, replace modules

- Friendly to unit test

Ex: Adapter

class UsersService {
  // from orm
  usersRepository: UsersRepository
  
  construtctor() {
    this.usersRepository = new UsersRepository();
  }

  getUsers() {
    return this.usersRepository.findAll();  
  }
}
class UsersService {
  usersRepository: UsersRepository
  
  construtctor(usersRepository: UsersRepository) {
    this.usersRepository = usersRepository;
  }

  getUsers() {
    return this.usersRepository.findAll();  
  }
}

If a critical bus appear one day, your app will bomb.

Ex: Testability

Mock repository easily be injected.

class UsersService {
  usersRepository: UsersRepository
  
  construtctor(usersRepository: UsersRepository) {
    this.usersRepository = usersRepository;
  }

  getUsers() {
    return this.usersRepository.findAll();  
  }
}
class MockUsersRepository implements UsersRepository {
  getUsers() {
    return [
      // ...
    ];
  }
}

const usersRepository = new MockUsersRepository();
const usersService = new UsersService(usersRepository);

Cons

- DI concept is indigestible, new developers will just think WTF

- The objects are initialized entirely from the beginning, which can reduce performance

- Architecture complexity ↑

- Using an interface can sometimes be difficult to debug, because it is not known exactly where the module comes from

Coupling & Cohesion

Coupling is the degree of interdependence between software modules.

A measure of how closely connected two routines or modules are

Cohesion refers to the degree to which the elements inside a module belong together.

The purpose of DI is to reduce coupling.

// 87% without config
const head = new Head();
// 87% without config
const body = new Body();

// ...

const human = new Human(
  head,
  body,
  ...
);

But

If human every where.
Every time you need to write a lot of boilerplate code.

So, try to be balance.

const a = new A();
const b = new B();
const c = new C(a, b);
const d = new D(a, c);

...

Inversion of Control

const a = new A();
const b = new B();
const c = new C(a, b);
const d = new D(a, c);

...

But if you do IoC like this

IoC Container

.Net Core

Java Spring

Laravel

Angular

...

Will look like ...

interface Attackable {
  attack(): void;
}

class Sword implements Attackable {
  attack() {
    ...
  }
}

@Injectable()
class Hero {
  constructor(public readonly attackable: Attackable) {}
}

const container = new Container();

container.resolveAndCreate([Sword, Hero]);

const hero = container.get(Hero);

hero instanceof Hero; // true
hero.sword instanceof Sword; // true

In JS/TS (References)

To be continued ...

Q & A

DIP & IoC & DI

By jjaayy

DIP & IoC & DI

  • 349