JavaScript’te Durum Tasarım Modeli – JSManifest

JavaScript'te Durum Tasarım Modeli – JSManifest

Durum Modeli, bir nesnenin uygulamanın mevcut “durumuna” bağlı olarak öngörülebilir, koordineli bir şekilde davranmasını sağlar.

Genel durum kendi durumuna geçtiğinde bazı işleyicileri çalıştırmaktan sorumlu olan bir durum nesnesinde bir davranış tanımlanır. Bu durum nesnelerinin üzerinde çalıştığı arabirime, Context.

Bu kalıbın pratikte çalışma şekli, belirli eylemlerin çalışmasını devletin bir parçasını temsil eden durum nesnelerine devrederek, devletin parçasını temsil eden eylemin, bu durumu ele almalarından güncellemekten sorumlu olmasıdır.

Bunun anlamı şudur: Context bir veya daha fazla işleyiciye sahip olabilir, ancak sonuçta Context durum değişikliklerini tamamen kendi aralarında birer birer tetikler.

Bunun nedeni, durum nesnelerinin, işleyiciden ne olduğuna bağlı olarak bir sonraki durumun neye geçeceğini belirleyebilen eylemleri tetikleyen işleyicileri tanımlamasıdır.

Devlet Modeli Hangi Sorunları Çözüyor?

Çözdüğü en önemli sorun, durumunuzun büyüdüğü ve birçok vaka olduğu zamandır. Uygulamamızın durumu birçok yönden değişebildiğinde, özellikle uygulamamız çok büyük olduğunda, sorunları ayıklamak zorlaşıyor.

redux karmaşık durum sorunlarını çözmek için kullanımı kolay, öngörülebilir bir arayüz sağlamada başarılı bir kütüphanedir.

uygulama

Bir sayaçla çalışacağımız bir tür durum uyguladığımızı varsayalım:

const state = {
  counter: 0,
  color: 'green',
}

sayaç başlar 0 ve her saniye sayacı artıracağız 1. renk kalır "green" sayaç daha küçükse 5. sayaç arasında ise 5 ve 7 renk olacak "orange". Ve son olarak, eğer sayaç 8 veya daha yüksek renk "red".

Durum kalıbı olmadan bu, şöyle bir şeyle uygulanabilir:

function start({ onEachInterval }) {
  let color = 'green'
  let counter = 0

  let intervalRef = setInterval(() => {
    counter++
    if (color > 5) {
      if (color < 8) color = 'orange'
      else color = 'red'
    }
    onEachInterval({ counter, color })
  }, 1000)

  setTimeout(() => {
    clearInterval(intervalRef)
    console.log(`Timer has ended`)
  }, 10000)
}

start({
  onEachInterval({ counter, color }) {
    console.log(`The current counter is ${counter} `)
  },
})

Oldukça basit ve işi hallediyor. Bu kod çok kısa olduğu için durum modelini uygulamaya gerek yoktur çünkü aşırıya kaçar.

Diyelim ki kodumuz 5000 satır fazla mesaiye ulaştı. Bunu düşün. Programınızı test etmek için kolay bir zaman biriminiz olacağını düşünüyor musunuz? Kodunuz her zaman mükemmelse bunu başaramazsınız, ancak büyük uygulamalarda asla hata yapmayan bir geliştirici diye bir şey yoktur. Bir noktada mutlaka bazı hatalar olacaktır, bu yüzden kod yazarken dikkatli olmamız ve akıllıca kararlar vermemiz bizim yararımızadır. Kodun test edilmesi her zaman kolay olmalıdır.

Durum Örüntüsünün kullanışlı olmasının nedeni budur. kolayca test edilebilir ve bir ölçeklenebilir büyük veya karmaşık duruma sahip uygulamalar için.

Bu kod parçasını çalıştırdığımızda şunu elde ederiz:

The current counter is 1
The current counter is 2
The current counter is 3
The current counter is 4
The current counter is 5
The current counter is 6
The current counter is 7
The current counter is 8
The current counter is 9
Timer has ended

