DEV Community

Discussion on: Taking Marko's Tags API for a Test Drive

Collapse
 
peerreynders profile image
peerreynders

My first impression was that we have moved from "what is that markup doing in my code?" to "why is my code scattered over all these tags?" Then I scrambled back to Introducing the Marko Tags API Preview to get a better sense of this effort's vision. Once I got a twinkle of an insight my first compulsion was to introduce this refactoring:

<!-- file: TodoStore.marko -->
import {
  updateTodo,
  clearCompleted,
  removeTodo,
  toggleAllTo,
  addTodo,
} from './store';

<let/nextId=1/>
<let/todos=[]/>
<let/filter="all"/>

<return = {
  todos,
  filter,
  setFilter(f) {
    filter = f;
  },
  updateTodo(todoId, newProps) {
    todos = updateTodo(todos, todoId, newProps);
  },
  clearCompleted() {
    todos = clearCompleted(todos);
  },
  removeTodo(todoId) {
    todos = removeTodo(todos, todoId);
  },
  toggleAllTodosCompleted(completed) {
    todos = toggleAllTo(todos, completed);
  },
  addNewTodo(todoData) {
    todos = addTodo(todos, nextId++, todoData);
  }
}/>
Enter fullscreen mode Exit fullscreen mode
// file: store.js
function updateTodo(todos, todoId, newProps) {
  return todos.map(updateProps);

  function updateProps(todo) {
    return todo.id !== todoId ? todo : { ...todo, ...newProps };
  }
}

const keepActive = (todo) => !todo.completed;

function clearCompleted(todos) {
  return todos.filter(keepActive);
}

function removeTodo(todos, todoId) {
  return todos.filter(keepOthers);

  function keepOthers(todo) {
    return todo.id !== todoId;
  }
}

function toggleAllTo(todos, completed) {
  return todos.map(syncCompleted);

  function syncCompleted(todo) {
    return todo.completed === completed ? todo : { ...todo, completed };
  }
}

function addTodo(todos, id, { title }) {
  return [
    ...todos,
    {
      title,
      id: 'c' + id.toString(),
      completed: false,
    },
  ];
}

export { updateTodo, clearCompleted, removeTodo, toggleAllTo, addTodo };
Enter fullscreen mode Exit fullscreen mode

This brings me to my concern which is expressed in UI as an Afterthought (and by extension Architecture the Lost Years ):

State is the root of all revenue.

Or put differently: Why is my core client-side application logic complected with the UI (framework)?

Now obviously the lets and return are necessary for the reactive system to track connections and dependencies — however the role of templates is to express structure via containment (and substitution), not interfaces for internal application communication and collaboration. On a different level while the UI depends on parts of application state, managing application state isn't its responsibility. So while the UI needs access to application state and perhaps even manage it's own, minor derived state it shouldn't be in the business of actually managing application state.


To elaborate on my perspective:

Conflation vs Separation

I'm familiar with Plank's Principle — especially the last part:

What does happen is that its opponents gradually die out, and that the growing generation is familiarized with the ideas from the beginning: another instance of the fact that the future lies with the youth.

The whole "innovation progresses one funeral at a time" perspective is often used to dismiss established insights justifying "out with the old, in with the new" failing to account for the fact that so called "innovations" have to stand the test of time to retain that title.

I believe one could make a convincing case that the introduction of JSX wasn't an "innovation" but in fact a regression. Even the Vue documentation perpetuates the faulty assumption that its premise is based on:

One important thing to note is that separation of concerns is not equal to separation of file types.

It never was about the separation of file types. Typically at this point I go into the how the separation relates to the web's approach to fault tolerance and resilience - however in this context it's not relevant as the compiler can be used to ensure the necessary post-compile separation.

The Vue documentation goes then further to claim:

In modern UI development, we have found that instead of dividing the codebase into three huge layers that interweave with one another, it makes much more sense to divide them into loosely-coupled components and compose them. Inside a component, its template, logic and styles are inherently coupled, and collocating them actually makes the component more cohesive and maintainable.

