Hacker News new | ask | show | jobs
by saltedmd5 3280 days ago
Oh look, more kludgy over-designed enterprise gumf.

When are people going to learn that if you just start with the entry point with granular components, letting each define the interface for its dependencies, you get a much nicer, looser, more flexible structure than these enterprise "patterns" that ultimately all just turn into a big ball of mud?

Stop pretending you can design codebases.

6 comments

People forgot at some point that design patterns are for solving problems. The key being that you should only be solving problems you actually have, and stop making up imaginary problems in your head before you even start coding.

My code is typically some dumb data objects, static functions to apply rules to that data, and a bunch of interfaces for abstracting external dependencies. The code that ties everything together lives at the entry points, and is hopefully written like one is reading a requirement.

To take the course registration example, the "register" entry point might look something like:

    register(studentID, courseID):
        // Load student from data store interface.
        Student student = this.datastore.getStudent(studentID)

        // Business rules for whether a student can still register for classes.
        // This would check things like course overload and duplicate registrations.
        if (!Students.canRegister(student, courseID)):
            return CannotRegisterError

        // Record registration.
        this.datastore.addRegistration(studentID, courseID)
Advantages:

* Data objects are dumb. No need to even test them.

* Rules are all static functions, so you can call them anywhere, anytime, including for testing. Extremely flexible and allows easy remixing of rules for new requirements.

* All external dependencies live behind an interface, so they can all be mocked away for testing.

I know this is written as OO, but it is very functional in style. Keeping IO as far away from entities and business rules goes a very long way in easing maintenance. However, sometimes you need to do IO in your business rules, which necessitates interfaces for mocking.

IMO, this general idea is very powerful for most code. I believe algebraic effect systems will popularize it further by making it a natural pattern. While OO should be written in this way, we are currently in this weird period of programming history where we don't talk about practices and design as seriously as we should.

But, I'm glad you mentioned this approach. It is not hard to understand, it is elegant, and it is as simple as can be. Just requires a tiny amount of glue and forethought.

What kind of design is this? Is there a name for it? I'd like to read more (both out of interest and being skeptical of how this scales)
I should come up with some catchy name and a blog post for it. It's basically my own custom mix of procedural (entry points), functional (static validations), and design by contract (external abstractions) in the noun worlds of Java and C#. I arrived at it after seeing too many instances of:

* Architecture / design pattern lasagna, where breaking through abstraction layers felt like Inception. And adding a data field meant going through all 5 layers of code...

* Inheritance for functionality instead of typing.

* Also, breaking Liskov substitution willy nilly then wondering why things are hard to reason about. (Because your code becomes entirely reliant on the types actually present at runtime, since you can't rely on semantic equivalence.)

* Tacking methods into data objects because that's where the data is. See other comments on this post advocating a "register" function on the Student object, or should it be on the Course class... Answer is neither. Encapsulation is about maintaining invariants. It's not about putting all functions related to students in the Student class.

* Zero unit tests because external dependencies weren't isolated and all the logic is hard coded into exact use cases, which eventually end in hard coded external dependencies...

I've seen that particular type of method/function be called a use case, interactor, or service object.
What are the disadvantages?
A great question. Honestly it's a very natural fit for me, so I'm probably not very well qualified to answer.

There can be a lot of code repetition. To me, that's not a bad thing. I've seen people to themselves in knots trying to DRY some common code, just to later have to undo it all because a requirement changed for only one of the code paths. And, as I said, I like my procedures to read like use cases.

I'll see if I can think of anything else...

You need to write more structure than if you had simply just put all the code in the web handler or whatever. You also need to be able to abstract all DB/API calls if you want to test easily, as mocking things you don't own just leads to pain later on.
This plus checking codebase in regular intervals for: * KISS? * Bundled components? * Dependency injection useful? * Duplicated components / functions? * Too much/less abstract classes? Interfaces useful?

In my case that looks like: Write code for 6h, review and refactor code 2h. Result is that less code is produced (due to refactoring/removing/etc.) and codebase keeps being simple. On the other hand it's easier to write tests.

No need to use complex enterprise patterns. Most of time simple facades and delegators are enough. Consider writing small simple components instead of using heavy patterns with a lot of boilerplate code.

It's just human nature. People come up with geocentric ideas for everything. It's certainly not limited to software designers or even particular modes of software design. Ever talk to Semantic Web people? Or, ontologists? People tend to naturally assume that there is one set of "Enterprise Entities". Heck, it even took physicists a long time to come up with general relativity. And, they only did it after their absolute models broke down.

So, yes, each granular component should have its own perspective of "the Enterprise". But, contexts can be very different. For a simple example, a "car" may have completely different interface depending on if its user is a "car designer", "car assembler", "car salesperson", "car driver", etc. So, someone trying to define a universal interface for a car will make themselves and their teams crazy. Worse, the idea of "a car" will change through time, so even perfect interfaces will have to change.

This isn't limited to object oriented design, by the way. Functional programming paradigms have the same issues.

I always advocate building up a walking skeleton of the system because only then does the architecture begin to emerge. I want to see how the data moves through the system to handle the core functionality; the actual goal of the system. I've sat in too long meetings where someone with their architect hat on spends hours going over this wonderful architecture for Core System Rewrite(tm) and not one time ever mentions the actual CORE functionality the system is trying to model. It's all gibberish about layers and entities and buses, etc. We're building a tire inflater, you have not one time mentioned how it actually inflates tires!
I find your extreme as problematic as the article's.

What you describe, at least in my experience, more often than not leads to a big ball of mud as easily as the methodology in the article.

Software can be designed. It's just there's little appetite for an actual rigorous design process in this industry. Instead there is a lot of bandwagoning and looking for one size fits all solutions (like the article's), mixed thoroughly with people who haven't ever really grown beyond thinking of textbook CS as the solution to every problem.

Which is essentially what this is.
My objection to Uncle Bob is that it seems really heavy on process, with lots of indirection via adapters, abstract base classes, etc.

I get that they're useful for taming a certain amount and kind of complexity, but it's not clear to me that it's always going to be apparent at the beginning that it's going to need taming in that specific way. I've found that starting with the concrete cases, I only sometimes have to go up a level of abstraction and indirection. Conversely, I've built the wrong abstraction many times by starting too high.

I don't read or write Java so I'm sure I'm missing a lot of context. That's what I'm looking for. Uncle Bob's an eloquent speaker and his talks make a lot of sense, but I have trouble reconciling that with the code samples I see.