Hacker News new | ask | show | jobs
by spoondan 2386 days ago
About 15 years ago, I built a system like this for a web application. I defined a series of core extension points such as the request router and authentication and authorization providers. Modules provided extensions that connected to the core extension points, as well as define their own extension points that other modules could connect to. Dependency injection was used throughout so that implementations could be easily swapped (mostly used for the tests). It took quite a bit of time to develop this framework, but, as a result, everything was modularized, contained, and composed.

It was a complete nightmare to reason about and work with, even for me, and especially for newcomers. A natural way of trying to understand a system is to look at its entry point. With a system like this, it looks like a skeleton:

    let di := DependencyContainer.make()
    let moduleSystem := ModuleSystem.with_container(di)

    di.register_singleton[IModuleSystem](moduleSystem)

    moduleSystem.find_and_register_modules()
Where do we go from here? The next logical step is into `find_and_register_modules` or into the documentation for the module system (just kidding: I was a "senior engineer," I didn't write docs).

We are left with a lot of questions. How does anything happen? It's somewhere inside the modules, but which one? Does the order that modules are loaded matter? The answer is maybe. Clearly, there are places where order of operations matter. For example, the main navigation should be ordered. How do we accomplish that? (The answer for my system was to have the interface for navigation extensions specify there was a `readonly weight: Float64` attribute. But then to actually understand why things are in the order they're in, and to get things ordered correctly, you need to look up the values for other navigation items. It's secret coupling.)

More recently, I saw a team at my previous company build a system like this. There were dozens of interfaces and implementation classes and code for composing all of this, and the end result was you couldn't just go somewhere and see what was happening. What we really want to see is:

    match maybe_user
        Some(&user) => nav.add(UserProfileNavItem.for_user(user))
        None => nav.add("Login", "/login")

    nav.add("Browse", "/browse")
    nav.add("Search", "/search")
    nav.add("Help", "/help")
Do you have to make a change here whenever something gets added? Yes. You do. But the idea that your program needs infinite flexibility in all areas is simply wrong. And, in fact, everything ends up secretly coupled and inscrutable.

For this reason, my advice has long been to limit extensibility to specific, narrow use cases (for example, filters in an image editing program). Do not build entire systems around extensibility. You are not building an abstract system, so don't waste your time with unnecessary and obscuring abstractions.

1 comments

Author here. I read your comment and just think I'm not a very good writer. Layers are absolutely not about adding extension points, and layers have nothing to do with modules with fixed interfaces. Often when I want to extend behavior in my layered programs, I just modify the line in place.

I mentioned a couple of rules of thumb for when I create new layers here: https://news.ycombinator.com/item?id=21766557#21767499

Here's the entry point for my current project which uses layers: http://akkartik.github.io/mu/html/000organization.cc.html

You can find a list of layers here: https://akkartik.github.io/mu1 (URL is slightly different; it's a previous prototype. But should suffice for this thread.)