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.
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.
* 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...
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...
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.
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.
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.
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.
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.