The Clean Object-Oriented and Layered Architecture (COLA) is designed to provide a set of simple guidelines and constraints for application architectures. These guidelines are replicable, readable, implementable, and able to control the degree of complexity. However, COLA is not simple enough in its implementation, so I upgraded COLA by removing some of its concepts and features, but did not add any new features. This upgrade makes COLA simpler and more effective.
Recently, a colleague told me that COLA had been selected as one of the candidate application architectures for the initialization of Alibaba Cloud Java applications.
This is really something. Therefore, I decided to review the strengths and weaknesses of COLA during its evolution.
Undoubtedly, COLA is a successful architectural concept, but it is not an effective framework because it is not simple enough and has redundant features.
As the developer of COLA, even I myself seldom used some of its features and could not figure out why they existed.
Therefore, I upgraded COLA 2.0 to 3.0. In this upgrade, I removed some of COLA's concepts and features, but did not add any new features. This makes COLA a more concentrated application architecture rather than just a provider of framework support and architectural constraints.
This upgrade follows the principle of Occam's Razor.
Occam's Razor states that "Entities should not be multiplied unnecessarily." Though not explicitly found in the writings of William of Ockham, the idea is hinted at in his Commentary on the Sentences of Peter Lombard.
In practice, Occam's Razor can be translated into the following guidelines:
"If there are multiple theories that can explain the same phenomenon, the simplest one is most likely to be correct. If you can achieve your goal in n actions, there is no need to take additional actions."
For example, in the fable of the Emperor's New Clothes, we cannot know for sure whether or not the emperor's clothes are invisible or he has been tricked. Imagine you are one of the ministers of the emperor, you can solve this puzzle using Occam's Razor.
The first possibility requires three assumptions: (1) The emperor is actually wearing clothes; (2) The foolish cannot see the emperor's clothes; and (3) You are foolish. Therefore, you cannot see the emperor's clothes.
The second possibility assumes only that the emperor is not wearing any clothes. Therefore, you cannot see the clothes.
The two lines of thinking lead to the same result, but the second argument is simpler. The first argument makes an incorrect assumption from the start, so it needs more assumptions to remedy it.
All truths are simple and clear, with no need for disguise.
Another example is the geocentric theory versus the heliocentric theory. Ptolemy's geocentric theory is based on the complex epicycle-deferent model. This model allows us to quantitatively calculate the motion of a planet and then infer its position.
As observational instruments continuously improved in the late Middle Ages, people were able to measure the positions and motions of planets more accurately and observed that the actual positions of planets deviated from the predictions of the epicycle-deferent model. These deviations were minor in the beginning but later seriously affected the accuracy of planet position calculations even when the number of epicycles was increased to over 80.
In 1543, the Polish astronomer Copernicus published the revolutionary work De Revolutionibus Orbium Coelestium (On the Revolutions of the Celestial Spheres). Copernicus's theory proposes that the Sun is the center of the universe, with all planets rotating around the Sun. This theory also holds that the Earth is also one of these planets and rotates on its own axis like a gyroscope while rotating around the Sun like other planets.
Copernicus's calculations were rigorous but simple. Compared with the geocentric theory with more than 80 epicycles, Copernicus's formula was in better agreement with the observed data. Therefore, the geocentric theory was ultimately replaced by the heliocentric theory.
A deep insight into our systems reveals many cases of unnecessarily multiplied design elements similar to the geocentric theory.
From the perspective of system architecture, unnecessarily multiplied design elements result from an improper definition of system boundaries, producing unclear responsibilities and confusing dependencies.
From the perspective of application architecture, unnecessarily multiplied design elements are the result of improper and excessive designs made to achieve flexibility and scalability. Such a design results in a series of packing, hide, and forward actions in the code logic that should be directly presented. This makes the code barely readable and comprehensible and significantly increases maintenance costs.
For example, the emphasis on workflow orchestration is a kind of unnecessary design typical of business systems.
The following figure shows the pipeline design of a business system that aims to break down a complex business operation into multiple small processing units.
The structural breakdown has no problem at all but it does not follow Occam's Razor. For more information about structural breakdown, see my article How to Code Complex Applications: Core Java Technology and Architecture. The code logic here is neither direct nor simple because it requires a maintenance engineer to query the database after the handler function is executed in order to know what components are called.
The code logic can be rewritten in the following way:
public class CreateCSPUExecutor {
@Resource
private InitContextStep initContextStep;
@Resource
private CheckRequiredParamStep checkRequiredParamStep;
@Resource
private CheckUnitStep checkUnitStep;
@Resource
private CheckExpiringDateStep checkExpiringDateStep;
@Resource
private CheckBarCodeStep checkBarCodeStep;
@Resource
private CheckBarCodeImgStep checkBarCodeImgStep;
@Resource
private CheckBrandCategoryStep checkBrandCategoryStep;
@Resource
private CheckProductDetailStep checkProductDetailStep;
@Resource
private CheckSpecImgStep checkSpecImgStep;
@Resource
private CreateCSPUStep createCSPUStep;
@Resource
private CreateCSPULogStep createCSPULogStep;
@Resource
private SendCSPUCreatedEventStep sendCSPUCreatedEventStep;
public Long create(MyCspuSaveParam myCspuSaveParam){
SaveCSPUContext context = initContextStep.initContext(myCspuSaveParam);
checkRequiredParamStep.check(context);
checkUnitStep.check(context);
checkExpiringDateStep.check(context);
checkBarCodeStep.check(context);
checkBarCodeImgStep.check(context);
checkBrandCategoryStep.check(context);
checkProductDetailStep.check(context);
checkSpecImgStep.check(context);
createCSPUStep.create(context);
createCSPULogStep.log(context);
sendCSPUCreatedEventStep.sendEvent(context);
return context.getCspu().getId();
}
}
public class CreateCSPUExecutor {
@Resource
private InitContextStep initContextStep;
@Resource
private CheckRequiredParamStep checkRequiredParamStep;
@Resource
private CheckUnitStep checkUnitStep;
@Resource
private CheckExpiringDateStep checkExpiringDateStep;
@Resource
private CheckBarCodeStep checkBarCodeStep;
@Resource
private CheckBarCodeImgStep checkBarCodeImgStep;
@Resource
private CheckBrandCategoryStep checkBrandCategoryStep;
@Resource
private CheckProductDetailStep checkProductDetailStep;
@Resource
private CheckSpecImgStep checkSpecImgStep;
@Resource
private CreateCSPUStep createCSPUStep;
@Resource
private CreateCSPULogStep createCSPULogStep;
@Resource
private SendCSPUCreatedEventStep sendCSPUCreatedEventStep;
public Long create(MyCspuSaveParam myCspuSaveParam){
SaveCSPUContext context = initContextStep.initContext(myCspuSaveParam);
checkRequiredParamStep.check(context);
checkUnitStep.check(context);
checkExpiringDateStep.check(context);
checkBarCodeStep.check(context);
checkBarCodeImgStep.check(context);
checkBrandCategoryStep.check(context);
checkProductDetailStep.check(context);
checkSpecImgStep.check(context);
createCSPUStep.create(context);
createCSPULogStep.log(context);
sendCSPUCreatedEventStep.sendEvent(context);
return context.getCspu().getId();
}
}
The rewritten code logic is direct and easy to maintain and supports the same degree of component reusability as the initial code logic. The rewritten code logic follows Occam's Razor. Though the pipeline design follows the open/closed principle (OCP), its code logic is barely readable and clearly inferior to the rewritten code logic.
The following sections explain the changes made when COLA was upgraded to 3.0.
In the initial development of COLA, I used the Command mode to process user requests by drawing on the experience of Command Query Responsibility Segregation (CQRS). This design aimed to build a framework to impose constraints on the processing of commands and queries and to split the service logic into the CommandExecutor, which prevents the service volume from increasing too fast.
This design is similar to the tedious, indirect pipeline design mentioned above. See the following code logic:
public class MetricsServiceImpl implements MetricsServiceI{
@Autowired
private CommandBusI commandBus;
@Override
public Response addATAMetric(ATAMetricAddCmd cmd) {
return commandBus.send(cmd);
}
@Override
public Response addSharingMetric(SharingMetricAddCmd cmd) {
return commandBus.send(cmd);
}
@Override
public Response addPatentMetric(PatentMetricAddCmd cmd) {
return commandBus.send(cmd);
}
@Override
public Response addPaperMetric(PaperMetricAddCmd cmd) {
return commandBus.send(cmd);
}
}
The code logic seems clean at first glance but is not direct at all because it does not clarify what executor processes ATAMetricAddCmd. Besides, we need to understand the workings of the CommandBus and how it registers executors. This makes the code logic very hard to read.
To simplify the code, I removed the CommandBus to comply with Occam's Razor and rewrote the code in a more direct way. The new code gets rid of the interceptor feature provided by the framework. This feature is also involved in the second change made in the COLA 3.0 upgrade.
public class MetricsServiceImpl implements MetricsServiceI{
@Resource
private ATAMetricAddCmdExe ataMetricAddCmdExe;
@Resource
private SharingMetricAddCmdExe sharingMetricAddCmdExe;
@Resource
private PatentMetricAddCmdExe patentMetricAddCmdExe;
@Resource
private PaperMetricAddCmdExe paperMetricAddCmdExe;
@Override
public Response addATAMetric(ATAMetricAddCmd cmd) {
return ataMetricAddCmdExe.execute(cmd);
}
@Override
public Response addSharingMetric(SharingMetricAddCmd cmd) {
return sharingMetricAddCmdExe.execute(cmd);
}
@Override
public Response addPatentMetric(PatentMetricAddCmd cmd) {
return patentMetricAddCmdExe.execute(cmd);
}
@Override
public Response addPaperMetric(PaperMetricAddCmd cmd) {
return paperMetricAddCmdExe.execute(cmd);
}
}
The interceptor was designed based on the CommandBus to better implement the Command mode. In essence, the interceptor works in the same way as Spring Aspect-Oriented Programming (AOP).
As AOP already provides complete capabilities, the interceptor is not an indispensable component and seldom used by COLA users, including me. Therefore, I removed the interceptor from COLA 3.0.
The importance of naming is not described here. When I designed COLA, I wanted to standardize the names of common features from the framework perspective. However, this idea was not feasible.
When my team implemented COLA early on, we often argued about how to correctly define a converter and an assembler.
Later, I realized that naming, despite its importance, was only part of the specification formulated by a team. There is no essential difference between a validator and a checker. It is only a matter of naming that varies from team to team. It does little good to unify conventions within a team from the framework perspective, so I removed the convertor, validator, and assembler from COLA 3.0.
Business identity and extension points are key concepts of the TMF and also the key methodology that the Alibaba business mid-end uses to support multiple businesses.
COLA aims to implement lightweight extensions, so the business identity and extension point features are retained in COLA 3.0. The extension point feature was designed by drawing on the experience of the mid-end TMF. Therefore, the corresponding class scanning solution completely follows the implementation mode of TMF.
In fact, the class scanning solution of TMF is unnecessary for COLA because COLA is based on Spring, which relies on class scanning. Therefore, we can reuse the class scanning feature of Spring instead of developing a solution on our own.
Native Spring provides at least three methods to fetch the beans of custom annotations. The simplest method is ListableBeanFactory.getBeansWithAnnotation. Alternatively, you can scan packages by using ClassPathScanningCandidateComponentProvider.
When I upgraded COLA 2.0 to 3.0, I used the getBeansWithAnnotation method to fetch the bean of @Extension so that COLA 3.0 can support the extension point feature. The class scanning solution of TMF is no longer used by COLA.
The upgrade from COLA 2.0 to 3.0 aims to remove the redundant features that I identified when using COLA in actual scenarios. As a basic application architecture of Alibaba Cloud, COLA has become increasingly influential. I have the responsibility to provide correct guidelines on COLA implementation. Therefore, compared with the 2.0 version, COLA 3.0 is simpler, more effective, and easier to use after I removed the redundant features.
COLA can be viewed as an architectural idea and also a componentized framework.
Viewed as an architectural idea, COLA is an application architecture that integrates the ideas of a layered architecture, adapter architecture, domain-driven design (DDD), clean architecture, and TMF.
COLA 3.0 retains a large portion of the architectural idea of the 2.0 version and gets rid of the Command concept. This turns CQRS into an optional feature.
Viewed as a componentized framework, I removed most of the component capabilities of COLA 2.0 based on Occam's Razor so that COLA 3.0 only retains the extension point feature. Therefore, COLA 3.0 is simple and effective, with minimum constraints on application developers.
In a sense, the upgrade of COLA 2.0 to 3.0 is actually a feature cleanup of COLA.
I believe that this cleanup will make COLA more compliant with Occam's Razor, lighter-weight, and more sustainable.
Link to the open-source COLA project: https://github.com/alibaba/COLA
The page start.aliyun.com (page in Chinese) is an engineering scaffold generation platform based on Spring Initializr. It allows you to quickly build distributed application systems by adding annotations and a few settings. It provides a wide range of localized component dependencies and is free of network latency. Currently, start.aliyun.com is only available in Chinese.
Cloud-Native Storage: Container Storage and Kubernetes Storage Volumes
507 posts | 48 followers
FollowFrank Zhang - August 4, 2020
Frank Zhang - April 28, 2020
Alibaba Cloud MaxCompute - December 20, 2018
Alibaba Cloud Native Community - January 27, 2022
Alibaba Cloud Community - December 20, 2022
Jitendra - April 20, 2022
507 posts | 48 followers
FollowMulti-source metrics are aggregated to monitor the status of your business and services in real time.
Learn MoreCustomized infrastructure to ensure high availability, scalability and high-performance
Learn MoreAccelerate and secure the development, deployment, and management of containerized applications cost-effectively.
Learn MoreAccelerate software development and delivery by integrating DevOps with the cloud
Learn MoreMore Posts by Alibaba Cloud Native Community