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.

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.

There are other ways that this could have been handled. Namely through a mapping of Log Level to String, where the String is the prefix. Then the mapping could be performed and used to prepend the correct prefix without this behavior being concerned about the specifics of the Log Level. That strategy would probably be preferable, either through a mapping accessible through an operator on the Log Message context or a standalone mapping that is purely responsible for that logic as it relates to Log Level.

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.

Last updated