Making complex problems easier by decomposing and composing

Making complex problems easier by decomposing and composing
Our natural way of dealing with complexity is to break it into smaller pieces and then put everything back together.
This is a two step process:
  • decompose the problem into smaller parts
  • compose the small parts to solve the problem
We decompose in smaller parts because they are easier to understand and implement. The smaller parts can be developed in parallel.
The process of decomposition is about assigning responsibilities and giving names. This makes it easy to talk and reason about. Once we identify a responsibility, we can reuse it.
Composition is about combining the small parts together and establishing a relationship between them. We decide the way these pieces communicate, the order in which they execute, and how data flows between them.
We find a system hard to understand even if it is split in smaller parts, if there is a high number of relations between these parts. In order to make a system easier to understand, we need to minimize the number of possible connections between its parts.

Object decomposition

Objects are more than state and behavior working together. Objects are things with responsibilities.

Decompose

In How to create a three layer application with React, I take a to-do list application and split the responsibilities between the following objects :
  • TodoDataService : responsible for the communication with the server Todo API
  • UserDataService : responsible for the communication with the server User API.
  • TodoStore : the domain store for managing to-dos. It is the single source of truth regarding to-dos.
  • UserStore : the domain store for managing users.
  • TodoListContainer : the root container component displaying the list of to-dos.
As you can see, when decomposing, I assign responsibilities and give names.

Compose





Next, I compose them together in a single function. This is the place where all objects are created and dependencies injected. It is called Composition Root.
import React from "react";
import ReactDOM from 'react-dom';
import TodoDataService from "./dataaccess/TodoDataService";
import UserDataService from "./dataaccess/UserDataService";
import TodoStore from "./stores/TodoStore";
import UserStore from "./stores/UserStore";
import TodoContainer from "./components/TodoContainer.jsx";

(function startApplication(){
    let userDataService = UserDataService();
    let todoDataService = TodoDataService();
    let userStore = UserStore(userDataService);
    let todoStore = TodoStore(todoDataService, userStore);
    
    let stores = {
      todoStore,
      userStore
    };
  
    function loadStaticData(){
      return Promise.all([userStore.fetch()]);
    }
 
    function mountPage(){  
      ReactDOM.render(
        <TodoContainer stores={stores} />,
        document.getElementById('root'));
    }

    loadStaticData().then(mountPage);
})();

Method decomposition

In How point-free composition will make you a better functional programmerI take a bigger task and split it in smaller parts.
I identify two kinds of methods:
  • Methods doing one task. I aim to make this as small pure functions.
  • Methods coordinating the smaller functions.

Decompose

Let’s read the requirement and decompose the problem in smaller tasks:
We need to find the technology titles in a list of books, prepare the book object with all information for the view, and sort the books by the author’s name.
  • isTechnology() predicate to check if it is a technology book
  • toViewBook() to build an object with all the information for the view
  • asByAuthorname() to sort two books ascending by the author’s name
  • getBooks() to combine all these small functions together
function isTechnology(book){
   return book.type === "T";
}
function toBookView(book){
  return Object.freeze({
    title : book.title,
    author : authors[book.authorID].name
  });
}
  
function ascByAuthor(book1, book2){
  if(book1.author < book2.author) return -1;
  if(book1.author > book2.author) return 1;
  return 0;
}
Again, each small part has a name and a responsibility.

Compose

The getBooks() is the coordinator method composing all together. I use the point-free functional style to join the small tasks.
function getBooks(){
  return books.filter(isTechnology)
              .map(toBookView)
              .sort(ascByAuthor);
}

Decomposition with Components

Decomposition is a common theme when implementing the user interface.

Decompose

Consider the flowing application managing a to-do list.



In How to communicate between Components, I decompose the page in the flowing components :
  • TodoAddForm : the form for adding a new to-do
  • TodoSearchForm : the form for searching a to-do
  • TodoList : the list for displaying the to-dos
  • TodoListItem: a single to-do item from the list
  • FormInput : a text-box
  • FormButton : a button
In the process of decomposing the page, I give names to components and assign them responsibilities.

Compose

Then, I compose the components back together in a tree structure.


Components’ Tree Structure

For composing them back I use HTML tags. Look at the flowing code from TodoContainer :
//Vue Component Template
<div>
<todo-add-form @add="add"></todo-add-form>
<todo-search-form @search="search"></todo-search-form>
<todo-list :todos="todos"></todo-list>
</div>
//React Componentreturn class TodoContainer extends React.Component {
 render() {
 return <div>
<TodoAddForm onAddClick={this.add.bind(this)} />
<TodoSearchForm onSearchClick={this.search.bind(this)} />
<TodoList todos={this.state.todos} />
 </div>;
 }
}
The bigger challenge in this case is the communication between components.

Decomposition with Decorators

In How to use Decorators with Factory Functions, I decompose the primary and secondary concerns of a method.
Consider this code:
function TodoStore(currentUser){
  let todos = [];
  
  function add(todo){
    let start = Date.now();
    if(currentUser.isAuthenticated()){
      todos.push(todo);
    } else {
      throw "Not authorized to perform this operation";
    }
            
    let duration = Date.now() - start;
    console.log("add() duration : " + duration);
  }
    
  return Object.freeze({
    add
  });  
}
The add() method has the primary responsibility of adding a to-do and two secondary responsibilities of authorizing the user and logging the duration of execution.

Decompose

We can decompose the secondary responsibilities in two decorators and give them intention-revealing names : authorize() and logDuration()
function add(todo){
    todos.push(todo);
}
function logDuration(fn){
  function decorator(...args){
    let start = Date.now();
    let result = fn.apply(this, args);
    let duration = Date.now() - start;
    console.log(fn.name + "() duration : " + duration);
    return result;
  }
  return decorator;
}
function createAuthorizeDecorator(currentUser){
  function authorize(fn){
    function decorator(...args){
      if(currentUser.isAuthenticated()){
        return fn.apply(this, args);
      } else {
        throw "Not authorized to execute " + fn.name + "()";
      }
    }
    return decorator;
  }
  return authorize;
}
let authorize = createAuthorizeDecorator(currentUser);

Compose

Then we compose everything together to create the original function.
let originalAdd = compose(logDuration,authorize)(add);
The compose() function can be found in libraries like underscore.js.

Conclusion

Decomposition is our main way of dealing with complexity.
Naming plays an important role in this process. Giving intention-revealing names makes the code easier to read.
Every time we decompose something, we also need the tools to compose it back. It is not enough to have a good pattern for splitting the problem in parts — we also need good ways to put everything back together.
Now it’s your turn! Are there any other decomposition techniques you use on a daily basis?Leave'em in the comments.

Post a Comment

1 Comments

  1. Via SEM advertising, our singapore pay per click packages will increase your brand's exposure on Google, ensuring that you are always number one as people search for products and services in your product line.

    ReplyDelete