Bu, kodumuzun çalıştığı anlamına gelir. bizim içinde start işlev uygulama bir kez yazılır, ancak neredeyse hiç kontrol yoktur. Kontrol ayrıca Durum Modelinin bir başka faydasıdır.

Durum Modelini kullanarak bunun nasıl göründüğünü görelim:

function createStateApi(initialState) {
  const ACTION = Symbol('_action_')

  let actions = []
  let state = { ...initialState }
  let fns = {}
  let isUpdating = false
  let subscribers = []

  const createAction = (type, options) => {
    const action = { type, ...options }
    action[ACTION] = true
    return action
  }

  const setState = (nextState) => {
    state = nextState
  }

  const o = {
    createAction(type, handler) {
      const action = createAction(type)
      if (!fns[action.type]) fns[action.type] = handler
      actions.push(action)
      return action
    },
    getState() {
      return state
    },
    send(action, getAdditionalStateProps) {
      const oldState = state

      if (isUpdating) {
        return console.log(`Subscribers cannot update the state`)
      }

      try {
        isUpdating = true
        let newState = {
          ...oldState,
          ...getAdditionalStateProps?.(oldState),
          ...fns[action.type]?.(oldState),
        }
        setState(newState)
        subscribers.forEach((fn) => fn?.(oldState, newState, action))
      } finally {
        isUpdating = false
      }
    },
    subscribe(fn) {
      subscribers.push(fn)
    },
  }

  return o
}

const stateApi = createStateApi({ counter: 0, color: 'green' })

const changeColor = stateApi.createAction('changeColor')

const increment = stateApi.createAction('increment', function handler(state) {
  return {
    ...state,
    counter: state.counter + 1,
  }
})

stateApi.subscribe((oldState, newState) => {
  if (oldState.color !== newState.color) {
    console.log(`Color changed to ${newState.counter}`)
  }
})

stateApi.subscribe((oldState, newState) => {
  console.log(`The current counter is ${newState.counter}`)
})

let intervalRef = setInterval(() => {
  stateApi.send(increment)
  const state = stateApi.getState()
  const currentColor = state.color
  if (state.counter > 8 && currentColor !== 'red') {
    stateApi.send(changeColor, (state) => ({ ...state, color: 'red' }))
  } else if (state.counter >= 5 && currentColor !== 'orange') {
    stateApi.send(changeColor, (state) => ({ ...state, color: 'orange' }))
  } else if (state.counter < 5 && currentColor !== 'green') {
    stateApi.send(changeColor, (state) => ({ ...state, color: 'green' }))
  }
}, 1000)

setTimeout(() => {
  clearInterval(intervalRef)
  console.log(`Timer has ended`)
}, 10000)

Örnekten seçilecek birkaç şey var.

Çizgi const ACTION = Symbol('_action_') kodun geri kalanında kullanılmaz, ancak bu stratejiyi, gönderilen eylemleri doğrulamak için kullanmanın iyi bir uygulama olduğunu belirtmek istedim. send yöntem, durumu güncellemeyi amaçlayan gerçek eylemlerdir.

Örneğin, bu doğrulamayı hemen işlemimizin başında yapabiliriz. send yöntem:

