Always remember that you’re absolutely unique. Just like everyone else.
Margaret Mead
The third component of the FLUID methodology is universal interoperability: each component provides a single generic functionality to the collective.
The FLUID methodology focuses on pragmatism, time to market and ease of implementation, while rejecting careless and unprofessional programming practices.
This approach is never about cutting corners for shipping a product earlier: in my experience, this shortcuts harm the project by increasing technical debts, and make the code base harder to maintain and extend.
Conversely, it emphasises adopting a development framework where effectiveness is the primary goal, alongside delivering professional-grade, high-quality software
The third principle of the FLUID methodology is central to this aim, which is just fitting as it’s represented by the third letter of the acronym: Universal Interoperability, or for brevity, Universality.
Universality refers to a component’s ability to offer a specific functionality to any user, allowing any other component with the same functionality to replace it seamlessly.
Universality cuts through systems at various levels: within a program, across programs using a common library or throughout different services serving a certain business operation; for this reason, the definition refers to generic systems and components.
Universality vs Separation of Concerns
At face value, universality appears to be a redefinition of the principle of separation of concerns from the SOLID methodology, but the two are different in a subtle but relevant way.
SoC focuses on a component’s internal implementation, whereas universality concentrates on the external functionality provided.
SoC is centred on constraining each code unit (class, function, module, etc.) to a single structural concern; for example, providing access to a database, convert the data from a certain source to the internal representation in a program, apply a specific flow of the business logic, save the log entries to a specific device, and so on.
Universality has a focus on a unique functionality exposed by a certain component, which might require to serve multiple, interconnected concerns. For example, a component providing a cached persistent storage has to handle the two separate concerns of persisting the data and serving it to to its customers, but that’s obfuscated by the universality principle; the component is free to implement the most effective strategy to fulfil its function, rather than rigidly separating the concerns. In other words,
universality allows to interpret internal concerns as implementation details.
Example: a persistent cache
Let’s analyse this example, implementing it following the SOLID principles; in particular, separation of concerns.
As we have two concerns, that of persistency and that of memory retrieval, we’ll create two components (I’ll put some classes together for brevity).
First the in-memory cache.
// Interface class file
public interface InMemoryCache<K, V> {
void add(K key, V value);
void remove(K key);
V retrieve(K key);
}
// Implementation class file.
import java.util.concurrent.ConcurrentHashMap;
public class ThreadSafeMemoryCache<K, V> implements InMemoryCache<K, V> {
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
@Override
public void add(K key, V value) { cache.put(key, value); }
@Override
public void remove(K key) { cache.remove(key); }
@Override
public V retrieve(K key) { return cache.get(key);}
}
This example implementation is trivial to the point of parody, but a real implementation may have additional processing of the cached data, logging, validation and so on (concerns that would be separated in other components, and used here through dependency injection).
Next, the persister; for brevity, I’ll remove the actual database connections and queries.
// Interface class file
public interface Persister<K, V> {
void persistAdd(K key, V value);
void persistRemove(K key);
V retrievePersisted(K key);
}
// Implementation class file
public class DatabasePersister<K, V> implements Persister<K, V> {
// Assume this interacts with a database
@Override
public void persistAdd(K key, V value) {
System.out.println("Persisting key-value pair to the database.");
}
@Override
public void persistRemove(K key) {
System.out.println("Removing key-value pair from the database.");
}
@Override
public V retrievePersisted(K key) {
// Placeholder for database retrieval
System.out.println("Retrieving key-value pair from the database.");
return null; // Should return the actual value from the database
}
}
Finally, putting all together, the persistent cache class:
public class PersistentCache<K, V> {
private final InMemoryCache<K, V> memoryCache;
private final Persister<K, V> dbPersister;
public PersistentCache(InMemoryCache<K, V> memoryCache, Persister<K, V> dbPersister) {
this.memoryCache = memoryCache;
this.dbPersister = dbPersister;
}
synchronized public void add(K key, V value) {
memoryCache.add(key, value);
dbPersister.persistAdd(key, value);
}
synchronized public void remove(K key) {
memoryCache.remove(key);
dbPersister.persistRemove(key);
}
synchronized public V retrieve(K key) {
V value = memoryCache.retrieve(key);
if (value == null) {
// If not in memory, try retrieving from the database
value = dbPersister.retrievePersisted(key);
// Optionally, re-add to memory cache for faster subsequent access
if (value != null) {
memoryCache.add(key, value);
}
}
return value;
}
}
This code respects all the SOLID principles, particularly SoC. Yet, a trained eye will immediately see some problems:
- the memory cache is already thread-safe. Putting it in a synchronised section is correct, but suboptimal: it constitutes a nested locking.
- because of the opaque nature of the components in SOLID, we need to be agnostic with respect to how the persister works. If it’s implemented as a synchronous database access, putting it in a synchronised section may have serious negative performance impacts for the class users.
A more FLUID approach
Let’s rewrite this code with universality rather than separation of concerns in mind.
import java.util.concurrent.ConcurrentHashMap;
public class PersistentCache<K, V> {
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
private final MyDatabaseConnection conn = new MyDatabaseConnection();
public void add(K key, V value) {
cache.put(key, value);
conn.async_insert(key, value);
}
public void remove(K key) {
memoryCache.remove(key);
conn.async_remove(key);
}
public V retrieve(K key) {
V value = cache.get(key);
if (value == null) {
// If not in memory, try retrieving from the database
value = conn.retrieve(key);
// Optionally, re-add to memory cache for faster subsequent access
if (value != null) {
cache.putIfAbsent(key, value);
}
}
return value;
}
}
For simplicity, I omit the implementation of MyDatabaseConnection; just consider it a thin layer on top of a well defined database connection.
The component offers a persistent cache functionality, but it doesn’t separates the concerns of persistent storage and memory cache.
Because of that, we observe a few advantages with respect to the previous code:
- The code is more compact, which is an advantage in FLUID as described in the previous article.
- The user of the class doesn’t have to prepare the sub-components in order to invoke its constructor; the object is fully self-contained.
- The universal component can make assumptions on the characteristics of its constituents; by knowing that the memory cache is a full-fledged concurrent object, and the storage provider has an asynchronous interface, it can chose its own synchronisation strategy.
This code is focused on offering the best possible service to its clients, while the previous implementation was focused on working with generalised, unknown sub-components.
Will this turn into spaghetti code?
By the second principle of the FLUID methodology, lean code, we know that the most flexible, future-proof code is the most compact one.
For example, were we to introduce multiple memory storage models or persistency providers, the smaller the codebase, the less effort would be required to accommodate such changes.
By making the component “universal”, that is, offering a publicly advertised functionality with little regard on how that is internally achieved, we retain the ability to change the underlying implementation when necessary, if the need ever arises.
Universality and Interface Contracts
For a component to be universal, it’s necessary that the contract offered by that component is well-defined and known by its users.
Contract-oriented languages (i.e. typescript), typing extensions, formal integrated documentation (i.e. function decorators, javadoc, swagger and so on), are all methods to enforce interface contracts between components at various levels of the system hierarchy.
The topic is too vast to be fully treated in this article. I will just note in passing that a formal contract definition of some sort is integral to the concept of universality.
Which sort of contract definition is adopted depends on the specifics of the project; technology stack and system hierarchy level are the two main factors determining how the interface contracts are implemented.
Conclusion
Universal Interoperability is the third principle of the FLUID methodology; it focuses on the functionalities publicly provided by components at any level of a project hierarchy to any user.
While maintaining well known contract with the users, the component themselves are free to implement the offered functionalities as they see fit, harmonising their internal structure with the other FLUID principles.