Запазването и възстановяването на обекти в 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')
Някои приятни последващи действия, които да ви дадат по-задълбочен поглед върху концепциите на функционалното програмиране.
Обратна връзка? „Чуруликане“ ми!