4.5 Adapting Behaviors
Overview
Just as contexts can be adapted, so can behaviors, although in more limited ways. Adaptation in behaviors is focused on extending or replacing functionality and it is generally very similar to operation hierarchies, with the purpose to either make it more specific in terms of its qualifications/contexts and available operations or change it entirely and prevent the original functionality when new qualifications are met. Doing either can be useful when working with third-party packages or when implementing conditional polymorphic functionality, as described below.
Extending
There have been a number of steps throughout this chapter to make the logging feature capable of handling multiple routes of logging; to output to the console or to one or more files. While that is possible with the current implementation, it would require multiple implementations of Perform Console Logging
for each different type of route, despite the over-arching behavioral logic (the contexts and qualifications) generally being the same.
The final concept of abstraction needed to fully realize this objective will be discussed later, but the implementation can be updated with behavior adaptation to prepare for that. Specifically, the detail of exactly what a specific Console Log Route
does can be extracted from Perform Console Logging
and implemented through an extending behavior.
To review, consider the Perform Console Logging
behavior as it was last seen:
Perform Console Logging ::
handles logging per a unique Console Log Route for shared {*Log Message*} :
behavior {route, messages}
where messages is messages filtered by
[(message) => message(level) >= route(minimum level)]?
Log Messages :: for {messages}
whenever |messages| > 0,
foreach message in messages?
evaluate "Log: \(message)" as Console Message,
deactivate message.
If the idea of a route is considered as an abstraction, it can be reasoned that the only important concepts are the minimum log level and what to do when a log message should be logged for that specific type of route. The minimum log level is something shared by all routes, so it can be part of any abstraction. What to do cannot be captured directly by a context's abstraction though, as it isn't data, it is functionality, so it is what will be re-implemented in an extending behavior.
To enable the extending behavior, Perform Console Logging
can consider any message that it would have logged itself as a Valid Log Message
and evaluate it, in anticipation that an extending behavior will add an operation to perform its specific type of logging for any Valid Log Message
:
Perform Route Logging ::
handles logging per a unique Console Log Route for shared {*Log Message*} :
behavior {route, messages}
where messages is messages filtered by
[(message) => message(level) >= route(minimum level)]?
Valid Log Message :: Log Message : context;
Initiate Logging :: for {messages}
whenever |messages| > 0,
foreach message in messages?
evaluate message as Valid Log Message,
deactivate message.
Perform Console Logging
has been renamed to Perform Route Logging
to indicate how it should be more generalized to any route (and will be made so later). It now has an internal context, Valid Log Message
. Any message that has passed the behavior's filter and can be iterated in Initiate Logging
is aliased as a Valid Log Message
and evaluated.
An internal context is declared the same as any other context, but it is declared within the behavior, and it is only usable within the behavior and any extending behaviors. Any operation that depends on such a context can only be evaluated from within the behavior, since creating or aliasing any instances of that context is not possible from outside of the behavior.
Now the extending behavior can be implemented to perform the actual logging for a Valid Log Message
:
Perform Console Logging [Perform Route Logging] ::
logging per a unique Console Log Route for shared {*Log Message*}:
behavior {route, messages}?
Log :: log a Valid Log Message to the console : <message>?
evaluate "Log: \(message)" as Console Message.
Perform Console Logging
has been re-created as the console-specific functionality that extends the more generalized Perform Route Logging
. Most of the declaration is the same, but the identifier now uses the stand-in notation to signify that Perform Console Logging
stands-in for (with the purpose of extending) Perform Route Logging
. This specifies that if a behavior instance of Perform Route Logging
has qualified for this Console Log Route
and this {*Log Message*}
, then this Perform Console Logging
is valid and will add its functionality onto Perform Route Logging
.
Every new route context type can have an accompanying Perform "Route Context" Logging
similar to Perform Console Logging
to handle the log messages in its specific way, while the overall filtering and iteration of those messages is left to the abstraction.
Extending behaviors offers a great way to build upon abstractions or more generalized functionality in a conditional way. An extending behavior can also extend multiple behaviors with and
or or
clauses within the stand-in notation, which provides a way to supplement multiple abstractions with the same code or to link two ancestor behaviors together with new functionality.
Replacement
Where extending builds upon existing functionality with new functionality, replacement prevents the existing functionality from occurring, usually adding new functionality to take its place. This is useful for branching functionality for similar concepts in a conditional manner or for replacing functionality that is outside of the programmer's direct control.
For this example, consider the logging feature built so far as functionality offered by a third-party package that cannot be altered. It would be nice if logs were printed with the type of log preceding the message, instead of just Log:
, as is currently done. Unfortunately, the developer of the package did not implement it as such and does not offer a way to customize the messages. This sets the stage for a replacement to be implemented, such as:
Perform Level Specific Console Logging [Perform Route Logging] ::
logging per a unique Console Log Route for shared {*Log Message*}:
behavior {route, messages}
replaces Perform Console Logging?
Log :: log a Valid Log Message to the console : <message>.
Log Verbose [Log] ::
when message(level) = Log Level (Verbose)?
evaluate "Verbose: \(message)" as Console Message;
Log Debug [Log] ::
when message(Level) = Log Level (Debug)?
evaluate "Debug: \(message)" as Console Message;
Log Warning [Log] ::
when message(Level) = Log Level (Warning)?
evaluate "Warning: \(message)" as Console Message;
Log Error [Log] ::
when message(Level) = Log Level (Error)?
evaluate "Error: \(message)" as Console Message;
Log Unknown [Log] :: `In case a new level is added, at least log it.`
default?
evaluate "Unknown: \(message)" as Console Message.
With this replacement behavior, any time Perform Level Specific Console Logging
qualifies alongside a qualifying Perform Console Logging
, the Perform Console Logging
behavior will be ignored and only a Perform Level Specific Console Logging
behavior instance will be created. Its Log
operation will be performed the same as the Log
of Perform Console Logging
, except this new Log
operation is an operation hierarchy with operations that will handle each type of Log Level
to output the message with a specific prefix.
Just like extending behaviors, a replacement behavior is conditional. If desired, this replacement could have a qualification so that it only replaces Perform Console Logging
if some condition is met, perhaps a setting within Console Log Route
, or even some other context existing that Perform Console Logging
does not specify.
Operations can be replaced as well, with matching syntax and similar use. This can be useful within extending behaviors and for non-behavioral operations.
Last updated