Запазването и възстановяването на обекти в JavaScript изглежда тривиално, нали?

const save    = obj => JSON.stringify(obj)
const restore = str => JSON.parse(str)

Е, не толкова бързо; нека опитаме това в реален (както става) пример за свят:

class TodoList {
  constructor(tasks) {
    this.tasks = tasks || []
  }
  addTask(task) {
    this.tasks.push(task)
  }
}
const todos = new TodoList([
  'laundry',
  'assignments',
  'procrastination'
])

Хубаво е, че имаме екземпляр на TodoList, с нашите задачи, нека нашата реализация за запазване да го запазим.

const savedTodos = save(todos)

Това е полезно, когато искаме да сериализираме нашия обект за записване в нещо като localStorage или redis. Така че можем да запазим нашия списък със задачи в любимото ни хранилище във формат низ. Нека се опитаме да го прочетем отново.

const restoredTodos = restore(savedTodos)

restoredTodos, за разлика от оригиналните todosе обикновен Object, а не екземпляр на TodoList, възстановяват се само полетата с данни, а не функциите-членове.

1. Възстановяване на функцията

Добавете статична функция restore към TodoList, която приема обикновеното JSON представяне и връща работещ екземпляр с обратно добавените методи.

class TodoList {
  constructor(tasks) {
    this.tasks = tasks || []
  }
  addTask(task) {
    this.tasks.push(task)
  }
  static restore(todos) {
    return new TodoList(todos.tasks)
  }
}

Това ни позволява да предадем savedTodosкъм функцията за възстановяване и да си върнем функциониращ обект.

const restoredTodos_ = TodoList.restore(restoredTodos)

Това работи, ако всички полета на членовете са инициализирани в конструктора, но какво ще стане, ако имаме поле, което не може да се настройва чрез конструктора. Обмисли

class TodoList {
  constructor(tasks) {
    this.tasks = tasks || []
  }
  addTask(task) {
    this.tasks.push(task)
  }
  addName(name) {
    this.name = name
  }
  static restore(todos) {
    return new TodoList(todos.tasks)
  }
}

Тук полето name е динамично присвоено на обекта чрез метода addName. Какво да направите в този случай, ръчно да зададете всяко такова поле? Е, благодаря на ES6, можем да използваме Object.assign, за да опростим работата за нас.

class TodoList {
  constructor(tasks) {
    this.tasks = tasks || []
  }
  addTask(task) {
    this.tasks.push(task)
  }
  addName(name) {
    this.name = name
  }
  static restore(todos) {
    return Object.assign(new TodoList(), todos)
  }
}

Това беше лесно, Object.assign е подобно на функцията extend/assign на lodash, може да се използва за сливане на два или повече обекта.

Object.assign(target, source...)

По този начин всички полета на обекта се възстановяват с неговите членски функции. Това звучи добре, така че приключихме ли? Не точно.

Ами ако задачите вместо обикновен низ тип също бяха потребителски обекти?

const Task {
  constructor(description) {
    this.description = description
  }
  update(description) {
    this.description = description
  }
}

Ще трябва да добавим подобна функция за възстановяване към класа Task.

const Task {
  constructor(description) {
    this.description = description
  }
  update(description) {
    this.description = description
  }
  static restore(task) {
    return Object.assign(new Task(), task)
  }
}

След това променете функцията за възстановяване на TodoList, за да извикате функцията за възстановяване на задачата за всяка задача, която държи.

class TodoList {
  constructor(tasks) {
    this.tasks = tasks || []
  }
  addTask(task) {
    this.tasks.push(task)
  }
  addName(name) {
    this.name = name
  }
  static restore(todos) {
    return Object.assign(new TodoList(), todos, {
      tasks: todos.map(Task.restore)
    })
  }
}

Върши работата, но не изглежда много красиво. Това заобиколно решение установява твърда връзка между родител и дете обект, всеки родител трябва ръчно да възстанови своя дъщерен обект, преди да възстанови себе си.

За да разрешите този проблем, не търсете повече от lodash или който и да е FP език като Haskell.

2. Функционален подход

Функционалният подход към този проблем прокламира

Структурите от данни съдържат данни, а не функции - Някой (надявам се)

Ако излезем от удобствата на Обектно-ориентираното програмиране и прекрачим в странния свят на Функционалното програмиране, проблемът се решава сам.

(todos: TaskList).addTask(task: Task) //OOP approach
addTask(todos: TaskList, task: Task)  //FP  approach

Вместо да дефинирате методите на екземпляри, дефинирайте ги така, че да приемат самия екземпляр като параметър.

Това няма много смисъл в областта на ООП, тъй като addTaskне означава нищо извън контекста на обект TodoList, който е вътрешното му състояние или това“. В OOP само TodoList може да има метода addTask, който променя вътрешното състояние на обекта.

Във FP обаче addTask е функция (подобна на математическите функции), тя приема някои параметри и върши известна работа (това е страничен ефект), но това е история за друг път. Във функционалното програмиране параметрите са контекстът, а не вътрешното състояние на обекта.

const addTask = (todos, task) => todos.tasks.push(task)
const addName = (todos, name) => todos.name = name
class TodoList {
  constructor(tasks) {
    this.tasks = tasks || []
  }  
}
const todos = new TodoList([
  'laundry',
  'assignments',
  'procrastination'
])
const savedTodos    = save(todos)
const restoredTodos = restore(savedTodos)
addTask(restoredTodos, 'win')

Някои приятни последващи действия, които да ви дадат по-задълбочен поглед върху концепциите на функционалното програмиране.



Обратна връзка? „Чуруликане“ ми!