Originally, AOP was about separating cross-cutting concerns by centralizing them in one place. It used weaving to separate infrastructure code, and implicitness was inherent in that approach. But the books I read back then said this led to 'ghost code.' It inevitably introduced unpredictability because the behavior wasn't visible in the code. And from a programmer's perspective, that becomes a problem when things break.
On top of that, while the cross-cutting concerns are centralized, they still end up being tied to the framework's syntax, like Spring AOP's Join Point syntax, so they become dependent on the framework itself.
That's why DDD became popular as another way to address OOP's limitations. DDD keeps the business logic pure and framework-agnostic, and that's where things like POJO emerged. At least that's what I read in books, just different approaches to the same problem.
AOP was first presented at ECOOP in 1997, and DDD is usually associated with Evans' book. Both are ways of handling OOP's complexity, but the article here doesn't seem to talk about problems with cross-cutting concerns, which is the core of AOP at all. And this is something AI doesn't handle well. AI makes the most mistakes with implicit knowledge.
Or maybe I'm wrong because I've been studying programming history through older books and have outdated knowledge. Maybe AOP has evolved since then.
What makes it difficult to talk about specific 'oriented' paradigms in programming is that as history moves on and certain problems get solved, if you bring up an older version, you'll get pushback from people who really know their stuff.
'That's a problem that was solved 5 to 10 years ago.' 'That issue has evolved and been covered in other books.'
So it's hard to talk about any 'oriented' approach because you have to specify which era's version you're referring to. For example, even with OOP, which everyone knows, there's a big difference between Smalltalk, C++, and the modern emphasis on composition over inheritance. Someone might say, 'Modern OOP is centered around composition and value objects — you're behind the times.'
AOP might have also evolved and introduced different solutions since then.
So while the perspective on a given 'oriented' paradigm does shift over time, it's really hard to have a conversation about programming because it all depends on which era the programmer is from and how far their knowledge goes.
That's why lately I've been thinking about problems and the approaches used to solve them, rather than focusing on the 'oriented' labels. I wish someone would write a history book about these paradigms. They'd get a lot of criticism, but for programmers like me, it would be much easier to understand.
Sometimes I think it's about time someone wrote a history book on programming
But the post entirely lacks the motivation for the AspectJ/AOP join point model: to have principled time/place for concern integration that was statically determined, type-safe, understandable to users -- and suitable for integration.
> I've also always hated the specific mechanism that AOP chose to implement it with – something called the "join point model" which basically amounts to runtime pattern matching on a program's call stack and running some code every time a pattern matches.
AspectJ's join point model is only dynamic where Java as a reference-based language could not support the static analysis. At compile-time, the "static shadow" of the pointcuts was calculated and implemented where staticly determinable; only the dynamic residue is deferred to runtime (e.g., is the caller to this method of type X?).
Many of AspectJ's join points and type extensions - method call or execution, exception throwing, field access - largely have been adopted in many languages (python context managers, swift getter/setters/extensions), and the residue are a bit hard to use.
But nothing really matches the power of pointcuts: to combine these predicates and the type-safe state-management - e.g., "when throwing an exception after a transaction, capture the span id along with the user id into a log message"
AOP was great for the 7% of code that it was intended for, but was largely displaced as too complicated. Now with LLM's it's a decent hypothesis that with proper training LLM's could actually handle the more complicated but ultimately cleaner programming model - cleaner because it avoids the scattering of similar code which makes it hard to change.
The key insight is that dominant concerns establish the basic structure of the application, leaving some important but residual aspects to fit themselves to the structure. That means the dominant structure must be suitable for the AOP integration (i.e., support the right pointcuts and type extensions); solve that and you've solved most integration issues. It's especially helpful for feature architectures, where you offer code in open-source to gain API adoption, paid for by closed-source library integrations with additional features.
And one giant problem with it is the reliance on global variables. An AOP wrapper has to modify something, and it typically does not have enough access to enough context to do it.
So it has to rely on ambient data that has to be saved in a global variable. And this is _bad_. It makes data flow opaque and impossible to follow.
And then there are issues with debugging. Where do you put a breakpoint? What happens if you try to step into an instrumented method?
PS: yes, a global variable can technically be a thread-local variable. It doesn't matter, it's still a non-local ambient state.
I'm unclear on AOP in general, esp as proposed here. That's a bigger leap...
It's possible that what you have here is an idea for what I consider to be eventually very likely, which is a computer languages still built for humans to be able to understand and debug it, but more primarily for LLMs to write it. Write a language designed to be an aspect-oriented language from the beginning. Equip it with the ability to run something like a language server and point it at a system and get all the "aspects" running and you might have something.
But I'm skeptical of bodging this on to an existing language.
One of the reasons I suggest making it a new language is that AOP was hampered by being able to use only what languages already supported. The need for a "weaver" is a smell anyhow. Something where the aspect code is the native representation and the "weaving" simply dissolves into the compilation process would not only make the whole thing more appealing in general, I think it would also allow for some things that even code generation might have found a challenge, like aspects that can maintain guarantees because the whole process is more aspect-aware and not broken by the embedded "payload" code written by a human.
Also one would say monkey patching on Python and Ruby frameworks is another way to do AOP.
Looking at transactions: The 99% solution is trivial: Every service call is a transaction. AOP can save me a few lines for every method and things look much cleaner.
But then comes the huge excel upload that is performance critical. Batch more service calls to fetch additional information in the background, commit every so-and-so records in a loop depending on the data size, do a custom roll-back if things fail.
And suddenly this whole separation of concerns breaks down and creates a huge mess.
The simple case saves a few minutes, the complicated case causes weeks of depression. Not a good tradeoff from my experience.
An LLM adding to the confusion by only sometimes getting things right and explaining that the separate documents are always valid, except when they are not, well, sounds like a fun experience.
Oh HELL NO.
The LAST thing you want is a non-deterministic process monkey patching your code.
And me, like others have tried structuring our code like this, and failed, assuming the fault lay not with the idea itself but our skill level. Of course, by now it's kind of common knowledge that inheritance isn't a thing that can and should be used to solve every kind of problem.
Same thing with AOP - it might be sometimes nice, but on the whole, elevating this to the language level seems to be counterproductive.
LLMs basically solve the classic Frame problem that prevented general problem solvers to be able to reason logically about the real world; however on their own they are utterly unpredictable and unreliable.
However if the database of weights is merely used as a heuristic to guide the logical reasoning engine to promising regions of the problem space, and the program itself is written to specification directly by an inference engine, the result would be classic software not affected by hallucinations.
The LLM could even help debugging the specifications by pointing out unclear or contradicting requirements, improving the process without compromising the integrity of the result.
IIRC—and it may have changed in the several years since I made use of it for this, but I don’t see why it would—the standard way to do AOP in Ruby is leveraging modules-as-mixins which are a core language feature, monkey patching is unnecessary (but since classes in Ruby are open, modules-as-mixins can be used to monkey patch classes provided by someone else just as easily as being done at class definition time.)
Aspect-oriented programming has annoying implementation in languages like Java which don’t natively support the right abstractions and where you are fighting the language to do it.
Its kind of unfortunate that those are also the languages also which do the most to shape people’s understanding of AOP.
> The LAST thing you want is a non-deterministic process monkey patching your code.
I'm not poking fun of you, but the irony here is that code-as-written is mostly a "suggestion" to modern compilers and JIT interpreters and the actual instructions emitted often look nothing like your ver-batim code.Consider all the things a programmer needs to keep track of while writing code:
Correctness: making sure the program does the right thing, satisfies all requirements, maintains all important invariants, performs the business logic in the desired way, etc.
Efficiency: the program uses a minimal amount of time, memory, and resources. This includes not holding onto memory for longer that it is needed, and not releasing the used memory in a fragmented state if possible. Some programs have "real time constraints" where certain efficiency goals, like returning a response to the user within 0.2 seconds, are considered part of their correctness.
Debuggability: when things go wrong with the program, it shouldn't be too hard to find out what they are and how to fix them.
Maintainability. The program should be documented and readable. It should be written in an internally consistent style that should also match the style of any external codebase it is a part of.
Testability. The program should be written in a way that unit and integration tests are both helpful and not too difficult to create.
Logging. The program should produce a usually non-comprehensive trace of its execution path and variable state that facilitates its debugging, allows its efficiency to be monitored, and allows for the collection of statistics regarding its behavior.
Security. The program should let the user take only authorized actions; typically these are the actions that the user could already perform without the program plus possibly a small set of explicitly defined actions that the user is granted by having access to run the program.
Extensibility: small numbers of small changes to the program should be relatively easy to make.
Privacy. The program should not reveal data about one user to any other person – possibly including the people who own the program – unless this is explicitly desired.
Dependency management. Most programs use code written by other people and other organizations, often in the form of libraries. The programmer needs to ensure that their use of these libraries is legal – i.e., does not violate any license the external code is shared under – and that collective set of libraries are cross-compatible – i.e., if library A uses library C version 1 and library B uses library C version 2, then the programming system either needs to support that [most don't] or situation needs to be resolved. The programmer also needs to ensure that none of the used libraries violate any of the other considerations in this list.
Deployment. Many programs are run somewhere other than the computer that the programmer wrote them on. This usually requires work by the programmer to enable, be it by having the program detect and adapt to the environment its running under, or containerizing it to run in a standard environment can be reproduced elsewhere, or writing another program to transform the original program for the new environment.
Monitorability/Observability. Not every program needs to worry about this, but long running programs will want the sort of information described under logging available in real time under a separate interface.
Persistence. Not every program needs to worry about this, but long running programs will often need to be stopped and restarted and almost all programming systems, the restarting part requires the programmer to do extra work.
Input validation. Not every program takes input, but most do, and when they do, they typically need to perform checks to make sure that the input follows the expected or required format, and/or can be converted to that format.
Error handling: when things go wrong, the program should degrade gracefully, by logging the error, alerting the correct set of users, and returning the program to an acceptable state.
Internationalization and localization. Not every program needs to worry about running in more than one country or communicating to users in more than a single language, but many do.
Accessibility. Again, not every program needs to worry about this, but many do need to accommodate users with different physical or mental capabilities, and to do so in a graceful manner.
I could go on, but I think I've made the point that that that's a lot of things, and it doesn't even include things the programmer needs to do while not writing code (such as estimating schedules or communicating with coworkers). Aspect Oriented Programming (AOP) was an idea pioneered by Gregor Kiczales and others at Xerox Parc in the mid-1990s that maybe programming would be easier if the programmer could concentrate on each of these separately one at a time, instead of all together at potentially each line of code.
I've always loved that idea, but I've also always hated the specific mechanism that AOP chose to implement it with – something called the "join point model" which basically amounts to runtime pattern matching on a program's call stack and running some code every time a pattern matches. For example, every time the call stack has a file open routine as the thing that was most recently pushed onto the stack, the program might log the name of the file that is about to be opened. As the Wikipedia page on AOP points out, this is very close to the "COME FROM" statement that was non-seriously proposed as a joke, and equally hard to debug and maintain.
But thanks to the newfound coding capabilities of LLM's, I think AOP deserves another look. In its new form, the programmer need only write separate documents for each concern. The documents will be of very different lengths, of course, with the correctness document typically being the longest. Many of the documents can be re-used and shared across projects; for example, an organization's style guide can be used for the maintainability concern. And the "weaver", to use the AOP term, is simply the LLM that generates the program from the documents.
Why is this better than AOP's old join point model? Well, that model was brittle: to log after every file open, it required matching against a specific string name or set of names for the file open functions. And if a coworker came along and added a new file open function (or worse, renamed one) that wouldn't get logged. Here, with the LLM doing the weaving, we are relying on its background knowledge of what opening a file is to do the matching.
LLMs also have the advantage that the output of their weave is static (i.e., not computed at the program's run time) and readable. There are some AOP implementations that do "compile-time weaving" like AspectJ's ajc, but they output things like not-really-readable raw Java bytecode.
It's also a bit of an unfair comparison, given that LLMs are going to be used to generate code whether or not they do AOP. My point is merely that AOP's notion of concerns gives us a useful way to organize the sentences that we feed into the LLM. (Or LLMs; one could also feed each concern's description into a separate agent whose job was to critique the code from that angle.)