NOTE: this article series is superceded by this article
When a codebase grows larger, splitting it into manageable chunks becomes important to keep up momentum. You want bite-sized pieces of app that you can edit and test independently, without impacting the rest of your code.
Some frameworks are built entirely around this principle. In React and Vue you code the various "parts" of your app in the form of "components". Hyperapp does not have any such constructs to guide you – how you structure your app is entirely up to you.
That can be bewildering, but it is also one of the strengths of such a minimalistic framework. General problems are solved using general programming techniques – not framework specific helpers. Experienced programmers can rely on what they already know, while the novice can be confident what they learn will be useful in future projects and domains.
Nevertheless, it can be bewildering. In this series of articles, I'll present several plain javascript techniques you can use to structure your Hyperapp apps in a modular way.
I'll kick off the series with a closer look at modules and modularity. It will be fairly high-level and not Hyperapp-specific, but it forms the foundation for the concrete patterns we'll look at in future installments.
Modules
Simply put, a module is something that you can easily "plug in" to add some new feature. Think of the International Space Station. When it needs more room to house more astronauts, a living-space module is built on earth and launched in to orbit. In space, all they need to do is plug it in to an available port, and voilá – the space station can now sustain more astronauts. Over time the ISS has handled wear and changing use cases simply by adding and removing modules.
In programming, what we typically mean by a module is a code-file which exports some values, functions, classes et c. Other modules can import these things to gain new powers.
Much of our discussion about modules and modularity applies to classes and object-oriented programming as well. But since Hyperapp is rooted in the functional programming paradigm, we'll stick to that.
To be able to use modules in javascript, add the type "module" to the script tag which starts your app. Then it can import whatever it needs from other modules.
<html>
<head>
<script type="module">
import {h, text, app} from 'https://unpkg.com/hyperapp'
import {foo, bar} from './foo.js'
/*
...
do stuff using, h, text, app
as well as foo and bar
...
*/
</script>
...
For a complete reference on the syntax of exporting/importing from modules, see this MDN guide
Complexity
While space-engineers use modules to avoid the dangers and difficulties of monkey-patching live systems in space, programmers use modules to manage complexity.
Once your app gets big enough, looking at your own code can start to feel like looking at an unintelligible tangle of wires. When even the people who wrote it can't understand it, further development is effectively halted. Modules can help avert this fate by breaking the complexity up into smaller chunks, where each module on it's own is manageable.
//This is foo.js
// some reasonably complex stuff:
const zip = ...
const zap = ...
const zorp = ...
//...hidden behind this simpler interface:
const foo = ... // uses zip, zap & zorp
const bar = ... // uses zip, zap & zorp
export {foo, bar}
Every module carries its own scope, meaning you can freely assign variables within the module without fear of naming conflicts in other modules. This is an essential feature of modules: they don't know anything about each other besides what they export.
The exports constitute an interface for other modules. As long as you fulfill this contract, it doesn't matter how you do it. Like an ISS module, as long as the docking-port has the right size and shape, with the right connectors in the right places, you can build the rest of the module however you like.
The Crux
Just hiding some complicated code behind an interface is not necessarily enough to actually manage complexity. Take this example:
const zip = (zipthing, index) => ...
const zap = (zapthing, value) => ...
const zorp = (zorpthing, options) => ...
const foo = (app) => {
let zapthing = zip(app.theZipper, app.current)
let zapResult = zap(zapthing, app.settings.zapopts.value)
return zorp(app.theZipper.zorp, {
...app.zorpopts,
zap: zapResult,
})
}
export {foo}
Notice how sensitive it is to the contents of app
! The idea behind a module like this, was probably to move the logic out of the way to simplify logic elsewhere in the app. But any change to app
risks breaking the module, so nothing was actually untangled. Instead, parts of the tangle were just hidden away, which only makes matters worse.
Any assumptions about the rest of your app that a module relies on, is technically a part of the interface. If the interface is broad, diffuse and sensitive then you aren't really creating the separation between complexities that would help you deal with them.
This is the trick to using modularity to its full advantage: keeping the interfaces small, simple and robust. Ideally they should be similar between modules as well. This means implementing your modules with as few assumptions as possible on the external.
Keeping interfaces small and simple is commonly referred to as "loose coupling", and making as few assumptions as possible is known as "the principle of least knowledge" or the "Law of Demeter"
Conclusion, Part 1
In summary there is more to modularity than just export
and import
. Knowing when it is time to break something out into a module, what to put in that module, and how to design the interface is a subtle art. Like anything, experience is the best teacher.
In the following installments we'll explore some specific cases and concrete patterns with discussions of their pros and cons.
Top comments (0)