DEV Community

Jin
Jin

Posted on • Originally published at mol.hyoo.ru

Decomposition of Software Components

Hello, my name is Dmitriy Karlovskiy and I... love power. When I take hold of the keyboard, every bit begins to dance to my tune. But when there are really a lot of these bits, it becomes difficult to keep track of them all. So let's compare popular design patterns that allow you to divide a large application into components so you can control them as efficiently and independently as possible.

Since the same application entity occurs in an application in many places, in different contexts, and must have different representations, the basic decomposition consists of selecting a model of the subject area, which is the source of truth for all places where it is viewed. And here the nuances begin...

πŸ’‘ Please note that further arrows do not show the movement of data, as they are often drawn, but the presence of knowledge of one component of the system how to work with another. In the limit, this knowledge is expressed in complete control of the life cycle: from creation to destruction. The lack of knowledge gives independence from a specific implementation, and therefore the ability to work with different implementations.

Model-View

The model knows how to present herself in different ways.

Example

class User { // Model

    _id: bigint
    _nickname: string

    toString() { // View
        return 'user=' + this._id
    }

    toJSON() { // View
        return {
            id: String( this._id ),
            name: this._nickname,
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Features

βœ… It is convenient to receive any displays from the model.
❌ Adding a new display requires changing the model.
❌ The display is completely determined by one main model.
❌ Loading the model pulls out all its displays according to dependencies.
❌ Two layers are too few on a large scale.

View-Model

The code for working with models is written directly in the display.

Example

// View
function Task_list() {
    return <ul>{
        Task.list.map( task =>
            <li><Task_row {task} /></li>
        )
    }</ul>
}

// Model
class Task {
    static list = [] as Task[]
}
Enter fullscreen mode Exit fullscreen mode

Features

βœ… Display can use arbitrary models.
βœ… Easily add new displays without changing models.
❌ To display different models, you need to duplicate the display code.
❌ Changing the model interface requires updating all displays that use it.
❌ Two layers are too few on a large scale.

Model-View-ViewModel

Mappings work with models through intermediaries that transform domain abstractions into mapping abstractions and back. The ViewModel also acts as a store of non-domain view state.

Example

// View
<li class="User_card" model="User_card_model">
    <img src={ image } />
    <p>{ message }</p>
</li>

// ViewModel
class User_card_model {
    user = User.current
    get image() {
        return this.user.avatar
    }
    get message() {
        return this.user.nickname
    }
}

// Model
class User {
    avatar: string
    nickname: string
    static current = new User
}
Enter fullscreen mode Exit fullscreen mode

Features

βœ… Display can use arbitrary ViewModels.
βœ… Easily add new displays without changing either the model or the ViewModel.
βœ… Changing the model interface or display requires changing only the ViewModel.
βœ… The same ViewModel can be shared between several displays.
❌ To display different models, you need to duplicate the display code and ViewModel.
❌ Three layers are too few on a large scale.

Model-View-Controller

The controller creates the display and tells it which model to work with. He also processes all commands from the user and manages his charges.

Example

// Controller
class Users_resource {
    GET() {
        return User.all.map( user_brief )
    }
}

// View
function user_brief( user: User ) {
    return {
        id: user.guid,
        name: user.passport.name_full,
    }
}

// Model
class User {

    static all = [] as User[]

    guid: GUID
    passports: Passport[]
    resumes: Resume[]

    get passport() {
        return this.passports[0]
    }

}
Enter fullscreen mode Exit fullscreen mode

Features

βœ… Display can use arbitrary models with the same interface.
βœ… Easily add new displays without changing models. And vice versa.
❌ To display different types of models, you need to duplicate the display code.
❌ Changing the model interface requires updating all views and controllers that use it.
❌ Three layers are too few on a large scale.

Model-View-Presenter

Models and views are passive and do not know about each other - they are controlled by the presenter, which also acts as an intermediary between them.

Example

// Presenter
class User_preview {
    user: User
    card = new Card({
        image: this.user.avatar,
        message: this.user.nickname,
        color: this.user.skin.color,
        click: ()=> this.skin_change(),
    })
    skin_change() {
        this.user.skin = Skin.random()
    }
}

// View
<div class="Card" onclick={click} style={{ background: color }}>
    <img src={ image } />
    <p>{ message }</p>
</div>

// Model
class User extends Model {
    avatar: string
    nickname: string
    skin: Skin
}
Enter fullscreen mode Exit fullscreen mode

Features

βœ… Easily add new displays without changing models. And vice versa.
βœ… Changing the interfaces of the model or display requires changing only the presenters.
❌ Three layers are too few on a large scale.
❌ To use the state of one presenter from another, it is necessary to artificially transfer it into the model.

ModelView Fractal

Each ModelView acts as a model/controller for slave ModelViews and as a view for the owning ModelView. Part of the logic can be transferred to both pure Model and pure View, which are only degenerate cases of ModelView.

Example

$my_user_list $my_view
    - \Owner ModelView
    users? /$my_user
    kids /
        <= Row*0 $my_user_row
            user <= user*

$my_user_row $my_card
    - \Having ModevView
    user $my_user
        avatar => image
        nickname => message

$my_card $my_view
    - \View not Model
    kids /
        <= Image $my_image
            uri <= image \about:blank
        <= Message $my_text
            text <= message \

$my_user $my_model
    - \Model not View
    avatar? \
    nickname? \
Enter fullscreen mode Exit fullscreen mode

Features

βœ… Each ModelView fully controls internal ModelViews and knows nothing about external ones.
βœ… Any ModelView can navigate between different other ModelViews at any composition level.
βœ… Changing the ModelView interface requires changing only its owners.
βœ… Fractal structure easily scales to applications of any size.

Conclusions

$mol_view is built on the ideas of MVF, since this is the simplest decomposition pattern that easily scales as needed and well separates different levels of abstraction.

Top comments (0)