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.