The concept is so appealing, “Hey I’ll just fire this message off and whoever needs to know about it will do what they need”.
It’s great to have that ability to focus on just the component or module you’re working on, but the lack of visibility on exactly what is about to happen, and what has happened, has caused many late nights of debugging and frustration.
It’s just how you described “there’s absolutely no way to tell the code that was going to get injected”
I’m guilty of contributing to this problem as well. Once you become familiar with the event system you’re working in, windows, DOM, whatever, you can make some pretty good assumptions about how other components are implemented. When you’re faced with a bug in code you don’t have access to, or a behaviour you don’t want, often the solution you’re left with is asking “okay, so what combination of events and timing do I need to force this component into the state I need?”
For example I remember doing this with some third party grid controls. We really really wanted the TAB key to insert a new row at the end. We had to orchestrate exactly the right mix of events and method calls in the right order, with a few BeginInvokes to get the job done.
I wish I had a solution to all of this and other frustrations. It’s all trade offs and a balancing act.
This clearly makes it pretty awkward to handle. Yet considering that the compiler manages to figure out where code needs to be injected, one can ask: why does the programmer not get to see this information?
This is not a fundamentally unsolvable problem. It is a tools problem. It's easy to imagine a programming environment where you can tell immediately which code gets injected where. Even in a dynamic system you should be able to see, on examining a function, what its current contents including injected code are.
Some programming language features require proper tools support to be useful. This requires languages that want to introduce such features to be opinionated on the matter of what tools you can use.
When developing a Spring project, you indeed start to quickly appreciate the introspection feature that advanced IDEs like IntelliJ IDEA provide. Inspecting the application container at runtime is also possible, but it's a clear sign that your app got waay too complicated and has to be refactored.
Good tooling takes the surprise out of nonlocal control flow, and can project it into local control flow on demand.
To take it back to skrebbel's complaint, "methods defined far, far away", "absolutely no way to tell", "very surprising", "no indication whatsoever" -- all things that good tooling fixes because it can show you what gets injected where and why, when you need it (and show you a minimally intrusive reminder when you don't).
Game modding also demonstrates the big strengths and weaknesses of AOP. New versions of games tend to break some or all mods until the mods are updated, even if the game developer is trying to support the modding scene and avoid breaking things. When literally every function in your code base is a potential extension point, it becomes impossible to change anything without breaking something else, and you can get insane impossible to debug errors.
While it can be misused (by making unclear what aspects are being applied, as you mention) it can be very useful and avoid a lot of boilerplate code and repeated logic.
On a project where "logging" was budgeted as a short, separate task that could be delegated to a junior engineer at the end, I used Spring's AOP to inject performance and usage logs across entire chunks of the program. It worked great and took way less time than scheduled.
I was convinced there'd be other uses for AOP, and kept an eye out for other opportunities for months. Given that I completely forgot this was a thing until now, I think you can guess how often that happened.
Other than logging and profiling I've used it in ejb3 app calling old plsql business logic - we had problems with some plsql code handling exceptions and transactions in non-standard way (basically - comiting or rolling back in pl/sql code) which messed up our ejb3 container-managed transactions. We had a template for pl/sql code that should be followed - every function should start with a savepoint, never commit, and only rollback to that savepoint in case of error. But there was a lot of plsql code and some of it didn't followed that rule.
So I added aspect in java that wrapped around any plsql call, created a savepoint before it, and checked after the call if it's still there - if not it would a special exception. That allowed us to quickly find all the places that weren't handling transactions properly in PL/SQL and fix them.
We also used AOP for integration testing with our java business logic and plsql - instead of preparing and using up data during tests we simply wrapped transactions around testing methods and rolled back everything afterwards.
I don't think AOP is bad, even if it's truly COME FROM with a different branding.
There was the guy who used AOP to inject logging and perf, and set it to "instrument every method".
_Every method_.
It was Ok in test, but when the production load rose to the daily peak, the volume of logging data was in itself sufficient to saturate a network and requests got dropped. Including health checks, which caused machines to be taken out of the load balancer, increasing load on the survivors. Autoscaling didn't help, new machines came up with the same issue.
The failure cascade brought down the entire service.
True story.
But as you said, devs find all kinds of mind melting creative ways of using it.
For those not in the know, AOP is basically the Intercal COME FROM statement (itself a pun on goto, ie it's a jump but in the opposite direction) (think that through a bit). Yes, that's a totally nuts idea, but it turns out that with a bit of effort you can formulate it in such a way that you can do conference talks about it and people nod and think "o wow nice I gotta tell the team".
More concretely, AOP is usually a bit of compiler infrastructure that lets you inject code into other people's functions/methods etc and make arbitrary changes. It's nice for eg inserting logging code at the beginning of each method, with the method name and argument. You'd be able to eg define a query somewhere and say "for all methods with annotation X, inject code Y at the beginning of the method".
But naturally, teams that adopted it always had this one team member who thought they knew better and started using it for injecting all kind of behavior-altering code in methods defined far, far away. When reading such a method's source, there was absolutely no way to tell that code was going to get injected. If the AOP crowd was well behaved there'd be an annotation/attribute/etc that would trigger the behavior, but that's not a requirement. I've seen people inject code that sanitized data, which was very surprising if said function didn't actually take user data and suddenly while debugging a string's content had changed and there was no indication whatsoever what had caused it.