send(action, getAdditionalStateProps) {
	if (!(ACTION in action)) {
		throw new Error(`The object passed to send is not a valid action object`)
	}
	const oldState = state

	if (isUpdating) {
		return console.log(`Subscribers cannot update the state`)
	}

Bunu yapmazsak, kodumuz hataya daha açık olabilir çünkü bunun gibi herhangi bir nesneyi iletebiliriz ve yine de çalışır:

function start() {
  send({ type: 'increment' })
}

Bu olumlu bir şey gibi görünebilir ama biz durum güncellemelerini tetikleyen tek eylemlerin olduğundan emin olmak istiyorum tarafından üretilen nesnelerdir. herkese açık olarak sağladığımız arayüz aracılığıyla onlara createAction. Kasıtlı olarak hata ayıklama için karmaşıklığı daraltmak ve hataların doğru konumlardan geldiğinden emin olmak istiyoruz.

Bakacağımız bir sonraki şey şu satırlar:

const increment = stateApi.createAction('increment', function handler(state) {
  return {
    ...state,
    counter: state.counter + 1,
  }
})

Daha önce şunu belirttiğimizi unutmayın (punto amaçlanmamıştır):

Bu kalıbın pratikte çalışma şekli, belirli eylemlerin çalışmasını devletin bir parçasını temsil eden durum nesnelerine devrederek, devletin parçasını temsil eden eylemin, bu durumu ele almalarından güncellemekten sorumlu olmasıdır.

tanımladık increment aracılığıyla tüketildiğinde her saniye artırmaktan sorumlu eylem send. akımı alır state ve bir sonraki duruma birleştirmek için dönüş değerlerini alır.

Artık bu davranışı bu durum parçası için kolayca izole edip birim test edebiliyoruz:

npx mocha ./dev/state.test.js
const { expect } = require('chai')
const { createStateApi } = require('./patterns')

describe(`increment`, () => {
  it(`should increment by 1`, () => {
    const api = createStateApi({ counter: 0 })
    const increment = api.createAction('increment', (state) => ({
      ...state,
      counter: state.counter + 1,
    }))
    expect(api.getState()).to.have.property('counter').to.eq(0)
    api.send(increment)
    expect(api.getState()).to.have.property('counter').to.eq(1)
  })
})
increment
	✔ should increment by 1


1 passing (1ms)

İlk örneğimizde, işleve kodlanmış bir uygulamamız vardı. Yine, bu işlevin birim testi zor olacak. Burada yaptığımız gibi kodun ayrı kısımlarını izole edemeyiz.

İzolasyon programlamada güçlüdür. Durum Modeli izole etmemizi sağlar. İzolasyon, parçaları bir araya getirmek için şimdi kolayca elde edilebilecek daha geniş olanaklar sunar:

it(`should increment by 5`, () => {
  const api = createStateApi({ counter: 0 })

  const createIncrementener = (amount) =>
    api.createAction('increment', (state) => ({
      ...state,
      counter: state.counter + amount,
    }))

  const increment = createIncrementener(5)
  expect(api.getState()).to.have.property('counter').to.eq(0)
  api.send(increment)
  expect(api.getState()).to.have.property('counter').to.eq(5)
})

Unutmayın, State Pattern’in ölçeklenebilir olduğundan da bahsetmiştik. Uygulamamızın boyutu büyüdükçe, kalıp, ölçeklenebilirlikle mücadele etmek için bizi yararlı kompozisyon yetenekleriyle korur:

it(`should increment from composed math functions`, () => {
  const addBy = (amount) => (counter) => counter + amount
  const multiplyBy = (amount) => (counter) => counter * amount

  const api = createStateApi({ counter: 0 })

  const createIncrementener = (incrementBy) =>
    api.createAction('increment', (state) => ({
      ...state,
      counter: incrementBy(state.counter),
    }))

  const applyMathFns =
    (...fns) =>
    (amount) =>
      fns.reduceRight((acc, fn) => (acc += fn(acc)), amount)

  const increment = api.createAction(
    'increment',
    createIncrementener(applyMathFns(addBy(5), multiplyBy(2), addBy(1))),
  )

  api.send(increment)

  expect(api.getState()).to.have.property('counter').to.eq(11)
})

Hikayenin ahlaki? Durum Modeli İşler.

büyük resim

Bu gönderiyi burada sonlandırmak için, Durum Tasarım Modelinin görsel bir perspektifi:

Çözüm

Ve bu yazının sonu burada bitiyor! Umarım bunu değerli bulmuşsunuzdur ve gelecekte daha fazlasını ararsınız!

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.