It's this type of argument that managed to get object-orientation programming into the mainstream - but over that past decade or so the cracks in that argument have become more and more noticeable. The argument has an undeniable allure which often enough fuels the popularity of the approach which in turn is then fielded as evidence of the original argument's validity.

Apart from failing to correctly identify the original motives behind the "separation of file types" it also doesn't address additional motivations in favour of maintaining separation deemed beneficial by practitioners:

Ultimately this was popularized as Unobstrusive JavaScript. In retrospect that moniker was unfortunate because it made it all too simple to dismiss it as irrelevant in the age of CSR frameworks which operate entirely in JavaScript. But that unjustifiably dismisses the core tenet and the benefits of:

    Separation of behaviour and structure

One may also be tempted to defend the likes of Vue and Svelte because internally (single file) components do have "separation of behaviour and structure" because markup and behaviour exist in separate sections - but ultimately that separation is meaningless as all of that simply compiles to pure conflated client-side JavaScript rather than generating server-side code capable of emitting fully formed markup that can smoothly rendezvous client-side with the JavaScript necessary for interactivity.

In terms of metaphor's one can look towards printing and graphical design. Their artefacts are often authored and produced in layers - often necessitating the use of registration marks to ensure correct alignment during production.

Similarly structure and behaviour are separate layers that have to be "in alignment".

It is easier, at least in the small, to conflate structure and behaviour because there are no "registration marks" to keep track of - however the component approach makes one fundamental assumption - that structural and behavioural boundaries coincide.

The simple approach keeps things separate and boundaries may or may not coincide - it just doesn't matter.

One blatant example of "boundary ambiguity" is a list; <ul>/<ol> vs <li>. List elements and list item elements are separate tags but are tightly coupled given that:

  • List elements should only have list item elements as direct child elements and
  • List item elements should only have a list element as their direct parent

That coupling could indicate that both <ul> and <li> should be both managed by the "List component" and that there is no need for an "Item Component" as that responsibility is covered by the "List Component".

At the same time there could be an argument that there can be behavioural aspects of a list item component that are more related to it being an "item" rather than being part of (and being positioned within) a list - at which point part of the <li> behaviour resides with the "List Component" while the remainder resides with a "List Item Component". Consequently the "behavioural boundary" doesn't occur cleanly between the "List Element" and <li> but right on top of the <li> element (unless one moves the "item" role to the child element which doesn't necessarily make semantic sense).

Inline event handlers to EventTarget.addEventListener() was an early move towards better separation of behaviour and structure.

One issue here from the structural (HTML) perspective is that there is no indication when and where added behaviour can be expected. This is exacerbated by the fact that there are no clear "registration marks" for any behaviour as the implementation could target a specific element through the rather broad means of any type of selector, e.g. by ID, Class or any other prominent feature in the overall document structure.

Contrast this with the clear separation of data and structure in a template, for example, in Nunjucks:

var tmpl = new nunjucks.Template('Hello {{ username }}');

tmpl.render({ username: "James" }); // -> "Hello James"
Enter fullscreen mode Exit fullscreen mode
  • At template design time the data's identity is clear as well the location(s) where it will be injected
  • At runtime the context (data) object is only concerned with mapping data values to the correct data identities independently of how that data may structured for display inside the template.

In their own way the double braces and variable names act as registration marks between the structural layer (template) and the data layer (context object).

Supporting a looping construct makes sense for supporting structural repetition; similarly conditional structures; perhaps even parametric structures;

That doesn't mean that Nunjucks (or Jinja) is some kind of gold standard. It (and many other templating languages) offers filters. In the context of their domain this makes sense as there is no behavioural layer.

Templates are primarily crafted to transform data structured for information processing to data structured for display. In the absence of a dedicated behavioural layer it is an accepted trade-off to conflate (some minor) data conversion within the structural template while accepting the consequence of increased complexity of the of the templating system — rather than forcing the wholesale adoption of a "to display data" transformation layer for all templates.

