These are the largest 2 regrets I've had in making architecture over the years.
Regret: making "magic" architecture
For instance, opting into being part of the architecture by implementing an interface or abstract class.
// the infrastructure runs some reflection code on startup// to find all IHandleCommands classes and make sure// messages are delivered to Handle methods inside thempublicclassSomeHandler:IHandleCommands{publicvoidHandle(SomeCommandcommand){...}}
The reflection code is not shown so your eyes don't glaze over.
This is nice and clever, because you don't have to write wiring code. But especially for developers it can be really hard to accept "Don't worry about how it works. Just put this interface on it and it will work." Magic. It is great as long as you don't run into any special cases which break it.
This is the same reason I avoid attributes (aka annotations) where possible. I'll use them for some compiler optimizations or literally for extra information like [Obsolete], but I avoid using them to control logic. Logic around these is not directly called from the code it adorns, so it's hard to track down when things go wrong. And it's not obvious how things work. Recently I looked around for a while on ASP.NET Core's source code for the exact code that is run by the [Authorize] attribute. I never found it. I found code that I suspect is called, but I can't prove it because I was not able to trace a direct call chain into that code.
Regret: making opinionated abstractions required
Changes to architecture are very costly since arch is typically used by a lot of different feature code. Any required architectural abstractions should be as course-grained as possible.
I've made the mistake of thinking that I'm going to make it super easy to just plug in new feature code and my arch framework will handle all the infrastructure details. Usually this involves requiring feature code to take on my arch abstractions. That works fine until next month when the customer requests a different kind of feature like exporting to CSV. Where the feature needs to handle some of the infrastructure itself, like writing directly to the response stream. Otherwise it can run out of memory reading a large data set. So I have to backup and rethink my whole architectural abstraction. And probably change every place where it was already used. And that's just the beginning of the fights with the required abstraction. You'll have to keep going back and refactoring to add handling for all the various cases you run into.
Instead, it's best to keep the architecture as course-grained and simple as possible. In most web frameworks you are given the Request and Response objects (although unfortunately most of the time they are just DTOs with getters and setters), sometimes packaged together in a Context object. This is a good example of a course-grained interface. Let feature code handle what they want at nearly this level. Then if there are common cases which use the same steps (e.g. a Load-Edit-Save workflow), make a helper abstraction to simplify that. Then feature code can choose to opt into the helper if it fits what they are doing or handle everything themselves. That way it should be really rare to need to change the architecture code in a way that breaks feature code. But you still have the opportunity to write very little code for really common features by using helpers.
I absolutely agree with "magic". Thats the main reason why I'm not using Cycle.js instead of Angular for my projects. Everything is explicit and traceable.
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
These are the largest 2 regrets I've had in making architecture over the years.
Regret: making "magic" architecture
For instance, opting into being part of the architecture by implementing an interface or abstract class.
This is nice and clever, because you don't have to write wiring code. But especially for developers it can be really hard to accept "Don't worry about how it works. Just put this interface on it and it will work." Magic. It is great as long as you don't run into any special cases which break it.
This is the same reason I avoid attributes (aka annotations) where possible. I'll use them for some compiler optimizations or literally for extra information like
[Obsolete]
, but I avoid using them to control logic. Logic around these is not directly called from the code it adorns, so it's hard to track down when things go wrong. And it's not obvious how things work. Recently I looked around for a while on ASP.NET Core's source code for the exact code that is run by the[Authorize]
attribute. I never found it. I found code that I suspect is called, but I can't prove it because I was not able to trace a direct call chain into that code.Regret: making opinionated abstractions required
Changes to architecture are very costly since arch is typically used by a lot of different feature code. Any required architectural abstractions should be as course-grained as possible.
I've made the mistake of thinking that I'm going to make it super easy to just plug in new feature code and my arch framework will handle all the infrastructure details. Usually this involves requiring feature code to take on my arch abstractions. That works fine until next month when the customer requests a different kind of feature like exporting to CSV. Where the feature needs to handle some of the infrastructure itself, like writing directly to the response stream. Otherwise it can run out of memory reading a large data set. So I have to backup and rethink my whole architectural abstraction. And probably change every place where it was already used. And that's just the beginning of the fights with the required abstraction. You'll have to keep going back and refactoring to add handling for all the various cases you run into.
Instead, it's best to keep the architecture as course-grained and simple as possible. In most web frameworks you are given the Request and Response objects (although unfortunately most of the time they are just DTOs with getters and setters), sometimes packaged together in a Context object. This is a good example of a course-grained interface. Let feature code handle what they want at nearly this level. Then if there are common cases which use the same steps (e.g. a Load-Edit-Save workflow), make a helper abstraction to simplify that. Then feature code can choose to opt into the helper if it fits what they are doing or handle everything themselves. That way it should be really rare to need to change the architecture code in a way that breaks feature code. But you still have the opportunity to write very little code for really common features by using helpers.
HTH
I absolutely agree with "magic". Thats the main reason why I'm not using Cycle.js instead of Angular for my projects. Everything is explicit and traceable.