To answer this question, we must first understand what is Aspect Oriented Programming (aka AOP). I like to see AOP as a response to a certain kind of failure of Object Oriented conceptions.
The failure of Object Oriented Programming ?
Object Oriented Programming has dominated the art of software engineering for many years. This approach modularises complex and large systems into technical and functional units according to an intuitive model close to the real world. This paradigm attempts to architect applications in modules that are, in principle, independent, by managing different aspects of the system. An object-oriented application is built around units of meaning: “objects”, whose life cycle are managed by “business” modules.
Cross-cutting concerns, however, appear quickly and systematically, slowly compromising the very principles of object-oriented programming. These concerns are meaningless in terms of modeling and are purely technical aspects serving the cause of the developed application. They are developed in the form of modules, not very conceptualized, and often too little accomplished in terms of abstraction.
Few examples of transverse preoccupations:
- persistence and transactions
- cache management
The more the application becomes large and complex, the more the presence of such modules becomes problematic. The developer will face two problems:
Entanglement occurs when a business module implements multiple cross-cutting concerns. Technical code appears across pure business models and logic, making the module less and less readable and focused more on its preocupation.
Dispersion occurs when the calls to these technical modules spread insidiously throughout the application. All parts of the code are somehow “contaminated” by the implementation of transversal concerns.
These are entanglement and dispersion phenomena
Aspect Oriented Programming is born from this failure
Until now, we discussed the notion of technical “aspects” of the code and the drifts they can introduce. Based on this observation, an elegant solution emerged: extracting these aspects and making them autonomous and applicable to any part of the application.
Cross-cutting concerns will now be managed from outside the business modules by specifying them in components called “Aspects”.
Aspects segregate transversale preoccupations
This new paradigm, beyond its purely conceptual definition, implies an adaptation of the languages in order to incorporate those “Aspects” into the business code.
Aspect-oriented programming does not replace object-oriented programming. It is actually the ideal complement to tend towards purely object-oriented modeling of an application.
The role of the developer is now to identify the “Aspects” to outsource and the conditions of their execution within the business logic.
Let’s bring in some formal concepts
Aspect Oriented Programming comes with the ability to specify the precise places where to insert the transversal code.
Jointpoint: Defines, in a theoretical way, WHERE an aspect can be inserted in the flow of execution of the code.The most common jointpoints are method calls or properties access.
Pointcut: Defines HOW to insert Aspect in joinpoints. It comes with a pattern language that allows to identify all the joinpoints WHERE to insert the transversal code.
Advice: Defines the transversale code to insert in the places spotted by the pointcuts.
Weaving: Defines the final process where the business code and the transversal code are mixed up together to form the whole application code, addressing all the preoccupations of the software design. AOP is about generating code (at compile time or runtime) at some specific point in the code flow to augment it.
Weaving advices into Jointpoints defined by pointcuts
Some examples with AspectJ
AspectJ is a reference implementation of the AOP paradigm in the Java world. As Java is a well known and easy to understand language, it will be a good llustration of the theoretical principles we’ve seen so far.
As the purpose of this article is not to master AspectJ, we will go through three simple examples of leveraging AOP to remove technical/transversal code from business logic: logging, the Singleton pattern, and the Observer pattern.
There will be a Swift counterpart for each one of these examples to echo the question asked in the title of the article.
Logging with AspectJ
Given these requirements:
The application must support the creation of a user, give this user a name, and prompt the user to display that name. The application should also log the manipulation of the user’s data.
We clearly see two concepts emerge: on the one hand, User modeling and its lifecycle, and on the other hand, logging the progress of the application. We could include logs in the code of the User; this would meet the requirements. But User would then lose its status of being a Plain Old Java Object and would not respect the principle of Single Responsibility. Obviously, one aspect must take care of this while maintaining the integrity of our object modeling.
The User POJO and its lifecycle
As is, this code won’t produce any logs. User is a POJO matching the OOP best practices: it’s only responsibility is to model our universe..
Here is the Aspect we can implement to add logs without corrupting the POJO:
This Aspect will log the calls to getName() and setName()
Without diving into AspectJ syntax, this aspect will add the following behaviors to the application:
- Each time User.getName() is called (actually around the call), an advice is executed to log the returned value
- Before the execution of User.setName(), an advice is executed to log the parameters passed to the method
After the weaving process, the execution of the application will result in the following printing:
Of course this is a trivial example, AspectJ is capable of so much more, but AOP has met its goal here. We added a purely technical preoccupation to the application without even modifying our business logic and model.
Logging with Swift
As our challenge is to implement a pure object model, we can start by implementing the User structure and its lifecycle similarly to what we’ve done with Java:
There is no such mechanism in Swift as weaving, but with Swift 5.1 we now have property wrappers (see my previous article on this topic), which act like a delegate when accessing a property. We could leverage that mechanism and delegate the logging to a generic property wrapper.
Each access to the value property will print logs
By annotating a property with the @Logged property wrapper, we will make its read/write access produce logs, BUT it requires us to slightly modify the original code:
The name property is now under the watch of a property wrapper
The execution of the application will print the following result:
The result is really close to what we achieved with true AOP. If we consider the @Logged annotation as some kind of metadata completing the “name” property, we can say that it is not intrusive in terms of object-oriented conception.
There is even an advantage in property wrappers. Since it is a generic concept, we can annotate the “name” property as well as a whole “user” property. This allows for logging to be easily extended on new fields added to the User structure.
The Singleton pattern with AspectJ
Singleton is a controversial design pattern. Having a single instance of an object can be useful, but it shouldn’t have an impact on the way the model is written.
AOP can help to segregate the implementation details inside an Aspect.
One of the rare cases where Singleton is acceptable is to implement a Logger.
Here is a naïve implementation in Java:
A naïve/not optimized implementation of a Logger in Java
In this implementation, there is no static instance member of type “Logger” to achieve the Singleton pattern. There will be as many instances of Logger as the number of times the constructor is called in our program.
However, AspectJ gives us the opportunity to capture the calls to the Logger constructor and provide a unique instance.
With AspectJ, the default instantiation strategy for an Aspect is to create a singleton
The instance of the logger to be created is now provided by the Aspect. Since the default instantiation strategy for an Aspect with AspectJ is a singleton, the created logger will inherently be a singleton.
Although loggerA and loggerB seem to be the result of two instantiations, there are the very same object
We achieved to have the benefits of using a Singleton without compromising the conception of the Logger.
The Singleton pattern with Swift
We will once again leverage property wrapper to delegate object creation to a third party actor that will ensure the uniqueness of the instance.
Let’s first implement a class that will enforce the singleton pattern by storing unique references inside a static dictionary.
Storage is a wrapper around a static dictionary that ensures uniqueness of instances
We can now implement a property wrapper that will use this storage as a provider for the generic value for which it acts as a delegate.
The value access is delegated to the static storage, thus enforcing the Singleton pattern
Let’s get back to the Logger example:
Once again, we are really close to what we achieved with AspectJ, if we can live with the fact of having to annotate the properties.
We can push the boundaries of the property wrapper even further with the two following add-ons.
1: By accessing the property wrapper dedicated functions (with the “$” notation), we can, for instance, reset the Singleton reference whenever it’s needed.
The property wrapper mechanism gives us opportunity to access utility functions to manage the storage
Once the reset()function is called, the related storage is cleared. A new Singleton reference will be created next time it is required.
2: As we can pass arguments to a property wrapper, we can introduce the concept of Scope. A scope could be seen as a strategy of instantiation for the managed objects. Singleton would only be one of the possible scopes (the default one actually). Whenever you define a custom scope, the instantiation would be attached to this scope only. It now makes sense to rename the Singleton property wrapper to a Managed property wrapper.
The scope is used as a component of the objects keys in the static dictionary of the Storage
By default, a @Managed property will belong to the “Singleton” scope. Whenever the scope is explicitly mentioned in the annotation, the created object will be segregated to this scope.
If we only consider AOP as a way to intercept property access and to inject code around them, then property wrappers are pretty close to what is an Aspect.
However, AOP is not just about capturing property calls. There is, for instance, a feature in AspectJ called inter-type declaration.
Inter-type declaration allows us to augment the business code by modifying the structure of the model. We can, for instance, add attributes and methods to a class or modify its inheritance relationships. We will see this principle in action by applying the Observer pattern to a model whose code won’t even mention this pattern and then see how Swift can address this issue.
The Observer pattern with AspectJ
The Observer pattern is used in object-oriented programming to decouple object modeling from the layers that will react to the mutations performed on this object. The observed subject maintains a list of observers, which it will notify when being mutated.
It is often used in architecture patterns like MVC to handle UI refresh when the business model changes.
In the traditional Java world, this pattern involves having interfaces like IObserver and ISubject to abstract the registration and notification mechanisms.
The model would have to implement the ISubject interface, whereas the observer layers would have to implement the IObserver interface
Let’s say we have a User model that implements ISubject and a UserView that implements IObserver. Establishing the connections between them would come down to:
The call to user.setName() would internally call the fireNotification() method
Making User and UserView implement interfaces is pretty intrusive in terms of modeling. For instance, it implies that User has a mechanism to keep track of its observers in some kind of list to be able to notify them. Fundamentally a User is just a User, it should not be aware of the observation mechanism.
We can take advantage of the inter-type declaration to remove this responsibility from the User model. From now on, take for granted that the code of User and UserView are completely free of technical concerns.
We will dynamically modify their structure in order to augment their code with the interface implementations.
This code can seem a little be tricky, but basically it does two things:
- make the User dynamically implement ISubject
- capture the calls to setter methods in User and subsequently fire notifications
We would do something similar for UserView to make it implement IObserver.
After the weaving process, User and UserView would implement their respective interfaces without even knowing it. The model is kept pure while the Observer pattern is implemented.
The Observer pattern with Swift
Although the challenge seems pretty high here, Swift gives us two tools to achieve our goal:
- extension will be leveraged to make UserView conform to Observer
- property wrapper will be used to make any desired property be an observed Subject
Let’s first define an Observer protocol. We will make use of an “associatedtype” to later take advantage of the language type-safety.
Observer being a protocol with an associatedtype, we apply a type-erasure technic to be able to use it as an instance property or as a function parameter whenever its needed
We can now define a Subject protocol. Like we did for Observer, we will also define an associatedtype representing the value that will be observed. It will allow to constrain both Observer and Subject associatedtypes to the same value type.
We can link Subject and Observers only if their respective associatedtypes are consistent
As expected, UserViewwill be a simple class whose only responsibility is to render a User. An extension can be implemented to make it conform to Observer.
Extensions can be seen as the Swift counterpart to AspectJ inter-type declaration as they modify the inheritance/conformance of a type
We now need to react to property mutations on a Subject to notify its observers. The trick here is to delegate the notifications to a property wrapper that will play the role of the Subject.
We can now annotate any property with @Observed and use the “$” notation to access its related Subject aspect.
This is pretty satisfying 👍. User and UserView remain pure in terms of Object Oriented conception, but they can be actors in an Observer pattern thanks to Extension and Property Wrapper.
Swift: An Aspect Oriented Programming Language ?
Not really. Aspect Oriented Programming languages are not limited to modifying a type inheritance or executing code whenever a property is accessed. They are about :
- precisely defining the conditions of insertion for an Aspect (before, after, around methods calls, properties accesses, exceptions thrown, …)
- weaving them statically or dynamically
- performing introspection on the intercepted code and being able to completely change the original code behavior
But Swift is a multi-paradigm language for sure. This is what makes it so versatile, powerful, and yet easy to learn. It is at the same time:
- an Object Oriented Programming language: with concepts such as classes and objects, abstraction, inheritance and polymorphism
- a Protocol Oriented Programming language: with tools such as protocol extensions, conditional conformance and protocol composition
- a Functional Programming language: with immutable data types like structs and enums or concepts like higher order functions and monads
- an Aspect Oriented Programming language: mainly thanks to extensions and property wrappers
Thanks for reading and don’t hesitate to give your feedback about Swift being or not being an Aspect Oriented Programming language.