DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

ryohey
ryohey

Posted on

Hi, again. Business Logic as a Good Old Procedural Programming

Why?

There is a gap between software specification and implementation. If we could write the program flow with a simple DSL and even run it ... This is just an idea, but we can do it. Half joke, half serious.

UI as function

My first thought was if we could write the user interface as an async function. In fact, it’s already we've seen.

if (confirm(β€œDo you want to send the message?”)) {
  somethingHappen()
} else {
  alert(β€œcancelled!”)
}
Enter fullscreen mode Exit fullscreen mode

Pretty simple, right? There are no any callbacks, no flux, no dispatch, no singleton state. It’s easily readable.

Re-invent the UI function

Let’s do as same way by using async/await in TypeScript.
We can define the view as async function that returns user input events.

interface View {
  confirmToSend: async () => boolean
}
Enter fullscreen mode Exit fullscreen mode

and then write the business logic.

const main = async (view: View) => {
  if (await view.confirmToSend()) {
    somethingHappen()
  }
}
Enter fullscreen mode Exit fullscreen mode

then implement View. At now I use React. it’s not important anyway.

class App implements View {
  async confirmToSend(): boolean {
    return new Promise((resolve) => {
      this.setState({
        isVisibleConfirm: true,
        onClickConfirmYes: () => resolve(true),
        onClickConfirmNo: () => resolve(false),
      })
    })
  }

  public render() {
    return <div>
      {this.state.isVisibleConfirm && 
        <div className="modal confirm">
          <p>Do you want to send the message?</p>
          <button onClick={this.state.onClickConfirmYes}>Yes</button>
          <button onClick={this.state.onClickConfirmNo}>No</button>
        </div>}
    <div>
  }
}
Enter fullscreen mode Exit fullscreen mode

The point is confirmToSend returns Promise that waits for user interaction.

Run them together.

ReactDOM.render(<App ref={view => view && main(view)} />)
Enter fullscreen mode Exit fullscreen mode

So, this application works according to business logic written in async / await.

Do you understand how we can write business logic in procedural way with React? We might need another example.

To-Do App

So let’s see the to-do app example.

First of all we write the business logic.

export interface Todo {
  title: string
  description: string
}

export interface Store {
  getTodos(): Promise<Todo[]>
  addTodo(todo: Todo): Promise<void>
}

export interface View {
  showTodos(todos: Todo[]): Promise<["click-todo", Todo] | ["create-todo"]>
  showTodo(Todo: Todo): Promise<["close"]>
  showCreateForm(): Promise<["cancel"] | ["create", Todo]>
  closeCreateForm(): Promise<void>
  closeTodo(Todo: Todo): Promise<void>
}

