At Salesforce you have heard the saying “Extend Salesforce with Clicks, Not Code.” However, there are times we must code to extend Salesforce — why not extend code to support clicks? After all, using configuration to change the runtime behavior of code is possible, if you are willing to follow good design principles and use the provided aspects in Apex.
This blog post will provide more details as how one might go about the “configuration first” in code. This post will specifically only address logging (cross-cutting concern); however, the other concerns (caching, exception handling, etc.) follow the same pattern.
Take Logging for example. It seems easy but it can become an issue no matter how small or large the project is. Here are issues I have experienced (you may as well):
- Too verbose
- Not verbose enough
- Disagreement on log level
- Disagreement on which layer to log
- Inconsistent logging behavior
Figure 1 Complexity of where to log?
Even if you could agree upon some of the above aspects, you still encounter code like:
Figure 2 Example logging code
Why is this considered a problem? The above code is only writing to the system debug log and only during tests. In production, or not running tests, it provides no value when trying to determine entry/exit for users. In addition, the class and method were excluded and only informational data is provided. The tightly coupled dependent code has the following issues:
- Dependent upon system debug log
- Dependent upon test execution
- Tightly coupled to INFO logging level and system log
- No means to vary the logging level or output content
As you can see, something like logging is more challenging than you may think. Couple this with different development teams, experience, changing requirements, and needs and the problem becomes more exacerbated — and this is only logging! Taking other cross-cutting concerns into consideration (exception handling, caching, etc.) we begin to see our code becomes very brittle and subject to software rot. We need the right hammer to address these issues.
New hammer / Apex Cross-Cutting Concerns
As discussed previously, these issues with logging surface in other cross-cutting concerns. We need to find a new hammer. This new hammer has a goal. The goal is to split code into loosely-coupled, highly-cohesive components and then glue them back together in a flexible manner to meet different requirements based on environments. To realize this goal, I decided to develop an unmanaged package I called Apex Cross-Cutting Concerns/ACCC (found in GitHub).
Cross cutting concerns are aspects, such as, logging, exception handling, caching, etc. which are found in your basic code layers, i.e., Presentation, Business and Data; thus, cross-cutting
Let’s go back to the original example and compare the old hammer with the new hammer.
New hammer (ACCC)
Using ACCC framework, you have the ability to be loosely coupled to a sink (system log, custom object, platform event, etc.) as well as vary the runtime environment (test, debug, production) behavior. As you can see, the old hammer littered the method with if/else as well as controlled the amount of information (via INFO) making the old hammer very brittle. The ACCC code can change without recompiling and testing and does not change. You are no longer coupled to new requirements, environments, and behavior.
Figure 3 ACCC framework — consistent, extendable, and reusable
Furthermore, the above code falls back to its parent class. For example, if there is an exception (i.e. DML exception, etc.) while writing to a custom object or publishing a platform event, the logger will call its parent class; in this case, accc_ApexLogger, which writes to the system debug log.
How does Apex Cross-Cutting Concerns work?
In order to satisfy the defined goal: loosely-coupled, highly-cohesive components, and then glue them back together in a flexible manner to meet different requirements based on environments, the following recipe was followed:
- Use SOLID Principles,
- Decouple the environments (test, debug, production) from the code (via custom metadata)
- Use Dependency Injection (DI) and Inversion of Control (IoC).
The sub-sections which follow expound upon this recipe.
SOLID Principles make it easy for a programmer to develop software that is easy to maintain and extend. SOLID defines five principles:
S – Single-Responsibility (SRP) – A class should have one and only one reason to change.
O – Open-Closed (OCP) – Class should be open for extension, closed for modification.
L – Liskov substitution (LSP) – Derived classes must be substitutable for their base classes.
I – Interface Segregation (ISP) – Make fine grained interfaces that are client-specific
D – Dependency Inversion (DIP) – Depend on abstractions, not on concretions.
Here is how SOLID principles were applied to the framework and how they benefit the user:
- [SRP] Ensured we kept the classes singularly focused and the number of methods small. This allowed for others to easily replace sub-types (classes/methods) as well as refactor and test.
- [OCP] If there was a need to vary the behavior of the class (i.e. logging), extensions (sub-classing) were used. This made testing the sub-classes easier because the parent was already tested; just tested the overridden methods. In addition, I could fallback to the parent’s behavior without introducing the duplicate logic.
- [LSP] All loggers supported the same interface. One can substitute logging to a custom object (accc_ApexObjectLogging) with sending platform events (accc_ApexPublishEventLogger).
- [ISP] Interfaces were used for specific behavior, i.e., ILogger for logging, ICache for caching, IExceptionHandler for exception handling, etc. This allows the concreteness of the logger behavior (ILogger) to be substituted at runtime.
- [DIP] The defined abstractions/interfaces were used so one can vary implementations. The reader will note that all cross-cutting concerns can be referenced by the interface. All interfaces were prefixed with an I, for example, ILogger. This will allow users to implement new functionality without overriding current behavior.
With SOLID principles the ACCC framework can easily support many different behaviors (sub-types):
- Logging to System.Debug (accc_ApexLogging->accc_ILogger)
- Logging to a custom object, Application_Log (accc_ApexObjectLogging -> accc_ApexLogging)
- Logging via Platform Events (accc_ApexPublishEventLogger->accc_ApexLogging)
- No Logging (accc_ApexNoOpLogger→accc_ApexLogging)
Notice how all classes (above) inherited from the base class, accc_ApexLogger. This was done to ensure if an exception occurred (DML exception, etc.) the child class could invoke the parent’s write method; in this case, writing to the system debug log.
Decouple the environments
At this time, ACCC supports three runtime environments:
- Debug (i.e. Are you in a sandbox?)
- Test (i.e. Are you running in a Test (Test.isRunning)?)
- Production (neither item 1 nor 2)
This provides the ability to have different runtime behavior in different environments. In a production environment you may want to log to the application-log (custom) object, but in the debug environment you want to log to the System Debug Log. In test, you may not want to log at all. This is all controlled by changing the custom metadata (DI/IoC). The next section covers the metadata.
Dependency Injection (DI) and Inversion of Control (IoC)
The cross-cutting concerns/aspects use custom metadata types to allow runtime behavior to be changed. There are three custom metadata types. However, in this blog, only two are discussed.
- Accc_Apex_Code_Configuration__mdt contains the concrete classes that implement the interfaces. These concrete classes are then loaded by the object factory and exposed via the runtime engine.
- AcccCrossCuttingUtilityData__mdt holds singleton values such as, log-level, tracing on/off, caching strategy turned on, etc.
- AcccDataCachingDataModel__mdt defines aspect of data caching (Which is covered in the Wiki in the GitHub repo, but not in this blog).
This metadata holds the concrete class names. To provide a bit more context, ACCC defines a core set of runtime interfaces. For example, logging (accc_ILogger), exception handling (accc_IApexExceptionHandler), caching (accc_ICache), etc. These interfaces rely on an implementation. When the runtime is referenced, it uses a factory to load the implementation by reading this custom metadata. It is this metadata which can be changed at runtime to vary execution behavior (the underlying implementation). Additionally, a developer is able to define new concrete classes that adhere to the interface/behavior (see the four different loggers) which can also be injected at runtime and different environments.
The observer will note there are three defined environments (Production, Debug, and Test). These defined environments allows one to further alter which behavior will be used. For example, if you look at test configuration below (outlined in red), you will see that the logging behavior is defined to use accc_ApexLogger (which writes to the system-debug log) in the test environment; while in the production environment, accc_ApexObjectLogger, is defined (which writes data to a custom application log when called).
Again, it should be noted that we are speaking on just the logger aspect. There are other aspects (caching, exception handling, caching strategy, etc.) which this blog does not cover (see the Wiki for more discussion on the other aspects).
This custom metadata type defines singleton values used to control finer details such as Log-Level, Caching Strategy On/Off, tracing, etc.
For example, in the above configuration, the production environment will log with Info details and will use a caching strategy, if caching is used (i.e. accc_CacheMgr), to determine whether a called SOQL expression should be cached. Show CC Information indicates whether to allow the cross-cutting code to write to the logger. Normally, you would not because it adds to the log data about ACCC code. However, enabling this flag will include log information from the ACCC framework. LogLevel for Trace provides a means to provide more detail information (see the below section on Log Level Information for more details).
Log level information
The trace method in logging uses log level to provide a convenient (controllable) way to write additional information to a sink. For example, the code below writes more information to the sink than what you see in the code below. This is controlled by the log level defined in the custom metadata (see previous section), LogLevel for Trace. This provides the ability to write more or less information out to the sink via configuration.
For example, selecting a Log Level of Finest (LogLevel for Trace) and enabling tracing (custom metadata, AcccCrossCuttingUtilityData__mdt), you would see the following additional information in the log (Note: the above code was extracted from the accc_ApexCacheMgr class and is an example only).
The above granularity applies to the following LogLevel: INTERNAL, ERROR and FINEST.
The following LogLevel values are defined below along with the additional information written to the sink:
- INTERNAL, ERROR and FINEST will add LogLevel, Class Name, Method Name, Line Number, User- Message, Start Time, Start Time (Long), Stack trace
- FINER will add LogLevel, Class Name, Method Name, Line Number, User- Message, Stack trace
- FINE will add LogLevel, Class Name, Method Name, Line Number, User- Message
- DEBUG will add LogLevel, Class Name, Method Name, User-Message
- None of the above will add LogLevel, User- Message
Furthermore, all loggers use accc_ApexLogFormatter (which inherits from accc_IDataFormat) to write the log level information to the sinks. This is advantageous because you can change what/how gets added to the sink.
The custom metadata configuration above shows the class, accc_ApexLogFormatter as the Log Formatter. Because this class is used by ALL the loggers to add log level information, you can change the log level data written to the sinks. Thus, if you did not want the LogLevel to be written out in the log, you can easily change this as well. Note: For long term compatibility, code changes made should be done via inheritance; thus, create a class call MyLogFormatter and inherit from accc_ApexLogFormatter and override the appropriate methods.
Whether Cross-Cutting Concerns or code in general, Salesforce provides the ability to extend code to support clicks as demonstrated in the Salesforce Cross-Cutting Concerns framework (ACCC). There are other aspects in the framework we did not cover in this blog for brevity. Unfortunately, with extensibility comes complexity. However, the Wiki found on GitHub provides considerable information on logging and the other cross-cutting concerns supported to provide more clarity.
I hope you have found it useful and/or informative. It is only the start as there are other layers (data, business, service, presentation) we have yet to touch.
To learn more about this topic in Trailhead, check out the Custom Metadata Types module.