Öffne Verschlüsse und injiziere Dependency Injection in JavaScript

  • Tutorial

Bild


In diesem Artikel wird beschrieben, wie sauberer, leicht testbarer Code mit dem Programmiermuster "Dependency Injection" in einem funktionalen Stil geschrieben wird. Der Bonus ist ein 100% iger Berichterstattungstest.


Die im Artikel verwendete Terminologie


Der Autor des Artikels wird genau diese Interpretation der folgenden Begriffe berücksichtigen, wobei er versteht, dass dies nicht die endgültige Wahrheit ist und dass andere Interpretationen möglich sind.


  • Dependency Injection
    ist ein Muster der Programmierung, was darauf schließen lässt , dass die externen Abhängigkeiten für Funktionen und Objekte von außen kommen Fabriken als Argumente dieser Funktionen. Die Abhängigkeitsinjektion ist eine Alternative zur Verwendung von Abhängigkeiten aus einem globalen Kontext.
  • Reine Funktion
    Dies ist eine Funktion, deren Ausgabe nur von ihren Argumenten abhängt. Auch sollte die Funktion keine Nebenwirkungen haben.
    Ich möchte sofort reservieren, dass die Funktionen, die wir in Betracht ziehen, keine Nebenwirkungen haben, aber dennoch die Funktionen haben können, die uns durch Dependency Injection erreicht wurden. Also die Reinheit der Funktionen haben wir mit großem Vorbehalt.
  • Unit - Tests
    Testfunktion aus , um sicherzustellen , dass alle Stecker in den Funktionen arbeiten genau wie der Autor den Code bestimmt. In diesem Fall wird anstelle eines Aufrufs anderer Funktionen ein Aufruf von moks verwendet.

Wir verstehen in der Praxis


Betrachten Sie ein Beispiel. Fabrikzähler, die zählen tick-und. Der Zähler kann mit der Methode gestoppt werden cancel.


const createCounter = ({ ticks, onTick }) => {
  const state = {
    currentTick: 1,
    timer: null,
    canceled: false
  }
  const cancel = () => {
    if (state.canceled) {
      throw new Error('"Counter" already canceled')
    }
    clearInterval(state.timer)
  }
  const onInterval = () => {
    onTick(state.currentTick++)
    if (state.currentTick > ticks) {
      cancel()
    }
  }
  state.timer = setInterval(onInterval, 200)
  const instance = {
    cancel
  }
  return instance
}
export default createCounter

Wir sehen von Menschen lesbaren, verständlichen Code. Aber es gibt einen Haken: Normale Unit-Tests können nicht darauf geschrieben werden. Mal sehen, was im Weg ist?


1) nicht die Funktionen innerhalb der Schaltung erreichen cancel, onIntervalund sie separat testen.


2) Die Funktion onIntervalkann nicht separat von der Funktion getestet werden cancel, weil der erste hat eine direkte Verbindung zum zweiten.


3) externe Abhängigkeiten werden verwendet setInterval, clearInterval.


4) Die Funktion createCounterkann nicht separat von anderen Funktionen getestet werden, da direkte Verbindungen bestehen.


Lassen Sie uns das Problem 1) 2) lösen - Remove - Funktion cancel, onIntervaldie Schließung und reißen direkte Verbindungen zwischen ihnen durch das Objekt pool.


export const cancel = pool => {
  if (pool.state.canceled) {
    throw new Error('"Counter" already canceled')
  }
  clearInterval(pool.state.timer)
}
export const onInterval = pool => {
  pool.config.onTick(pool.state.currentTick++)
  if (pool.state.currentTick > pool.config.ticks) {
    pool.cancel()
  }
}
const createCounter = config => {
  const pool = {
    config,
    state: {
      currentTick: 1,
      timer: null,
      canceled: false
    }
  }
  pool.cancel = cancel.bind(null, pool)
  pool.onInterval = onInterval.bind(null, pool)
  pool.state.timer = setInterval(pool.onInterval, 200)
  const instance = {
    cancel: pool.cancel
  }
  return instance
}
export default createCounter

Wir lösen das Problem 3). Wir verwenden das Injection Abhängigkeitsmuster auf setInterval, clearIntervalund sie auch auf ein Objekt übertragen pool.


export const cancel = pool => {
  const { clearInterval } = pool
  if (pool.state.canceled) {
    throw new Error('"Counter" already canceled')
  }
  clearInterval(pool.state.timer)
}
export const onInterval = pool => {
  pool.config.onTick(pool.state.currentTick++)
  if (pool.state.currentTick > pool.config.ticks) {
    pool.cancel()
  }
}
const createCounter = (dependencies, config) => {
  const pool = {
    ...dependencies,
    config,
    state: {
      currentTick: 1,
      timer: null,
      canceled: false
    }
  }
  pool.cancel = cancel.bind(null, pool)
  pool.onInterval = onInterval.bind(null, pool)
  const { setInterval } = pool
  pool.state.timer = setInterval(pool.onInterval, 200)
  const instance = {
    cancel: pool.cancel
  }
  return instance
}
export default createCounter.bind(null, {
  setInterval,
  clearInterval
})

