Hacker News new | ask | show | jobs
by jdmichal 3280 days ago
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.

3 comments

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.