export const mainLoop = async (store: Store, view: View) => {
  initial: while (true) {
    const todos = await store.getTodos()
    const event = await view.showTodos(todos)

    switch (event[0]) {
      case "click-todo": {
        const e = await view.showTodo(event[1])
        switch (e[0]) {
          case "close":
            await view.closeTodo(event[1])
            continue initial
        }
        break
      }
      case "create-todo": {
        const e = await view.showCreateForm()
        switch (e[0]) {
          case "cancel":
            await view.closeCreateForm()
            continue initial
          case "create":
            await view.closeCreateForm()
            await store.addTodo(e[1])
            continue initial
        }
        break
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Looks pretty! This is self-contained, complete behavior definition of this app. We can understand how this app works by just reading this function. And this is a specification for the app.

Let's see some tricks.

Store

Store fetches data from somewhere and storing. We have the view as an interface. So we should write the store as an interface to separate business logic and implementation.

Infinite Loop

Main loop! You probably used an infinite loop long ago to make a game or something ...
This To-Do app should run until the user closes the tab, so it will be written in an infinite loop.

Label and continue

When user closes modal view, app should reset to the first state.
We can use continue to go back to start of while loop.

Events

We used boolean to handle user-interaction in above example.
But in real app, we have to handle many events for one showSomewhat function.
I defined some event types for each show functions using array. We can use the object that have type and payload. But type inference also works so I don't want to type too much.

["click-todo", Todo]|["close-todo"]
Enter fullscreen mode Exit fullscreen mode

maybe you like

interface CloseTodoEvent {
  type: "close-todo"
}

interface ClickTodoEvent {
  type: "click-todo"
  payload: Todo
}
Enter fullscreen mode Exit fullscreen mode

To-Do View

Now let see the View implementation.

import * as React from "react"
import { Todo } from "./main"

interface State {
  todos: Todo[]
  modalTodo: Todo | null
  isCreateFormVisible: boolean
  formTitle: string
  formDescription: string
  onClickTodo: (todo: Todo) => void
  onClickCreateNew: () => void
  onClickModal: () => void
  onClickAdd: () => void
  onClickCancelCreation: () => void
}

export class AppView extends React.Component<{}, {}> {
  public state: State = {
    todos: [],
    modalTodo: null,
    isCreateFormVisible: false,
    formTitle: "",
    formDescription: "",
    onClickTodo: (todo: Todo) => {},
    onClickCreateNew: () => {},
    onClickModal: () => {},
    onClickAdd: () => {},
    onClickCancelCreation: () => {}
  }

  showTodos(todos: Todo[]) {
    return new Promise<["click-todo", Todo] | ["create-todo"]>(resolve => {
      this.setState({
        todos,
        modalTodo: null,
        onClickTodo: (todo: Todo) => resolve(["click-todo", todo]),
        onClickCreateNew: () => resolve(["create-todo"])
      })
    })
  }

  showTodo(todo: Todo) {
    return new Promise<["close"]>(resolve => {
      this.setState({
        modalTodo: todo,
        onClickModal: () => resolve(["close"])
      })
    })
  }

  closeTodo(todo: Todo): Promise<void> {
    this.setState({ modalTodo: null })
    return Promise.resolve()
  }

  showCreateForm() {
    return new Promise<["cancel"] | ["create", Todo]>(resolve => {
      this.setState({
        formTitle: "",
        formDescription: "",
        isCreateFormVisible: true,
        onClickCancelCreation: () => resolve(["cancel"]),
        onClickAdd: () =>
          resolve([
            "create",
            {
              title: this.state.formTitle,
              description: this.state.formDescription
            }
          ])
      })
    })
  }

  closeCreateForm() {
    this.setState({
      isCreateFormVisible: false
    })
    return Promise.resolve()
  }

  public render() {
    const {
      todos,
      modalTodo,
      isCreateFormVisible,
      formTitle,
      formDescription,
      onClickCreateNew,
      onClickTodo,
      onClickModal,
      onClickCancelCreation,
      onClickAdd
    } = this.state
    return (
      <>
        <ul>
          {todos.map((t, i) => (
            <li className="todo" onClick={() => onClickTodo(t)} key={i}>
              <p className="title">{t.title}</p>
              <p className="description">{t.description}</p>
            </li>
          ))}
        </ul>
        <button onClick={onClickCreateNew}>Create new To-Do</button>
        {modalTodo !== null && (
          <div className="modal">
            <p className="title">{modalTodo.title}</p>
            <p className="description">{modalTodo.description}</p>
            <button onClick={onClickModal}>Close</button>
          </div>
        )}
        {isCreateFormVisible && (
          <div className="modal create-todo-form">
            <label>title</label>
            <input
              type="text"
              value={formTitle}
              onChange={e => this.setState({ formTitle: e.target.value })}
            />
            <label>description</label>
            <input
              type="text"
              value={formDescription}
              onChange={e => this.setState({ formDescription: e.target.value })}
            />
            <button onClick={onClickCancelCreation}>Cancel</button>
            <button onClick={onClickAdd}>Add</button>
          </div>
        )}
      </>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

This is little ugly but works. We may need somewhat library to do this better.

Screenshot

todo the movie
It works!
Let’s say hi again to good old procedural programming!

Further more

  • Can we apply this method to real-world application?
  • How we handle interrupted events?
  • How we handle the application that has multiple pane.
  • Does this business logic make tests more efficient?

Source Code

https://github.com/ryohey/Hi-Again

Top comments (2)

Collapse
aleclofabbro profile image
Alessandro Giansanti

Yes, I love that!
I started thinking this way too, during last year.
Some topics really opened my mind and I am applying them to typescript :

  • Algebraic Data Types
  • Elm architecture
  • Domain Modeling and DSL
  • and the "Making Impossible States Impossible" concept

This kind approach can be applied to UIs and to Backend Systems too !

Unfortunately, these approaches and concepts find a lot of mental resistance in most tech teams

Collapse
ryohey profile image
ryohey Author

Thank you very much! I am glad that you are interested.
I do not see such thought so much, so I would be happy if you could tell me more about your opinion.
Does this topic have a name? Do you know the articles or communities that I should look for?

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.