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,
}
}
}
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[]
}
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
}
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]
}
}
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
}
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? \
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)