According to Data-Oriented programming, the best way to reduce complexity of information systems is to follow three basic principles:
- Separate code from data
- Keep data immutable
- Represent data with generic data structures
Here are a series of 6 short programming challenges and their solutions written in JavaScript according to the principles of Data-Oriented programming. The purpose is to illustrate the simplicity of Data-Oriented programming.
If you agree with DOP principles, please implement your solutions in the programming language of your choice, according to DOP principles. I'm sure you'll enjoy!
If you disagree with one or more DOP principles, feel free to implement the solutions by breaking one or more principles, and explain why you think that your solution is simpler than the ones that I wrote.
Rules
- You are allowed to choose any programming language
- You are allowed to use any third-party library
- You are allowed to use reflection
- In the context of the challenges, simplicity is more important than performances.
- Submit your code snippets as a pull request to the official book source code Github repository, under the
challenges
folder - In case you disagree with DOP, please add a few words as comments in your code that explain why you think that your solution is simpler than the ones that I wrote.
- Four copies of Data-Oriented programming will be given away among the folks that submit a correct solution to at least 4 of the challenges.
Data model
Here is the library data model in an informal way:
More formally, here is a UML diagram that describes the relationships between the data entities of the library:
Here is an example of library data in JavaScript that we are going to use through this article:
var libraryData = {
"name": "The smallest library on earth",
"address": "Here and now",
"catalog": {
"booksByIsbn": {
"978-1779501127": {
"isbn": "978-1779501127",
"title": "Watchmen",
"publicationYear": 1987,
"authorIds": ["alan-moore",
"dave-gibbons"],
"bookItems": [
{
"id": "book-item-1",
"rackId": "rack-17",
},
{
"id": "book-item-2",
"rackId": "rack-17",
}
]
}
},
"authorsById": {
"alan-moore": {
"name": "Alan Moore",
"bookIsbns": ["978-1779501127"]
},
"dave-gibbons": {
"name": "Dave Gibbons",
"bookIsbns": ["978-1779501127"]
}
}
},
"userManagement": {
"librarians": {
"franck@gmail.com" : {
"email": "franck@gmail.com",
"encryptedPassword": "bXlwYXNzd29yZA=="
}
},
"members": {
"samantha@gmail.com": {
"email": "samantha@gmail.com",
"encryptedPassword": "c2VjcmV0",
"isBlocked": false,
}
}
}
};
~~~{% endraw %}
# Warm up
What's the title of the book whose ISBN is "978-1779501127" in upper case?
In this article, I am using [Lodash FP](https://github.com/lodash/lodash/wiki/FP-Guide) configured so that it never mutates data in place. Instead of mutating data in place, functions like {% raw %}`_.set()` create a new version.
~~~javascript
var fp = _.noConflict();
var _ = fp.convert({
// Specify capping iteratee arguments.
'cap': false,
// Specify currying.
'curry': false,
// Specify fixed arity.
'fixed': false,
// Specify immutable operations.
'immutable': true,
// Specify rearranging arguments.
'rearg': false
});
~~~
~~~javascript
var informationPath = ["catalog", "booksByIsbn", "978-1779501127", "title"];
_.get(libraryData, informationPath).toUpperCase();
~~~
In Data-Oriented programming, each piece of information has an information path. As you'll see through the upcoming challenges, this unusual approach has many benefits.
# Challenge #1: Retrieve a piece of information
**Challenge **: Write a function named {% raw %}`getBookProperty`{% endraw %} that receives library data and ISBN and a field name and returns the value of the field for the book with the given ISBN{% raw %}
~~~javascript
function getBookProperty(libraryData, isbn, fieldName) {
var informationPath = ["catalog", "booksByIsbn", isbn, fieldName];
return _.get(libraryData, informationPath);
}
~~~{% endraw %}
Example of usage:{% raw %}
~~~javascript
getBookProperty(libraryData, "978-1779501127", "title");
~~~{% endraw %}
In Data-Oriented programming, data fields are first-class citizens. We are free to create and combine field names dynamically in our program.
# Challenge #2: Search information
**Challenge **: Write a function named {% raw %}`bookInfo`{% endraw %} that receives library data and a string and returns a JSON string that contains book information about the books whose title contains the given string, in a case insensitive way. Book information is made of: title, isbn, author full names.
**Remark**: You are not allowed to extract author names from author ids. Assume that author ids are opaque strings.{% raw %}
~~~javascript
function authorNames(catalogData, book) {
return _.map(_.get(book, "authorIds"),
function(authorId) {
return _.get(catalogData, ["authorsById", authorId, "name"]);
});
}
function bookInfo(catalogData, book) {
return {
"title": _.get(book, "title"),
"isbn": _.get(book, "isbn"),
"authorNames": authorNames(catalogData, book)
};
}
function searchBooksByTitle(libraryData, query) {
var catalogData = _.get(libraryData, "catalog");
var allBooks = _.get(catalogData, "booksByIsbn");
var matchingBooks = _.filter(allBooks, function(book) {
return _.get(book, "title").toLowerCase()
.includes(query.toLowerCase());
});
return JSON.stringify(_.map(matchingBooks, function(book) {
return bookInfo(catalogData, book);
}));
}
~~~{% endraw %}
Example of usage:{% raw %}
~~~javascript
searchBooksByTitle(libraryData, "watCH");
~~~{% endraw %}
Here, the main benefits are the power of expression of {% raw %}`map`{% endraw %} and {% raw %}`reduce`{% endraw %} combined with the freedom of creating on the fly a book info structure and serialize it for free.
# Challenge #3: Add a piece of information
**Challenge**: Write a function named {% raw %}`blockMember`{% endraw %} that receives library data and an email address and returns a new version of library data **without altering the original version**, where the user with the given email is blocked.
Remember that I am using a version of Lodash that, instead of mutating data in place, creates a new version.{% raw %}
~~~javascript
function blockMember(libraryData, email) {
var informationPath = ["userManagement", "members", email, "isBlocked"];
return _.set(libraryData, informationPath, true);
}
~~~{% endraw %}
Example of usage:{% raw %}
~~~javascript
blockMember(libraryData, "samantha@gmail.com");
~~~{% endraw %}
In Data-Oriented programming, data is immutable. Functions like {% raw %}`_.set()_`{% endraw %} make it efficient (both in terms of memory and computation) to create modified versions of data.
# Challenge #4: Rename keys in a data entity
**Challenge**: Write a function named {% raw %}`renameKeys`{% endraw %} that receives a data entity and a key mappings and returns a new data entity, without altering the original entity, where the fields are renamed according to the key mappings{% raw %}
~~~javascript
function renameKeys(map, keyMap) {
return _.reduce(keyMap,
function(res, newKey, oldKey) {
var value = _.get(map, oldKey);
var resWithNewKey = _.set(res, newKey, value);
var resWithoutOldKey = _.omit(resWithNewKey, oldKey);
return resWithoutOldKey;
},
map);
}
~~~{% endraw %}
{% raw %}`renameKeys`{% endraw %} works with author entities:{% raw %}
~~~javascript
var alanMoore = {
"name": "Alan Moore",
"bookIsbns": ["978-1779501127"]
};
renameKeys(alanMoore, {"bookIsbns": "books"});
~~~{% endraw %}
{% raw %}`renameKeys`{% endraw %} works also with book item entities:{% raw %}
~~~javascript
var bookItem = {
"id": "book-item-1",
"rackId": "rack-17",
"isLent": true
};
renameKeys(bookItem, {"rackId": "id",
"id": "bookItemId"});
~~~{% endraw %}
In Data-Oriented programming, data entities are represented with generic data structures that can be manipulated with generic functions that work with any data entity.
# Challenge #5: Merge pieces of information
**Challenge**: Write a function named {% raw %}`mergeAndSerialize`{% endraw %} that receives two pieces of book information, one from the database and one from an external service like [Open Library Books API](https://openlibrary.org/dev/docs/api/books) and returns a JSON string with information from both sources.
~~~javascript
var watchmenFromDB = {
"isbn": "978-1779501127",
"title": "Watchmen",
"publicationYear": 1987,
"authorIds": ["alan-moore",
"dave-gibbons"],
"bookItems": [
{
"id": "book-item-1",
"rackId": "rack-17",
"isLent": true
},
{
"id": "book-item-2",
"rackId": "rack-17",
"isLent": false
}
]
};
var watchmenFromOpenLib = {
"publishers": [
"DC Comics"
],
"number_of_pages": 334,
"weight": "1.4 pounds",
"physical_format": "Paperback",
"subjects": [
"Graphic Novels",
"Comics & Graphic Novels",
"Fiction",
"Fantastic fiction"
],
"isbn_13": [
"9780930289232"
],
"title": "Watchmen",
"isbn_10": [
"0930289234"
],
"publish_date": "April 1, 1995",
"physical_dimensions": "10.1 x 6.6 x 0.8 inches"
}
~~~{% endraw %}
We simply merge the two maps:{% raw %}
~~~javascript
_.merge(watchmenFromDB, watchmenFromOpenLib);
~~~{% endraw %}
And we JSON serialize the result:{% raw %}
~~~javascript
function mergeAndSerialize(a, b) {
return JSON.stringify(_.merge(a, b));
}
~~~{% endraw %}
When we represent data with generic data structures, we benefit from many well defined functions like {% raw %}`merge`{% endraw %}, implemented either in the programming language itself or in third-party libraries like {% raw %}`Lodash.js.
# Challenge #6: Compare versions of data
**Challenge**: Write a function named `{% endraw %}diff{% raw %}` that receives two versions of library data and returns an object that contains the diff between the two versions, in the format of your choice.
~~~javascript
function diffObjects(data1, data2) {
var emptyObject = _.isArray(data1) ? [] : {};
if(data1 == data2) {
return emptyObject;
}
var keys = _.union(_.keys(data1), _.keys(data2));
return _.reduce(keys,
function (acc, k) {
var res = diff(_.get(data1, k),
_.get(data2, k));
if((_.isObject(res) && _.isEmpty(res)) ||
(res == "data-diff:no-diff")) {
return acc;
}
return _.set(acc, [k], res);
},
emptyObject);
}
function diff(data1, data2) {
if(_.isObject(data1) && _.isObject(data2)) {
return diffObjects(data1, data2);
}
if(data1 !== data2) {
return data2;
}
return "data-diff:no-diff";
}
diff(libraryData, updatedLibraryData);
diff(libraryData, libraryData);
When every piece of data in the system is represented with generic data structures, it is quite easy to compare recursively different data versions.
Conclusion
Do you like 😃 DOP or do you hate 😡 it?
Anyway, it's time to show off your coding skills 🧑💻!
Submit your code snippets as a pull request to the official book source code Github repository, under the challenges
folder.
In case you disagree with DOP, please add a few words as comments in your code that explain why you think that your solution is simpler than the ones that I wrote.
This article was first published on my blog.
Top comments (0)