Connections of Things
We continue from the previous chapter, regarding the small episode about Search.
The first scenario is quite simple and intuitive. The UserInterface
and Search
are developed independently:
public class UserInterface implements IProcessor<String, String>
protected IProcessor<String, Integer> finder;
public void setFinder(IProcessor<String, Integer> finder) {
this.finder = finder;
}
public String process(String keyword){
return keyword + " is: " + finder.process(keyword);
}
}
public class Search implements IProcessor<String, Integer>
public Integer process(String keyword){
do something search...
}
}
Querying the number of times a keyword appears, both developers naturally thought of using an IProcessor that takes a String as input and returns an Integer. This basic similar-concept is almost instinctive, and not thinking this way would be somewhat challenging (such developers should be taken out and used as an example). Of course, the implementer of UserInterface
might name it finder from their perspective, but that is not important. It is also noteworthy that the Search
interface is missing, and it is directly the Search
class. Because they must adhere to the agreement of only using similar-concepts interfaces, the constraint and shaping of human thinking by similar-concepts interfaces become evident
.
Next, the business changes to regular expressions, which is irrelevant and belongs to the Search
module’s domain. It can update itself independently. Since there is no dependency, UserInterface
is unaware of any changes.
Then, Search
needs to connect to multiple services, requiring additional parameters for each service call to specify the query directory. How should this be handled?
Doesn’t this seem like an easy task:
public class UserInterface implements IProcessor<String, String>
protected IBiProcessor<String, String, Integer> finder;
protected String path = "defaultPath";
public void setFinder(IBiProcessor<String, String, Integer> finder) {
this.finder = finder;
}
public void setPath(String path) {
if (path != null) this.path = path;
}
public String process(String keyword){
return keyword + " is: " + finder.perform(keyword, path);
}
}
public class Search implements IBiProcessor<String, String, Integer>
public Integer perform(String keyword, String dir){
do something search...
}
}
Isn’t this fine? UserInterface
and Search
use a binary processor, and they update together. Since the modules are independent, the external interface of UserInterface
remains unchanged, and the change goes unnoticed. Oh, to pretend that UserInterface
and Search
are not acquainted, the directory is called path
in UserInterface
and dir
in Search
. Perfect!
Perfect? Not really! Does UserInterface
truly not know Search
? How does it know about the service changes in Search
? Why should it understand these, and how does it learn about them? When using Eight, one should gradually develop the ability to segment things and have a clear boundary awareness. Do what must be done well, and do not touch what should not be done; each party is responsible for its own tasks
. Some might argue that it is not a big deal, just an additional path parameter, and it does not have much impact, only taking a few milliseconds. Without discussing how UserInterface
knows about this change (Eight does not encourage frequent communication), or the development and upgrade costs, how do you know that UserInterface
only connects to this Search
module? It might be deployed elsewhere where Search
does not need this parameter. How do you know UserInterface
is your development? What if its code is in Mauritius?
Ultimately, what does Search
’s path have to do with UserInterface
’s business? Why embed the required path into UserInterface
’s logic? If UserInterface
cannot maintain its stability, how can it be a reliable partner for other modules (including Search
in other environments)?
At this point, one might be confused. Logically, you are correct, but how should this be handled? Should Search
be responsible? It cannot be! How does it know where a call comes from? This is not within its business scope.
Indeed, this is neither UserInterface
’s domain nor Search
’s. So, who should be responsible?
This issue arises during the connection between UserInterface
and Search
. Both businesses are correct, but their similar-concepts has deviated. Therefore, it is the connection’s responsibility to adapt. Eight provides element for this purpose:
public class AdaptProcessor implements IBiProcessor<Object, Object, Object>, IProcessor<Object, Object> {
protected ITriProcessor<Object, Object, Object, Object> tri;
protected IBiProcessor<Object, Object, Object> bi;
protected Object para;
protected Boolean adapt;
public void setAdapt(Boolean adapt) {
this.adapt = adapt;
}
public void setBi(IBiProcessor<Object, Object, Object> bi) {
this.bi = bi;
}
public void setTri(ITriProcessor<Object, Object, Object, Object> tri) {
this.tri = tri;
}
public void setPara(Object para) {
this.para = para;
}
@Override
public Object perform(Object first, Object content) {
if (adapt == null) return tri.operate(first, content, para);
else if (adapt) return tri.operate(first, para, content);
else return tri.operate(para, first, content);
}
@Override
public Object process(Object instance) {
if (adapt == null) return bi.perform(instance, para);
else return bi.perform(para, instance);
}
}
It can masquerade as an IBiProcessor or IProcessor to proxy an ITriProcessor or IBiProcessor, injecting the pre-configured para (here, path) into the call process based on the adapt parameter. Thus, UserInterface
does not need to change a single line of code, and Search
does not need to worry about where the parameter comes from.
Why complicate things this way? Why insert a set of connections in the middle? It must be understood that this is its only place. The connection does not belong to A or B, and it is not stable, as it can disappear with changes in A or B. Modules need to maintain their independence and stability. Unless you are absolutely certain of the binding relationship between UserInterface
and Search
(in which case, you might as well merge the two modules and handle everything internally without pretending not to know each other), one should not let one party’s business be handled by the other. Even if the actual situation is known before development, potential dependencies should be filtered out during development, which is why Eight does not recommend deep communication (too many concerns can lead to losing oneself).
So, what exactly is a connection?
What is a Connection
Humans are carriers of experience and memory, each with their desires and motivations, which collectively form their essence. Different individuals have inherent differences, resulting in various cognitions. Is communication always effective? Sometimes yes, sometimes not. Even if UserInterface
knows that Search
needs an additional path, how do you know it does not think of perform(String path, String keyword)
? Humans can never fully understand each other, and this gap is insurmountable.
What enables people to collaborate and coexist? The connections between them. These connections do not belong to any individual but exist in the moments of their mutual entanglement. There are always differences between people, and their interfaces are often difficult to fully align. This is an unavoidable yet commonplace issue. So, what should be done? People naturally adapt to each other, adjusting their relationships in their entanglements. Think about your interactions with others; are any two relationships exactly the same? But these connections do not belong to individuals and are unstable. If the target disappears, so does the connection. If these connections were part of the individuals, they would have nothing left.
What if adaptation is impossible? Then they part ways; no one must stay together forever.
This is why Eight externalizes connections. But what exactly is a connection? What does it look like? Before answering this, let’s outline the entire runtime structure of Eight for a visual understanding.
The connection is the component named linker.
Running and Change
Eight differs from other frameworks in that it introduces an additional state - assembly time. Connections occur during assembly time. This state has no fixed boundary with runtime; it can occur before or during runtime, thus having both static and dynamic forms. Before entering runtime, Eight often requires configuring various connections.
We have already covered these, and using Spring for configuration needs no further introduction. Next, we will focus on dynamic connections during runtime, involving package, bundle, component, config, instance, and linker relationships.
- Firstly, the foundation, which is the Felix framework, with three built-in fundamental libraries: 1) Java runtime libraries; 2) similar-concepts interfaces; 3) Eight elements libraries. Eight also relies on some common utility libraries, which are generally invisible to the upper-level running bundles. Users may also develop their elements libraries. These form the lowest layer of Eight. Third-party libraries often have various cross-dependencies, making changes difficult, and upgrades usually mean a restart.
- Above this is the bundle layer. In Eight, there are no cross-dependencies between bundles; all dependencies are vertical, ensuring that no bundle physically depends on its peer bundles. This unravels the dependency web, so when code or configuration in a bundle is modified and needs reloading, other bundles remain unaffected. Of course, they all depend on the underlying libraries, especially the similar-concepts interfaces. If the underlying layer changes… well, that requires a system restart, so maintaining stability at the base is crucial. But what if the base lacks the required libraries? As previously mentioned, bundles can carry their own dependencies.
- Next is the component layer, transitioning from the physical to the logical layer. Components correspond to Eight’s components. The relationship and stories between components and instances can be referenced in the previous iPojo introduction. Note that a bundle can define zero to multiple components, but it is generally recommended to define only one, as multiple components inevitably have lifecycle coupling, which should be avoided in most cases.
- We know from earlier that a component often comprises a set of SpringContext, with numerous beans forming a relatively stable and meaningful whole. As mentioned, components and Spring can be entirely unused. Since bundles are independent, so are components.
- Eight’s elements libraries defines many components, forming part of the Eight platform, maintaining runtime services like dynamic loading and updating of bundles, components, config, instances, thread pool management, and message management. Additionally, like elements, it provides many optional components for system framework building.
- The above are static structures. Moving up, we enter the running part. Instances are to components what objects are to classes. Multiple instances can be generated from a component template. What is the config file for during generation? It is akin to constructor parameters, with Spring configurations and other environment parameters like instance name, type, label, priority, etc., configured here. Different configs generate different instances.
- After instance generation, config is injected into SpringContext, which then starts, generating and linking beans to provide services. Note that unlike typical modules (libraries, packages),
Eight's modules are living modules, independently existing and running without depending on the whole or others
, akin to a service in container. - Config is a text file monitored by Eight. Any modification to its items causes the instance to restart (note, not the bundle or component, avoiding class loader reloading). Modifying a bundle causes all instances generated by its components to restart.
- Linker is a connection. It is an instance created by a component corresponding to a bundle, configured and maintained by config, changing with config modifications. Linker does not run SpringContext internally; instead, it uses OSGi filters to select other instances and inject itself into both ends, acting as a bridge. Calls from the referencing end are passed through the IUniversal interface to the service end, enabling collaboration.
- Linker can execute simple adaptation scripts, currently supporting Groovy. For complex adaptations, an adaptation component may be added in the middle, though this brings extra development and instability, disappearing with the relationship. So, let’s hope our module developers’ similar-concepts differences are not too vast.
- Finally, due to Java’s nature, despite class dependency removal, Object dependencies may still exist. Control the types within a bundle spreading to other modules. Recommended solutions include: 1) Using basic data structures provided by core libraries like Java’s String, Map, List, Json for data provision, as internal class transmission is often incomprehensible to others (containerized systems achieve service dependency decoupling through serialization and deserialization). 2) If a service provides internal classes via similar-concepts interfaces, e.g., returning an IProcessor for further processing, use Eight’s similar-concepts proxy, adhering to null-query-weak reference rules to avoid holding internal class handles; 3) The calling side should follow a use-and-discard principle for non-basic types, avoiding long-term holding.
These are the introductory aspects of Eight. For more information, refer to the related technical documentation and introduction document.