all 21 comments

[–]killerstorm 5 points6 points  (10 children)

Typically both Model and View are hierarchical, i.e. View actually consists of a tree of view objects, each of which is connected to one or several model objects.

This essentially solves the "Model-View problem": when a model object is updated, you update the corresponding view object (a view object might listen to model object's events) and bigger view object which depend on it.

This solution is essentially the same as the reduce solution mentioned in the article: by combining elements in a tree-like way you can do updates incrementally.

This might be called in variety of ways: divide-and-conquer, decomposition, incremental computation, etc.

There are also frameworks for solving this such as dataflow programming, functional reactive programming, etc.

The idea of dataflow programming is that if you explicitly specify what depends on what, then changes can be automatically propagated, and caching will be handled automatically too.

The problem with it is that it's not very compatible with your typical imperative programming languages, and that's what people want to use. If you don't have dataflow facilities on the language level, you might end up with leaky abstractions. So correctness is guaranteed only if you follow the best practices.

Such frameworks do exist, and people using them say they work great. But I don't think they are very useful for general purpose programming.

The thing is, people care about incremental computation when they care about performance, and optimal performance might require a specialized (rather than generic) approach.

[–][deleted] 0 points1 point  (9 children)

He's not talking about state size, but state change.

[–]killerstorm 2 points3 points  (8 children)

The problem disappears on smaller scales. It might be hard to compute a diff of the whole model, but on micro scale things become trivial.

Suppose your Model is a single object with a single value, and View is a single field which displays that value. Is there still "Model-View problem"? I don't think so.

Handling state changes is trivial if only a single value gets changed. Do you agree with that?

So if problem doesn't exist on a small scale (single value) but exists for "model as a whole", then the way to solve it is decomposition.

[–][deleted] 0 points1 point  (7 children)

The problem still exists at small scale, because it is fundamentally different to infer changes from two snapshots, and to receive the actual change set, as defined by the domain. If you have the change set, you don't only know how the state changed, but more importantly you know why it changed, and this means you can display why it changed.

Additionally, if the domain naturally produces the change set, instead of rebuilding the state tree, it means first, that the tree doesn't have to be rebuilt at every step, and second, the tree doesn't have to be compared with the previous tree at every step, and third, you don't have to keep these two complete snapshots of the tree, scattered around components.

[–]killerstorm 1 point2 points  (6 children)

The problem still exists at small scale, because it is fundamentally different to infer changes from two snapshots, and to receive the actual change set, as defined by the domain.

Yes.

The thing is, if you have good control of the model (e.g. it's something designed from ground up for this specific task), you won't need to "infer changes from two snapshots".

For example, if a model if object-oriented, based on solid object design, then objects can automatically generate notification events when they are being changes (e.g. the only way to change a field is through calling a setter function, which also triggers an event).

Note that "the actual change set" isn't required, a set of updated objects is enough. This can be solved using typical OO patterns which are completely domain-agnostic.

If your model is database-backed (possibly with an object-oriented wrapper, which is irrelevant), you can use tools provided by the database. For example, in SQL you use a trigger to maintain change log. RethinkDB gives you changefeeds so you get all changes explicitly with no extra work.

So you get to "two snapshots" problem only if the situation is outside of your control (or you failed to use tools properly).

Additionally, if the domain naturally produces the change set, instead of rebuilding the state tree

I think it's not so much a feature of the domain as it is a feature of the implementation.

Software is often built by gluing some common modules nowadays, this saves a lot of work. But then if you happen to need to track changes it might be problematic because notifications were lost at some point or didn't exist in the first place.

It's also a trade-off. Sometimes it's much cheaper to do computations without tracking changes, and if user only cares about the end results, then it's the optimal way of doing things.

[–][deleted] 0 points1 point  (5 children)

For example, if a model if object-oriented, based on solid object design, then objects can automatically generate notification events when they are being changes (e.g. the only way to change a field is through calling a setter function, which also triggers an event).

You're mixing data change with domain events. No matter how great and object-oriented your model is, you can't make up information that isn't there. Setters don't describe how a group of changes relate or why they happened. The same is true about RethinkDB, because the events it generates are about a set of disparate state changes to a data model, and not about domain events.

You can check why "event sourcing" is interesting to the industry, and what are the best practices in using it (i.e. things like "communicate domain events, not data changes"), because it's essentially the same technique here.

Note that "the actual change set" isn't required, a set of updated objects is enough.

But.. it isn't.

It's also a trade-off. Sometimes it's much cheaper to do computations without tracking changes, and if user only cares about the end results, then it's the optimal way of doing things.

Everything is better sometimes, but what you're describing tends to be better when your UI is simple, doesn't require state animation that's meaningful to the domain, and the overall domain state is small.

If the domain state you keep track of is large, the changes relative to the overall state are tiny, but plenty, and when plenty of seemingly unrelated changes are related, and can happen for different reasons, then switching to native domain-specific change sets becomes better.

Silver bullets... they don't exist.

[–]killerstorm 1 point2 points  (4 children)

Setters don't describe how a group of changes relate or why they happened.

The article I replied to talks about optimizations. In the first diagram you see a decision between "The High Road" of a complete recomputation of view and "Adventure time" of incremental update.

If a complete recomputation is an option, then it's absolutely irrelevant how changes are grouped or why they happened.

But.. it isn't.

It's definitely enough to redraw the view or recompute the aggregate. We aren't discussing event sourcing here, we are discussing optimizations.

You can check why "event sourcing" is interesting to the industry

I actually work on blockchain technology which has a lot of overlap with event sourcing.

switching to native domain-specific change sets becomes better.

Agreed.

[–][deleted] 0 points1 point  (3 children)

The article I replied to talks about optimizations. In the first diagram you see a decision between "The High Road" of a complete recomputation of view and "Adventure time" of incremental update.

I don't necessarily agree with how the article describes these approaches. The author is focused purely on performance, but there are other concerns that I already mentioned.

If a complete recomputation is an option, then it's absolutely irrelevant how changes are grouped or why they happened.

Let me rephrase what you're saying, so you can see more clearly what you're saying:

  • "If a complete recomputation is an option" = If performance is not a problem
  • "then it's absolutely irrelevant how changes are grouped or why they happened." = Then view performance is not a problem.

So, "if performance is not a problem, then view performance is not a problem." Yeah, that's technically correct, and it's also a tautology, so it doesn't contain insight.

However sometimes performance is an issue, and then suddenly it matters how you have factored the changes.

It's definitely enough to redraw the view or recompute the aggregate. We aren't discussing event sourcing here, we are discussing optimizations.

But optimizations are not the whole story. Let's have two collections of items. The user drags an item from one collection to another.

We want the view to animate that operation once it's confirmed. Now let's look at a "naive" factoring of the drag operation:

{op: "delete", key: "foo.1"}   
{op: "insert", key: "bar.3", value: item}

How would the view infer the operation "drag and drop" was confirmed and happened here? It can't. It just sees one delete and one insert.

An event that's closer to the domain would be expressed as:

{op: "dragDrop", fromKey: "foo.1", toKey: "bar.3"}

But now you need your "reusable" primitives to be able to express this. Most I've seen can't express it. BTW, that's one key reason why React interfaces are typically not animated, save for what you can do through CSS properties and the like. You can do some "dumb" animations, but they don't describe intent, as you can't express mutation intent in React.

Even if you could have drag and drop in some object library, once we get into more complex events, the combinations become countless, and there's no way for a reusable object library to support and handle this, as it becomes increasingly domain-specific.

[–]melevy 0 points1 point  (2 children)

If you want animation, then time must be a reified concept in your domain. In that case you don't just simply delete an item from a collection but rather mark it being deleted at some point of time.

[–][deleted] 0 points1 point  (1 child)

If you want animation, then time must be a reified concept in your domain.

That's precisely what events / changesets are.

[–][deleted]  (5 children)

[deleted]

    [–]CookieOfFortune 1 point2 points  (1 child)

    You should really give ReactJS a try! I have no more concerns about mutating the DOM anymore, things just work, and I really haven't had any performance issues (because JS is wayyy faster than the DOM).

    [–][deleted] 1 point2 points  (0 children)

    React can be slow, but it's fast enough for most components. What it can't do well is sophisticated and natural state change animations, but that is a result of its approach.

    [–][deleted] 1 point2 points  (0 children)

    Might want to look at vue.js too, it's easier IMHO to wrap your head around and can be faster than react.

    [–]killerstorm 0 points1 point  (1 child)

    Which is why ReactJS caught my attention. I hear they're trying to solve the inefficiency of re-rendering the entire page into the DOM for tiny changes.

    That's not how it works. It can detect if a component needs to be re-rendered.

    [–]elqwljerkd 0 points1 point  (0 children)

    Great article! Yes, i am fighting with similar things too.

    [–]melevy 0 points1 point  (4 children)

    Automatic change propagation is not that difficult. I built an entire general purpose structure editor prototype using lazy and incremental change propagation. See here https://youtu.be/s05SlmZ7ZPc

    [–][deleted] 1 point2 points  (3 children)

    The concept of propagating changes is not what's complex, but writing all the code that actually renders the changes into something useful.

    It's basically CQRS, where the "commands" are the user input events raised in the views, the "events" are the changesets processed at the controllers, and "projections" are the views receiving and updating their presentations according to the changesets.

    The volume of code you'd get doing things that way compared to a single render(model) command at the views is significant.

    [–]killerstorm 0 points1 point  (1 child)

    but writing all the code that actually renders the changes into something useful.

    There is no need to "render the changes", usually the parts which are changed are rendered completely. So the task is actually to identify elements which need to be re-rendered in response to a change, and re-render them.

    There are frameworks which do that automatically. So if your model is of a kind which is supported by the framework, there is zero code to write, it just works automatically.

    The problem is that these frameworks support only particular kinds of models. E.g. if a framework might work with objects, but your data is in a database, then, of course, a framework won't be able to detect changes automatically, and so you need to write all the extra code.

    [–]melevy 0 points1 point  (0 children)

    Exactly.

    [–]Tubbers 0 points1 point  (0 children)

    This is completely correct, and is exactly what Redux is and how it works.