Jetzt ist fast alles in Ordnung, aber es gibt immer noch ein Problem 4). Im letzten Schritt wenden wir Dependency Injection auf jede unserer Funktionen an und unterbrechen die verbleibenden Verbindungen zwischen ihnen durch das Objekt pool. Gleichzeitig werden wir eine große Datei in viele Dateien aufteilen, damit es später einfacher ist, Komponententests zu schreiben.


// index.js
import { createCounter } from './create-counter'
import { cancel } from './cancel'
import { onInterval } from './on-interval'
export default createCounter.bind(null, {
  cancel,
  onInterval,
  setInterval,
  clearInterval
})

// create-counter.js
export const createCounter = (dependencies, config) => {
  const pool = {
    ...dependencies,
    config,
    state: {
      currentTick: 1,
      timer: null,
      canceled: false
    }
  }
  pool.cancel = dependencies.cancel.bind(null, pool)
  pool.onInterval = dependencies.onInterval.bind(null, pool)
  const { setInterval } = pool
  pool.state.timer = setInterval(pool.onInterval, 200)
  const instance = {
    cancel: pool.cancel
  }
  return instance
}

// on-interval.js
export const onInterval = pool => {
  pool.config.onTick(pool.state.currentTick++)
  if (pool.state.currentTick > pool.config.ticks) {
    pool.cancel()
  }
}

// cancel.js
export const cancel = pool => {
  const { clearInterval } = pool
  if (pool.state.canceled) {
    throw new Error('"Counter" already canceled')
  }
  clearInterval(pool.state.timer)
}

Fazit


Was haben wir am Ende? Eine Reihe von Dateien, von denen jede eine Bereinigungsfunktion enthält. Die Einfachheit und Verständlichkeit des Codes hat sich zwar etwas verschlechtert, dies wird jedoch durch das Bild einer 100% igen Abdeckung in Komponententests mehr als ausgeglichen.


Abdeckung


Ich möchte auch darauf hinweisen, dass wir zum Schreiben von Komponententests keine Manipulationen mit requiredem Dateisystem Node.js. durchführen und dieses überfliegen müssen


Unit-Tests
// cancel.test.js
import { cancel } from '../src/cancel'
describe('method "cancel"', () => {
  test('should stop the counter', () => {
    const state = {
      canceled: false,
      timer: 42
    }
    const clearInterval = jest.fn()
    const pool = {
      state,
      clearInterval
    }
    cancel(pool)
    expect(clearInterval).toHaveBeenCalledWith(pool.state.timer)
  })
  test('should throw error: "Counter" already canceled', () => {
    const state = {
      canceled: true,
      timer: 42
    }
    const clearInterval = jest.fn()
    const pool = {
      state,
      clearInterval
    }
    expect(() => cancel(pool)).toThrow('"Counter" already canceled')
    expect(clearInterval).not.toHaveBeenCalled()
  })
})

// create-counter.test.js
import { createCounter } from '../src/create-counter'
describe('method "createCounter"', () => {
  test('should create a counter', () => {
    const boundCancel = jest.fn()
    const boundOnInterval = jest.fn()
    const timer = 42
    const cancel = { bind: jest.fn().mockReturnValue(boundCancel) }
    const onInterval = { bind: jest.fn().mockReturnValue(boundOnInterval) }
    const setInterval = jest.fn().mockReturnValue(timer)
    const dependencies = {
      cancel,
      onInterval,
      setInterval
    }
    const config = { ticks: 42 }
    const counter = createCounter(dependencies, config)
    expect(cancel.bind).toHaveBeenCalled()
    expect(onInterval.bind).toHaveBeenCalled()
    expect(setInterval).toHaveBeenCalledWith(boundOnInterval, 200)
    expect(counter).toHaveProperty('cancel')
  })
})

// on-interval.test.js
import { onInterval } from '../src/on-interval'
describe('method "onInterval"', () => {
  test('should call "onTick"', () => {
    const onTick = jest.fn()
    const cancel = jest.fn()
    const state = {
      currentTick: 1
    }
    const config = {
      ticks: 5,
      onTick
    }
    const pool = {
      onTick,
      cancel,
      state,
      config
    }
    onInterval(pool)
    expect(onTick).toHaveBeenCalledWith(1)
    expect(pool.state.currentTick).toEqual(2)
    expect(cancel).not.toHaveBeenCalled()
  })
  test('should call "onTick" and "cancel"', () => {
    const onTick = jest.fn()
    const cancel = jest.fn()
    const state = {
      currentTick: 5
    }
    const config = {
      ticks: 5,
      onTick
    }
    const pool = {
      onTick,
      cancel,
      state,
      config
    }
    onInterval(pool)
    expect(onTick).toHaveBeenCalledWith(5)
    expect(pool.state.currentTick).toEqual(6)
    expect(cancel).toHaveBeenCalledWith()
  })
})

Nur wenn wir alle Funktionen bis zum Ende öffnen, gewinnen wir Freiheit.


Jetzt auch beliebt: