Реализация паттерна Repository в браузерном JavaScript

Хорошая архитектура освобождает Вас от привязки к конкретной реализации. Она позволяет Вам отложить момент принятия решения о реализации, и начать конструирование кода еще не имея этого решения. Принципиально важным моментом является то, что Вы обретаете возможность принять решение в момент наибольшей информированности, а также всегда можете легко подменить конкретную реализацию на любую другую. Вот эта обязанность возложена на паттерн Repository.

Таким образом, у Вас появляется полная абстракция от источника данных, будь то REST-API, MBaaS, SaaS, IndexedDB, HTML, сторонний сервис по протоколу JSON-RPC или Service Stub.

“Однако мы часто забываем, что принятие решений лучше всего откладывать до последнего момента. Дело не в лени или безответственности; просто это позволяет принять информированное решение с максимумом возможной информации. Преждевременное решение принимается на базе неполной информации. Принимая решение слишком рано, мы лишаемся всего полезного, что происходит на более поздних стадиях: обратной связи от клиентов, возможности поразмышлять над текущим состоянием проекта и опыта применения решений из области реализации.”

“We often forget that it is also best to postpone decisions until the last possible moment. This isn’t lazy or irresponsible; it lets us make informed choices with the best possible information. A premature decision is a decision made with suboptimal knowledge. We will have that much less customer feedback, mental reflection on the project, and experience with our implementation choices if we decide too soon.” («Clean Code: A Handbook of Agile Software Craftsmanship» [1])

A good architecture allows major decision to be deferred! (Robert Martin)

Вы получаете возможность принимать важные решения настолько поздно, насколько это возможно. Это делается для того, чтобы осуществлять связанные с этим затраты как можно позже. Кроме того, если вы откладываете решение важных вопросов на более поздний срок, тем самым вы повышаете вероятность того, что выбранное вами решение окажется правильным. Другими словами, сегодня вы должны реализовать только то, без чего сегодня не обойтись, при этом вы можете рассчитывать на то, что проблемы, решение которых вы отложили до завтра, развеются сами собой, то есть перестанут быть актуальными.

You would make big decisions as late in the process as possible, to defer the cost of making the decisions and to have the greatest possible chance that they would be right. You would only implement what you had to, in hopes that the needs you anticipate for tomorrow wouldn’t come true. (Kent Beck [10])

Кроме того, у Вас появляется возможность реализовать паттерны Identity Map и Unit of Work. Последний очень часто востребован, так как позволяет сохранять на сервере только измененные объекты окончательно сформированного агрегата вложенных объектов, либо выполнить откат состояния локальных объектов в случае, если сохранить данные невозможно (пользователь передумал или ввел невалидные данные).

Модель предметной области (Domain Model)

Наибольшим преимуществом полноценных Моделей предметной области в программе является возможность использования принципов Domain-Driven Design (DDD) [4]. Если Модели содержат исключительно бизнес-логику, и освобождены от служебной логики, то они могут легко читаться специалистами предметной области (т.е. представителем заказчика). Это освобождает Вас от необходимости создания UML-диаграмм для обсуждений и позволяет добиться максимально выского уровня взаимопонимания, продуктивности, и качества реализации моделей.

В одном из проектов я пытался реализовать достаточно сложную доменную логику (которая содержала около 20 доменных Моделей) в парадигме реактивного программирования, когда атрибуты экземпляра Модели, содержащие аннотации агрегации или зависимые от них, изменяли свое значение путем реакции на изменения других моделей и хранилищ. Суть в том, что вся эта реактивная логика уже не принадлежала самой доменной модели, и располагалась в разного рода слушателях и обработчиках.

“Весь смысл объектов в том, что они позволяют хранить данные вместе с процедурами их обработки. Классический пример дурного запаха – метод, который больше интересуется не тем классом, в котором он находится, а каким то другим. Чаще всего предметом зависти являются данные.”

“The whole point of objects is that they are a technique to package data with the processes used on that data. A classic smell is a method that seems more interested in a class other than the one it actually is in. The most common focus of the envy is the data.” («Refactoring: Improving the Design of Existing Code» [6])

“Хороший дизайн размещает логику рядом с данными, в отношении которых она действует.”

“Good design puts the logic near the data it operates on.” (Kent Beck [10])

“Если требования архитектурной среды к распределению обязанностей таковы, что элементы, реализующие концептуальные объекты, оказываются физически разделенными, то код больше не выражает модель.

Нельзя разделять до бесконечности, у человеческого ума есть свои пределы, до которых он еще способен соединять разделенное; если среда выходит ха это пределы, разработчики предметной области теряют способность расчленять модель на осмысленные фрагменты.”

“If the framework’s partitioning conventions pull apart the elements implementing the conceptual objects, the code no longer reveals the model.

There is only so much partitioning a mind can stitch back together, and if the framework uses it all up, the domain developers lose their ability to chunk the model into meaningful pieces.” («Domain-Driven Design: Tackling Complexity in the Heart of Software» [4])

Это привело к такому огромному количеству хитросплетений слушателей, что превосходство в performance было утрачено, но еще раньше была утрачена читаемость кода. Даже я не мог на следующий день сказать что делает тот или иной фрагмент кода, не говоря уже о специалисте предметной области. Мало того, что это в корне разрушало принципы Domain-Driven Design, так это еще и в значительной мере снижало скорость разработки новых функций проекта.

Надежды на такой подход окончательно рухнули когда выяснилось, что каждый экземпляр модели должен изменять значения своих атрибутов, содержащих аннотации агрегации или зависимых от них, в зависимости от контекста использования (выбранной группировки отображения или критериев фильтрации).

Впоследствии модели вернули себе свои концептуальные контуры и читаемость кода, и вместе с тем сохранили механизм реакций при добавлении, изменении или удалении объектов. Для достижения этого результата пришлось своими силами создать библиотеку реализующую паттерн Repository, так как существующих решений для реляционных данных с качественной кодовой базой я не смог найти.

Парадигма реактивного программирования

Сегодня модно увлекаться реактивным программированием. Знаете ли Вы, что разработчики dojo впервые применили реактивное программирование в своей реализации паттерна Repository еще 13 сентября 2010?

Реактивное программирование дополняет (а не противопоставляет) паттерн Repository, о чем красноречиво свидетельствует опыт dojo.store, Dstore и нового Dojo 2 - data stores.

Разработчики dojo - команда высококвалифицированных специалистов, чьи библиотеки используют такие серьезные компании как IBM. Примером того, насколько серьезно и комплексно они подходят к решению проблем, может служить история библиотеки RequireJS.

Примеры реализаций паттерна Repository в JavaScript

Примеры простейших реализаций паттерна Repository на JavaScript в проекте todomvc.com:

Другие реализации:

Я хотел бы добавить сюда и Ember.js, но он реализует паттерн ActiveRecord.

Реализация реляционных связей

Синхронное программирование

На заре появления ORM, мапперы делали таким образом, чтобы они извлекали из базы данных все связанные объекты одним запросом (см. пример реализации).

Domain-Driven Design подходит к связям более строго, и рассматривает связи с позиции концептуальных контуров агрегата вложенных объектов [4]. Доступ к объекту осуществлялся либо по ссылке (от родительского объекта к вложеному), либо через Repository. Здесь также особую роль играет направление связей, и соблюдение принципа минимальной достаточности (“дистиляция моделей” [4]).

In real life, there are lots of many-to-many associations, and a great number are naturally bidirectional. The same tends to be true of early forms of a model as we brainstorm and explore the domain. But these general associations complicate implementation and maintenance. Furthermore, they communicate very little about the nature of the relationship.

There are at least three ways of making associations more tractable.

  1. Imposing a traversal direction
  2. Adding a qualifier, effectively reducing multiplicity
  3. Eliminating nonessential associations

It is important to constrain relationships as much as possible. A bidirectional association means that both objects can be understood only together. When application requirements do not call for traversal in both directions, adding a traversal direction reduces interdependence and simplifies the design. Understanding the domain may reveal a natural directional bias. («Domain-Driven Design: Tackling Complexity in the Heart of Software» [4])

Minimalist design of associations helps simplify traversal and limit the explosion of relationships somewhat, but most business domains are so interconnected that we still end up tracing long, deep paths through object references. In a way, this tangle reflects the realities of the world, which seldom obliges us with sharp boundaries. It is a problem in a software design. («Domain-Driven Design: Tackling Complexity in the Heart of Software» [4])

С появлением ORM, в синхронном программировании активно начали применяться ленивые вычисления для разрешения связей. В Python для этого активно используются Descriptors, а в Java - AOP и Cross-Cutting Concerns [1].

Ключевым моментом является освобождение Domain Model от логики доступа к источнику данных. Это необходимо как из принципа чистоты архитектуры и проектных решений, чтобы снизить сопряжение (Coupling), так и из принципа простоты тестирования. Наибольших успехов позволяет достигнуть принцип Cross-Cutting Concerns, который полностью освобождает модель от служебной логики.

С появлением ОРМ, организация связей стала настолько легкой, что о ней перестали задумываться. Там где требуются однонаправленные связи, разработчики с легкостью применяют двунаправленные связи. Появились механизмы оптимизации выборки связанных объектов, которые неявно предзагружают все связанные объекты, что значительно сокращает количество обращений в базу данных.

Отказ от связей

Стоит упомянуть и другую распространенную точку зрения, которая гласит, что объект не должен отвечать за свои связи, а исключительное право на доступ к объекту должно принадлежать только Repository. Такой точки зрения придерживаются некоторые уважаемые мною друзья.

Асинхронное программирование

Рост популярности асинхронных приложений заставил пересмотреть устоявшиеся представления о ленивой реализации связей. Асинхронное обращение к каждой ленивой связи каждого объекта значительно усложняет ясность программного кода, и препятствует оптимизации.

Это привело к росту популярности объекто-ориентированных баз данных в асинхронном программировании, которые позволяют сохранять агрегаты целиком. Все чаще REST-frameworks стали использоваться для передачи клиенту агрегатов вложенных объектов.

To do anything with an object, you have to hold a reference to it. How do you get that reference? One way is to create the object, as the creation operation will return a reference to the new object. A second way is to traverse an association. You start with an object you already know and ask it for an associated object. Any object-oriented program is going to do a lot of this, and these links give object models much of their expressive power. But you have to get that first object.

I actually encountered a project once in which the team was attempting, in an enthusiastic embrace of MODEL-DRIVEN DESIGN , to do all object access by creation or traversal! Their objects resided in an object database, and they reasoned that existing conceptual relationships would provide all necessary associations. They needed only to analyze them enough, making their entire domain model cohesive. This self-imposed limitation forced them to create just the kind of endless tangle that we have been trying to avert over the last few chapters, with careful implementation of ENTITIES and application of AGGREGATES . The team members didn’t stick with this strategy long, but they never replaced it with another coherent approach. They cobbled together ad hoc solutions and became less ambitious.

Few would even think of this approach, much less be tempted by it, because they store most oftheir objects in relational databases. This storage technology makes it natural to use the third way of getting a reference: Execute a query to find the object in a database based on its attributes, or find the constituents of an object and then reconstitute it. («Domain-Driven Design: Tackling Complexity in the Heart of Software» [4])

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

Однако, решение одной проблемы порождало другую проблему.

Функциональное программирование

Функциональное программирование сложнее использовать для объектов предметной области, так как его сложнее структурировать логически (особенно при отсутствии поддержки множественной диспетчеризации), что зачастую приводит к появлению плохо читаемого кода, который выражает не то, “что” он делает, а то, “как” он делает непонятно что.

If you wanted polymophism in C, you’d have to manage those pointers yourself; and that’s hard. If you wanted polymorphism in Lisp you’d have to manage those pointers yourself (pass them in as arguments to some higher level algorithm (which, by the way IS the Strategy pattern.)) But in an OO language, those pointers are managed for you. The language takes care to initialize them, and marshal them, and call all the functions through them.

... There really is only one benefit to Polymorphism; but it’s a big one. It is the inversion of source code and run time dependencies. («OO vs FP» [7])

Все же мой опыт подсказывает мне, что стоимость изменений увеличивается в большей степени в случае, если вы не используете объекты, чем в случае, если вы основываете свой проект на объектно-ориентированном подходе.

However, my experience is that the cost of change rises more steeply without objects than with objects. (Kent Beck [10])

А между тем, неясность намерений и целей автора - это ключевая проблема при чтении чужого кода.

Шестимесячное исследование, проведенное в IBM, показало, что программисты, отвечавшие за сопровождение программы, «чаще всего говорили, что труднее всего было понять цель автора кода» (Fjelstad and Hamlen, 1979).

A six-month study conducted by IBM found that maintenance programmers “most often said that understanding the original programmer’s intent was the most difficult problem” (Fjelstad and Hamlen 1979). («Code Complete» [2])

Как упоминалось в статье “How to quickly develop high-quality code. Team work.”, в процессе конструирования кода разработчик 91% времени читает код, и только 9% времени он вводит символы с клавиатуры. А это значит, что плохо читаемый код на 91% влияет на темпы разработки.

Также такой подход разрушает все выгоды использования Domain-Driven Design, и разделяет элементы, реализующие концептуальные объекты, которые оказываются физически разделенными, что приводит к появлению кода, который больше не выражает модель.

Все это способствовало появлению в сообществе ReactJS таких библиотек как:

  • Normalizr - Normalizes (decomposes) nested JSON according to a schema.
  • Denormalizr - Denormalize data normalized with normalizr.

Лирическое отступление

Несмотря на то, что приемы функционального программирования часто используются совместно с парадигмой реактивного программирования, в своей сути эти парадигмы не всегда сочетаемы в каноническом виде в веб-разработке.

Это потому, что реактивное программирование основано на распространении изменений, т.е. подразумевает наличие переменных и присваивания.

Это означает, что должна существовать возможность легко выражать статические и динамические потоки данных, а также то, что нижележащая модель исполнения должна автоматически распространять изменения благодаря потоку данных.

К примеру, в императивном программировании присваивание a := b + c будет означать, что переменной a будет присвоен результат выполнения операции b + c, используя текущие (на момент вычисления) значения переменных. Позже значения переменных b и c могут быть изменены без какого-либо влияния на значение переменной a. В реактивном же программировании значение a будет автоматически пересчитано, основываясь на новых значениях.

... К примеру, в MVC архитектуре с помощью реактивного программирования можно реализовать автоматическое отражение изменений из Model в View и наоборот из View в Model.

This means that it becomes possible to express static (e.g. arrays) or dynamic (e.g. event emitters) data streams with ease via the employed programming language(s), and that an inferred dependency within the associated execution model exists, which facilitates the automatic propagation of the change involved with data flow.

For example, in an imperative programming setting, a := b + c would mean that a is being assigned the result of b + c in the instant the expression is evaluated, and later, the values of b and/or c can be changed with no effect on the value of a. However, in reactive programming, the value of a is automatically updated whenever the values of b and/or c change; without the program having to re-execute the sentence a := b + c to determine the presently assigned value of a.

... For example, in an model–view–controller (MVC) architecture, reactive programming can facilitate changes in an underlying model that automatically are reflected in an associated view, and contrarily. (“Reactive programming”, wikipedia)

Именно поэтому парадигма реактивного программирования может сочетаться с различными парадигмами, императивной, объектно-ориентированной и функциональной.

Однако, вся суть вопроса заключается в том, что в каноническом виде функциональное программирование не имеет переменных (от слова “переменчивость”, изменяемость). т.е. изменяемого состояния:

A true functional programming language has no assignment operator. You cannot change the state of a variable. Indeed, the word “variable” is a misnomer in a functional language because you cannot vary them.

...The overriding difference between a functional language and a non-functional language is that functional languages don’t have assignment statements.

... The point is that a functional language imposes some kind of ceremony or discipline on changes of state. You have to jump through the right hoops in order to do it.

And so, for the most part, you don’t. («OO vs FP» [7])

Поэтому, использование подходов функционального программирования не делает программу функциональной до тех пор, пока программа имеет изменяемое состояние, - это просто процедурное программирование. А если это так, то отказ от Domain-Driven Design просто отнимает превосходства обоих подходов (ни полиморфизма объектно-ориентированного программирования, ни неизменяемости функционального программирования), объединяя все худшее, подобно объектам-гибридам [1], так и не делая программу по настоящему функциональной.

Гибриды

Вся эта неразбериха иногда приводит к появлению гибридных структур — наполовину объектов, наполовину структур данных. Гибриды содержат как функции для выполнения важных операций, так и открытые переменные или открытые методы чтения/записи, которые во всех отношениях делают приватные переменные открытыми. Другим внешним функциям предлагается использовать эти переменные так, как в процедурных программах используются структуры данных (иногда это называется «функциональной завистью» (Feature Envy) — из “Refactoring” [6]). Подобные гибриды усложняют как добавление новых функций, так и новых структур данных. Они объединяют все худшее из обеих категорий. Не используйте гибриды. Они являются признаком сумбурного проектирования, авторы которого не уверены (или еще хуже, не знают), что они собираются защищать: функции или типы.

Hybrids

This confusion sometimes leads to unfortunate hybrid structures that are half object and half data structure. They have functions that do significant things, and they also have either public variables or public accessors and mutators that, for all intents and purposes, make the private variables public, tempting other external functions to use those variables the way a procedural program would use a data structure (this is sometimes called Feature Envy from “Refactoring” [6]). Such hybrids make it hard to add new functions but also make it hard to add new data structures. They are the worst of both worlds. Avoid creating them. They are indicative of a muddled design whose authors are unsure of—or worse, ignorant of—whether they need protection from functions or types. («Clean Code: A Handbook of Agile Software Craftsmanship» [1])

Каноническое функциональное программирование не имеет состояния, и поэтому идеально подходит для распределенных вычислений и обработки потоков данных.

The benefit of not using assignment statements should be obvious. You can’t have concurrent update problems if you never update anything.

Since functional programming languages do not have assignment statements, programs written in those languages don’t change the state of very many variables. Mutation is reserved for very specific sections of the system that can tolerate the high ceremony required. Those sections are inherently safe from multiple threads and multiple cores.

The bottom line is that functional programs are much safer in multiprocessing and multiprocessor environments. («OO vs FP» [7])

Но значит ли это то, что парадигма объектно-ориентированного программирования противостоит парадигме функционального программирования?

Несмотря на то, что парадигма ООП традиционно считается разновидностью императивной парадигмы, т.е. основанной на состоянии программы, Robert C. Martin делает поразительный вывод - так как объекты предоставляют свой интерфейс, т.е. поведение, и скрывают свое состояние, то они не противоречат парадигме функционального программирования.

“Objects are not data structures. Objects may use data structures; but the manner in which those data structures are used or contained is hidden. This is why data fields are private. From the outside looking in you cannot see any state. All you can see are functions. Therefore Objects are about functions not about state.” («OO vs FP» [7])

Поэтому некоторые классические функциональные языки программирования имеют поддержку ООП:

  • Enhanced Implementation of Emacs Interpreted Objects

  • Common Lisp Object System

    Are these two disciplines mutually exclusive? Can you have a language that imposes discipline on both assignment and pointers to functions? Of course you can. These two things don’t have anything to do with each other. And that means that OO and FP are not mutually exclusive at all. It means that you can write OO-Functional programs.

    It also means that all the design principles, and design patterns, used by OO programmers can be used by functional programmers if they care to accept the discipline that OO imposes on their pointers to functions. («OO vs FP» [7])

Разумеется, объекты в функциональном программировании должны быть неизменяемым.

Эмулировать объекты можно даже в функциональных языках программирования с помощью замыканий, см. статью “Function As Object” by Martin Fowler. Тут нельзя обойти вниманием замечательную книгу “Functional Programming for the Object-Oriented Programmer” by Brian Marick.

Давайте вспомним главу “Chapter 6. Working Classes: 6.1. Class Foundations: Abstract Data Types (ADTs): Handling Multiple Instances of Data with ADTs in Non-Object-Oriented Environments” книги «Code Complete» [2].

Абстрактный тип данных (АТД) — это набор, включающий данные и выполняемые над ними операции.

An abstract data type is a collection of data and operations that work on that data. («Code Complete» [2])

Абстрактные типы данных лежат в основе концепции классов.

Abstract data types form the foundation for the concept of classes. («Code Complete» [2])

Размышление в первую очередь об АТД (Абстрактный Тип Данных) и только во вторую о классах является примером программирования с использованием языка в отличии от программирования на языке.

Thinking about ADTs first and classes second is an example of programming into a language vs. programming in one. («Code Complete» [2])

Я не буду переписывать сюда достоинства АТД, их можно прочитать в указанной главе этой книги.

Но ведь изначально вопрос состоял в том, стоит ли отказываться от АТД в объектно-ориентированном языке при проектировании объектов предметной области в пользу “Anemic Domain Model”, и стоит ли приносить в жертву все выгоды Domain-Driven Design в угоду удобства конкретной реализации обработки связей?

The bottom, bottom line here is simply this. OO programming is good, when you know what it is. Functional programming is good when you know what it is. And functional OO programming is also good once you know what it is. («OO vs FP» [7])

Также стоит отметить, что далеко не все виды связей вписываются в концепцию агрегата. Если объект логически не принадлежит агрегату, то мы не можем вкладывать его в агрегат ради удобства разрешения связей, ибо в таком случае у нас интерфейс будет следовать за реализацией что в корне разрушает фундаментальный принцип абстракции. Также агрегат не совместим со связями типа Many-To-Many и перекрестными иерархиями связей.

Реализация связей путем присваивания

Принцип физического присваивания связанных объектов реализован также и в библиотеке js-data.

В нашей библиотеке мы предусмотрели как возможность декомпозиции агрегатов вложенных объектов, так и возможность их композиции из плоских данных в Repositories. Причем, агрегат всегда сохраняет актуальное состояние, и при добавлении, изменении, удалении объекта в Repository, изменения автоматически отображаются в структурах соответствующих агрегатов. Библиотека реализует это поведение как в парадигме Реактивного программирования, так и в парадигме Событийно-ориентированного программирования (на выбор).

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

Таким образом, для реализации связей объекту совершенно не требуется никакая служебная логика доступа к данным, что поддерживает нулевое сопряжение (Coupling) и образует кристально чистые доменные модели. Это значит, что доменные модели могут быть инстанцией “класса” Object.

Я также учел точку зрения, что доменная модель не должна отвечать за связи. Поэтому предусмотрена возможность легкого доступа к любому объекту через его Repository.

Исходный код

Footnotes

[1](1, 2, 3, 4) «Clean Code: A Handbook of Agile Software Craftsmanship» by Robert C. Martin
[2](1, 2, 3, 4, 5) «Code Complete» Steve McConnell
[3]«Patterns of Enterprise Application Architecture» by Martin Fowler, David Rice, Matthew Foemmel, Edward Hieatt, Robert Mee, Randy Stafford
[4](1, 2, 3, 4, 5, 6, 7) «Domain-Driven Design: Tackling Complexity in the Heart of Software» by Eric Evans
[5]«Design Patterns Elements of Reusable Object-Oriented Software» by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, 1994
[6](1, 2, 3) «Refactoring: Improving the Design of Existing Code» by Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts
[7](1, 2, 3, 4, 5, 6) «OO vs FP» by Robert C. Martin
[8]«Clean Architecture» by Robert C. Martin
[9]«The Clean Architecture» by Robert C. Martin
[10](1, 2, 3) «Extreme Programming Explained» by Kent Beck

Updated on Oct 04, 2017

Comments

comments powered by Disqus