However introduction of any minor conflation is often just the "thin edge of the wedge" of which PHP serves as a historical example:

… and I tried to convince people that you should write the business logic in C and use my templating system, … Nobody agreed with me. Everybody just wanted to write their business logic in the templating system and it was very discouraging to me for years … Eventually I had to sort of give in and say, OK, fine, yes, …

In many ways the same thing has happened with JSX; people liked it because it's easy even though it conflates matters; JSX elements are neither markup nor DOM elements but are simply a representation of a component instance's position in the component tree (for better or worse).

Many attempts at humour have a kernel of truth to them.

In the presence of a behavioural layer it makes sense to not support data conversion inside the structural layer (template) but instead delegate it to the behavioural layer even if this leads to the introduction of "template adaptors" which are solely responsible for the conversion of "behavioural data/types" to "structural (display) data/types" (and vice versa).

Conflation also makes tooling more complicated. Server-side markup is all about "strings". Typically optimal server-side performance is achieved by pre-compiling templates to functions that transform their inputs into markup via extremely optimized string concatenation. Similarly because HTML is text-oriented the majority of DOM properties are string based. As a result the benefits of "typing" templates should be minimal given that the majority of data handled is string based anyway. Along comes JSX introducing an "XML-like syntax to ECMAScript":

  • Unlike HTML, ECMAScript is typed (even if it is dynamically typed)
  • Unlike HTML, XML can be typed, though of course that isn't the style of typing JSX uses.

… all for the sake of replacing

