At some point you come across that principal, where your application and your components are handling data which is similar but slighly off. Sometimes your data comes from different sources, or your application creates variations of existing data.
This concept is using a variation of Data Transfer Objects to map data back and forth. DTO's are a common way to aggregate all the required data for a service into an object with a predefined structure. It works like an interface for a method, but allows way more complex arguments.
This is not the best explanation of a DTO, but it's enough to understand this concept.
The issue with a naive apporch
This is a simple task from our imaginary task manager. The structure can be used to load and update that item on our API.
// todos.example.com/api/todo/19{"id":19,"title":"Request access to the cake factory","priority":3,}
But what happens if we want to import data from a an other source?
// todone.example.com/tasks/29{"taskID":29,"name":"Request access to the cake factory","priority":"high",}
The data is similar enough to display them side by side, but the structure is different. You can not throw that data into your application without mapping it over to our structure.
The Quest
We want to be able to convert similar data to a structure our components can handle and export back to the old structure.
The Concept
The followiing class will convert, hold and transform our data from different sources. The following parts are used in this structured class.
The constructor defines private properties and it's empty state.
Note: We are intentionally not usingprivate class fields, since VueJS's Observer has trouble handling private fields (vuejs 2.*).
Create(data) is the import
classTodoBox{// Create a box from default data structurestaticcreate(data){if(!data)returnnull;const box =newTodoBox();
box.id= data.id;
box.title= data.title;
box.priority=PriorityBox.create(data.priority);return box;}//...
Our create method is responsible to setup a fresh object and convert the incomming data properly, so that we can use it within our class. We are using public setter for our properties, to ensure proper fallback values (more on that on the getters/setters section).
The combination of multiple boxes is what makes this concept powerful. We can create an other box based on our current creation path.
The point here is that we are controlling the transformation and fallback values.
Unfold is the export
classTodoBox{// Unfold a box to plain dataunfold(){return{
id:this.id,
title:this.title,
priority:this.priority?.unfold(),}}//...}
Unfold will return the encapuslated data back to a regular object. Into exact the same structure which has been used by the create(data) method.
This leeds to a very simple clone mechanism, where cascading is generating a new version of your stored data.
// Clone the current instanceclone(){returnTodoBox.create(this.unfold());}
Using Getter and Setter
classTodoBox{//...getid(){returnthis._id;}setid(value){this._id= value ??null;}//...}
Our getter and setter are controlling how other componets are interacting with our data. The setter is also responsible to provide a fallback value. The nullish coalescing operator?? will use the value on the right side if null or undefined was provided.
This ensures that we control our unset/empty values.
staticcreate(data){if(!data)returnnull;}
This is why our create function will return null on falsy values. A box is handling complex data most of the time, and primitive values are stored within a box.
Assembled parts leed the way
The following class is a base version of a box.
classTodoBox{constructor(){this._id=null;this._title=null;this._priority=null;}// Create a box from default data structurestaticcreate(data){if(!data)returnnull;const box =newTodoBox();
box.id= data.id;
box.title= data.title;
box.priority=PriorityBox.create(data.priority);return box;}// Unfold a box to plain dataunfold(){return{
id:this.id,
title:this.title,
priority:this.priority?.unfold(),}}// Clone the current instanceclone(){returnTodoBox.create(this.unfold());}// Getter and Setter belowgetid(){returnthis._id;}setid(value){this._id= value ??null;}// and so on...}
The Solution
Our data flow works by creating a box out of data, using the box withing our components, and exporting them back to raw objects to send them to an API.
const taskBox =TaskBox.create({"id":19,"title":"Request access to the cake factory","priority":3,});console.log(taskBox.title)// -> "Request access to the cake factory"console.log(taskBox.priority.label)// -> PriorityBox.label -> "HIGH"
taskBox.unfold();// -> { "id": 19, "title": "Request access to the cake factory", "priority": 3 }
Let's get back to our first attempt, and import data with a different structure.
const taskBox =TaskBox.createFromTodone({"taskID":29,"name":"Request access to the cake factory","priority":"high",});console.log(taskBox.title)// -> "Request access to the cake factory"console.log(taskBox.priority.label)// -> PriorityBox.label -> "HIGH"
taskBox.unfoldToTodone();// -> { "taskID": 29, "name": "Request access to the cake factory", "priority": "high" }
To achive the previous example, it's required to explicitly write down the import and export paths inside of our box.
This leeds to a lot of import logic, but that is why we need the box in the first place.
Let's add the following methods to our box.
classTaskBox{// Create a box from todone data structurestaticcreateFromTodone(data){if(!data)returnnull;const box =newTodoBox();
box.id= data.taskID;
box.title= data.name;
box.priority=PriorityBox.createFromTodone(data.priority);return box;}// Unfold a box to plain data in the structure of todoneunfoldToTodone(){return{
taskID:this.id,
name:this.title,
priority:this.priority?.unfoldToTodone(),}}//...}
Note: The path of our createFrom* and unfoldTo* calls it's children, they may not change, but they carry the information of the transformation source eg. createFromTodone or unfoldToTodone.
Conclusion
With the explicit declaration of data flows, it's possible to work with "same same but different" data structures.
This concept reaches it's potential where you mix and match.
Hey there! I'm Johannes aka Scribblerockerz, a fullstack developer from Germany. Most of the time
I'm trying to convert concepts into code.
Hit me up on twitter
if you want to talk!