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