ReactDOM.render(
  React.createElement(
  React.Fragment,
    null,
    React.createElement('div', null),
    React.createElement('div', null)
  ),
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

or

import { createElement as h, Fragment } from 'react'

ReactDOM.render(
  h(
    Fragment,
    null,
    h('div', null),
    h('div', null)
  ),
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

with

ReactDOM.render(
  <>
    <div></div>
    <div></div>
  </>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

… just because it's easier on the eyes?

When TypeScript gained popularity (for better or worse) JSX increased the burden of adoption that either the "enthusiasts" or the tooling ecosystem had to somehow bear (including popular IDEs — though there is an entirely separate conversation lurking there).

So when I saw TodoStore.marko the first thought that went through my mind was:

How is ESLint going to get at this code, what new rules will be required …

where the typical answer is more plugins, loaders, etc. The point is that this type of conflation tends to act as a deterrent to adoption as it doesn't fit into existing workflows and necessitates the creation of additional complex tooling, potentially redirecting effort that could be spent more constructively elsewhere.

Keeping things separate usually means that existing tooling can still be leveraged to maximum effect. For example, before Svelte supported Typescript OOB it was still possible to simply move any component code that Svelte didn't have to be aware of out into a standard module which could then be processed as per usual by the standard tool chain (and micro-tested) — if one was so inclined.


I'm well aware of the Priority of Constituencies:

In case of conflict, consider users over authors over implementors over specifiers over theoretical purity. In other words costs or difficulties to the user should be given more weight than costs to authors; which in turn should be given more weight than costs to implementors; which should be given more weight than costs to authors of the spec itself, which should be given more weight than those proposing changes for theoretical reasons alone.

The way I see it, preferring separation over conflation isn't a case of preferring purity over practicality but choosing simplicity over easiness. While separation in the short term may create authoring inconveniences it should on the whole lead to "parts" that are internally simpler which interact with one another in much simpler ways, reducing system complexity and requiring less complex tooling (leaving more room to invest complexity in other, higher value avenues).

In some ways the current obsession in the web industry with short term DX (and how it's perceived to improve productivity) parallels the development process indulgences prior to the agile manifesto — in particular:

Working software over comprehensive documentation

i.e. the additional complexity cost of some forms of DX needs to be carefully assessed against the potential (opportunity) cost on the UX end (and in terms of long term maintainability) — I believe the myth of the "trickle-down economics" from DX to UX has been well and truly debunked. Without that kind of assessment DX efforts often are more about "press of a button, comfy chair development" rather than improving (long-term) product quality.

Collapse
 
ryansolid profile image
Ryan Carniato

Admittedly the store example is probably where this is the most involved. But I wonder how far it goes beyond this in Marko apps. This is a simple form. And these sort of pieces are probably the extent of state management in most MPAs.

Still as you may have noted I'm big on aggressive co-location. Mostly looking at if we can bridge the gap between the small and the largest apps better. You've probably noticed this trend in refactoring or software scaling. But where we start on the small side is closer to where we end up on the enormous side than all the steps in between. In Marko's case I think this article better captures our goals that the release announcement for the preview, and especially look at the code example at the bottom and the refactoring story.

I like that example a lot because it mirrors the pattern that I constantly see as we scale things. Every framework would have benefitted going from version 1 directly to version 3 if we knew we were going to version 3 ultimately, but introducing boundaries prematurely is not great. Only Marko in that example seamlessly goes through the 3 versions as a natural progression rather than a bunch of rewriting and refactoring.

When we are small we have everything in a single container. It's easier to see it all in front of us. The complexity is not at a point that this is hard to manage. As we grow this structure it becomes harder and we break it apart. Our first instinct is to group by like things. Whether that is as far as separating out state management from view, or simply reorganizing our component code to make it easier to add more features. Eventually though it scales beyond our ability to scale people or process. And we need to slice things the opposite way. At 90 degrees so to speak. More often this involves undoing what we did in the first step. And funny enough it often looks like having more of the initial thing rather than the intermediate step.

Now to be fair we go through this cycle over and over again. As now each of those pieces will do the same alignment over time and eventual split. That cycle I think is what a lot of the work is looking to address.

Now in my opinion this is all about the old game of hot of potato so to speak of who is left holding the state. When we moved to the client side we brough MVC with us, which was terrible because it didn't account for local state. This is what killed Angular1 etc, and lead to the rise of React. React wasn't the first one though to understand this. The whole MVVM thing was poking the holes in MVC. And Angular and Ember were trying to add a bunch of stuff to MVC to account for the fact they were missing something so vital in their model. It was the complication they added to an architecture that didn't want it that sunk those V1 versions. People probably could have kept using the awkward pattern as the fix was worse than the problem in a sense, but we moved on.

Now what we've been seeing recently is the continuation of that. Global State management has been a source of a lot of friction for frontend development. Well it is a bit more complicated than that. It's more that pulling everything local obviously doesn't exactly work in a naive sense. More boundaries have a cost as we scale. But since frameworks control the change mechanism instead of some externalized controller we end up playing a bit of an awkward game as we try to move certain things out of the components back into something shared. And this is basically where I've felt this whole "UI as an Afterthought" started gaining steam again.

But I just see 2 extremes with misalignment. Client frameworks don't really want global state, but are somewhat doomed to be inefficient without it. And classic server side mentality doesn't want local state. They want to be stateless. So who the hell is left holding the bag. That is why GraphQL has been a big deal, and things like ReactQuery in React ecosystem. Frameworks want to treat all state as local, and backend want to treat all things as stateless, the solution it feels is not to try to force either side to do what it doesn't want to. Just to abstract out the adapter. There is complexity here but there is a surprising amount of alignment.

Now, where does business logic live is the resulting question, because I'm basically advocating for smart backends, smart UIs, and dumb stores, (instead of the proposed smart stores and dumb UIs). The answer is mostly on the backend. Instead of viewing our architectures as 2 distinct MV___ that feed into each other I think we need something else. Classic server mechanics have us go from smart(business layer) to dumb(presentation layer) as we serve out our content. Classic app mechanics have us go from dumb (database) to smart (interactive UIs). So to me this about connecting and wrapping up those dumb parts. Ironically I call them dumb but they consist of some of the hardest problems like caching.

So what does this have to do with anything? I think we need to set ourselves up be able to best slice things the other way. And the way we do that is collapse the client/server model. It's not to say the results end up being that different than UI as an Afterthought but in a world where we offload the "global" state logic outside of our whole frontend mindset leaves the frameworks and UI tools the flexibility to optimize for their usage. Where aggressive co-location is a good thing.

TodoMVC example is fundamentally at odds because our model a client side one. Whereas ideally the store would look more like your refactor except we wouldn't need the separate JS file.

Collapse
 
peerreynders profile image
peerreynders

Still as you may have noted I'm big on aggressive co-location.

I understand the motivation of facilitating local reasoning through co-location. But explicit separation makes the need to keep things where they need to be more obvious. And I'm concerned that co-location would lead to coupling that even tooling may have a hard time to pry apart.

I like that example a lot because it mirrors the pattern that I constantly see as we scale things.

Which is why I took your "chart container" example and applied my thinking to arrive at rendezvous (live on glitch).

Now, where does business logic live is the resulting question, because I'm basically advocating for smart backends, smart UIs, and dumb stores, (instead of the proposed smart stores and dumb UIs).

Maybe you understand something differently by the term "Smart UI" - to me "Smart UI" means this:

The Smart UI “Anti-Pattern”

… That sums up the widely accepted Layered Architecture pattern for object applications. But this separation of UI, application, and domain is so often attempted and so seldom accomplished that its negation deserves a discussion in its own right.

Many software projects do take and should continue to take a much less sophisticated design approach that I call the Smart UI. But Smart UI is an alternate, mutually exclusive fork in the road, incompatible with the approach of domain-driven design. If that road is taken, most of what is in this book is not applicable. My interest is in the situations where the Smart UI does not apply, which is why I call it, with tongue in cheek, an “anti-pattern.” Discussing it here provides a useful contrast and will help clarify the circumstances that justify the more difficult path taken in the rest of the book.

❊ ❊ ❊

A project needs to deliver simple functionality, dominated by data entry and display, with few business rules. Staff is not composed of advanced object modelers.

If an unsophisticated team with a simple project decides to try a Model-Driven Design with Layered Architecture, it will face a difficult learning curve. Team members will have to master complex new technologies and stumble through the process of learning object modeling (which is challenging, even with the help of this book!). The overhead of managing infrastructure and layers makes very simple tasks take longer. Simple projects come with short time lines and modest expectations. Long before the team completes the assigned task, much less demonstrates the exciting possibilities of its approach, the project will have been canceled.

Even if the team is given more time, the team members are likely to fail to master the techniques without expert help. And in the end, if they do surmount these challenges, they will have produced a simple system. Rich capabilities were never requested.

A more experienced team would not face the same trade-offs. Seasoned developers could flatten the learning curve and compress the time needed to manage the layers. Domain-driven design pays off best for ambitious projects, and it does require strong skills. Not all projects are ambitious. Not all project teams can muster those skills.

Therefore, when circumstances warrant:

Put all the business logic into the user interface. Chop the application into small functions and implement them as separate user interfaces, embedding the business rules into them. Use a relational database as a shared repository of the data. Use the most automated UI building and visual programming tools available.

Heresy! The gospel (as advocated everywhere, including elsewhere in this book) is that domain and UI should be separate. In fact, it is difficult to apply any of the methods discussed later in this book without that separation, and so this Smart UI can be considered an “anti-pattern” in the context of domain-driven design. Yet it is a legitimate pattern in some other contexts. In truth, there are advantages to the Smart UI, and there are situations where it works best—which partially accounts for why it is so common. Considering it here helps us understand why we need to separate application from domain and, importantly, when we might not want to.

Advantages

  • Productivity is high and immediate for simple applications.
  • Less capable developers can work this way with little training.
  • Even deficiencies in requirements analysis can be overcome by releasing a prototype to users and then quickly changing the product to fit their requests.
  • Applications are decoupled from each other, so that delivery schedules of small modules can be planned relatively accurately.
  • Expanding the system with additional, simple behavior can be easy.
  • Relational databases work well and provide integration at the data level.
  • 4GL tools work well.
  • When applications are handed off, maintenance programmers will be able to quickly redo portions they can’t figure out, because the effects of the changes should be localized to each particular UI.

Disadvantages

  • Integration of applications is difficult except through the database.
  • There is no reuse of behavior and no abstraction of the business problem. Business rules have to be duplicated in each operation to which they apply.
  • Rapid prototyping and iteration reach a natural limit because the lack of abstraction limits refactoring options.
  • Complexity buries you quickly, so the growth path is strictly toward additional simple applications. There is no graceful path to richer behavior.

If this pattern is applied consciously, a team can avoid taking on a great deal of overhead required by other approaches. It is a common mistake to undertake a sophisticated design approach that the team isn’t committed to carrying all the way through. Another common, costly mistake is to build a complex infrastructure and use industrial strength tools for a project that doesn’t need them.

Most flexible languages (such as Java) are overkill for these applications and will cost dearly. A 4GL-style tool is the way to go.

Remember, one of the consequences of this pattern is that you can’t migrate to another design approach except by replacing entire applications. Just using a general-purpose language such as Java won’t really put you in a position to later abandon the Smart UI, so if you’ve chosen that path, you should choose development tools geared to it. Don’t bother hedging your bet. Just using a flexible language doesn’t create a flexible system, but it may well produce an expensive one.

By the same token, a team committed to a Model-Driven Design needs to design that way from the outset. Of course, even experienced project teams with big ambitions have to start with simple functionality and work their way up through successive iterations. But those first tentative steps will be Model-Driven with an isolated domain layer, or the project will most likely be stuck with a Smart UI.

❊ ❊ ❊

The Smart UI is discussed only to clarify why and when a pattern such as Layered Architecture is needed in order to isolate a domain layer.
There are other solutions in between Smart UI and Layered Architecture. For example, Fowler (2003) describes the Transaction Script, which separates UI from application but does not provide for an object model. The bottom line is this: If the architecture isolates the domain-related code in a way that allows a cohesive domain design loosely coupled to the rest of the system, then that architecture can probably support domain-driven design.

Other development styles have their place, but you must accept varying limits on complexity and flexibility. Failing to decouple the domain design can really be disastrous in certain settings. If you have a complex application and are committing to Model-Driven Design, bite the bullet, get the necessary experts, and avoid the Smart UI.

Evans, Eric. "Domain Driven Design: Tackling Complexity in the Heart of Software"; Four: Isolating the Domain, pp.76-79, 2003.


Perhaps now you understand my apprehension with "Smart UIs" — the implication here is that the approach does not scale.

Where aggressive co-location is a good thing.

The thing is I think with rendezvous having the bare Solid reactive system wrap the core application and binding it directly into the DOM on mount would be useful.

FYI: Congratulations on having Rich Harris to mention Marko in his Transitional Apps talk!
(Then again you're always talking about Svelte so it's only fair.)

Collapse
 
peerreynders profile image
peerreynders

Thank You for your response!

I've revisited

from 2020-Nov-23 as that seems to be the point where you got the most public feedback. Some questions:

<h1>${input.headerText}</h1>
<div/el />
<mount=() => {
  const chart = new window.Chart(el, input.data);
  return () => chart.release();
}/>
Enter fullscreen mode Exit fullscreen mode

Given that Chart uses a native DOM reference is it safe to assume that it is purely a client-side widget?

Consequently what actual HTML is sent from the server to the client? Something like:

<h1>Chart Header</h1>
<div></div>
Enter fullscreen mode Exit fullscreen mode

Side note: Good thing eBay doesn't plan on adopting Tailwind CSS.

Thread Thread
 
ryansolid profile image
Ryan Carniato

Yeah <mount> was an early version of our <lifecycle> tag. So this is all client side. So the server HTML would be what you wrote and when hydrated in the browser the chart would be created in this example. The refactor story equally applies to nested state, but nested side effects(browser only) I felt were the most obvious for this example.