Blog Infos
Author
Published
Topics
, , , ,
Published
Functional Replacement — Changing The Control Flow of Hard Structures

 

Have you ever wondered how many times it took a great logical approach to make a simple change in legacy code?

How many times have you spent hours thinking about how to change code that contains a callback or continuous execution structure?

How many times have you realized that in an old structure you cannot perform a simple test to continue your work?

Seeing this common problem in everyday life, the question arose:

“How can I create a pattern that helps me decouple the control flow from code that I cannot perform simple testing or maintenance on?”

What is Functional Replacement, after all?

It’s a pattern that involves the use of three behavioral interfaces and one functional interface to facilitate the alteration of the control flow of a class that we don’t want to continue in the system.

Here is a structural example.

UML:
UML — Control flow

 

Note, initially, that our client only knows the execution interface. It is responsible for executing our abstractions in the implementation and transforming the data needed for the new and scalable flow we are creating.

It is important to emphasize that the main structures are the interfaces of the pattern. They will define the flow of the implemented structures.

Now, let’s go through each of the execution stages in detail.

First stage: ClientFlow and ClientFlowTransformation

 

// Control interface
interface ClientFlow {
    fun executeFlow()
}

Control interface for execution.

The ClientFlow interface aims to define a method for the execution of structures that will be encapsulated within it’s abstraction.

class ClientFlowTransform(
    private val executionFlow: ExecutionFlow,
    private val emitterFlow: EmitterFlow
) : ClientFlow {

    override fun executeFlow() {
        executionFlow.contextFlow { data: Any ->
            // Transforms, creations, instances, updates, etc.
            emitterFlow.emitFlow(anotherObject)
        }
    }
}

Implementation of the interface for changing the control of operation.

During the implementation of the interface in the ClientFlowTransform class, it’s observed that the interfaces are invoked in the execution of the implemented method. The context of this class involves the transformation of the new object.

— It’s important to note that, the class where the implementation was done is just an example for where the transformation will stay.

As translated in the second stage, this object is purely data without logical transformations, since we want to make this behavior and transformation explicit and testable in a new runtime context.

By receiving the pure data in an immutable form through the lambda, we can, in a controlled manner, have an object that is secure in terms of mutability and predictable in relation to the tests that will be performed.

— As an ideal, I like to mention that the structure can contain more than one data source for this transformation within the lambda. These invocations can be performed through the inversion of control carried out in the class constructor. I always prefer to use interfaces so that I do not depend on concrete objects, making the testing simple to perform.

Good patterns for better structuring within this abstraction are the fundamental concepts of SOLID. Among them, the most important for this structure are the I (Interface Segregation Principle) and D (Dependency Inversion Principle), which clearly define the breaking of abstractions through well-defined interfaces.

Second stage: ExecutionFlow and LegacyExecutionFlow
// Functional interface
fun interface ContextScope {
    fun onContext(contextData: Any)
}

// Isolation interface
interface ExecutionFlow {
    fun contextFlow(scope: ContextScope)
}

Context interface

The second interface, ExecutionFlow, along with yourself functional interface (or lambdas/HOFs), is created to establish a controlled access point to the output of immutable data. The Any object represents a generic definition that can be any object the developer wants to represent in this context.

The mapping of abstraction will be carried out in the following implementation:

class LegacyExecutionFlow : ExecutionFlow {

    // Isolated, non-interpretable context.
    private val legacyContext = LegacyContext()

    // Transformed and emitted context.
    override fun contextFlow(scope: ContextScope) {
        /**
         * Creations, invocations, and definitions of 
         * data which you would like to pass to the 
         * new code execution structure.
         */
        val contextData = // Código final mapeado.    
        scope.onContext(newContext)
    }
}

Implementation of the context interface in the class considered of high complexity.

During the implementation of the interface in the legacy class or highly complex class, it should be evaluated which data, methods, and structures will be invoked. Ultimately, the goal is to define an object that contains immutable data.

And why without mutability?

We know due to the mutations that we have side effects on the code alghoritm result. So, when creating new ones, we should always be concerned with retaining and encapsulating mutations to clearly define when, how, and where they will occur!

Third stage: EmitterFlow and NewFeatureContext
// Receiving interface
interface EmitterFlow {
    fun emitFlow(data: Any)
}

Interface for receiving or outputting the data.

In the EmitterFlow interface, we concentrate on the emission or reception of data, ultimately representing our finalization. Through its implementation, we concentrate on the external connection points or new abstractions.

Implementation containing the structure and the output of information.

The output of implementation contains all of your new testable and scalable code, this dictates the end and continuation of the control flow of the structure. This way, we can fully define and control what we want from this point of the new implementation.

The SOLID principles guide us to define classes for individual responsibilities, so it’s perfectly acceptable to create new classes to handle output control.

If you have an external library or dependencies that you know need isolation because they are a detail of the code, don’t hesitate to isolate them through encapsulation. In the long run, it will be quite effective to be able to leverage the benefits of polymorphism to partially or completely alter the library.

This allows the versioning of code and the controlled distribution through interfaces.

class NewFeatureContext : EmitterFlow {
  
    // Reception and final processing method.
    override fun emitFlow(data: Any) {
        /**
         * Creation of the new algorithms that will be 
         * maintained and structured in a testable manner.
         */
    }

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

No results found.

interface ExecutionFlow {
    fun contextFlow(scope: ContextScope)
}

class ExecutionFlowFake(
    private val newContext: NewContext
) : ExecutionFlow {

    override fun contextFlow(scope: ContextScope) {
    scope.onContext(newContext.param)
    }
}
Tests

Finally, the tests. They are what will give you confidence in future modifications, so if you are building an abstract layer, we need to test it’s outcome!

Test structure:

As an example, let’s consider a data that invokes a toUpperCase method in its transformation. In the end, this abstraction will provide us with all characters in uppercase.

Fake structures:

The structures that are stubs or fakes work as our helpers. Today, we have various libraries that can manipulate and structure returns through mocking, but to fully grasp the power of the pattern, we will perform integration and testing manually.

Utilization:

In the example, we will modify the Any structures to real objects so that it is possible to visualize the execution flow clearly.

Execution:

In the execution structure, which is where the interface would be implemented in the legacy code, we have the definition of the ExecutionFlowFake class.

data class NewContext(
    val param: String
)

// Manual test structure
fun main() {
    val expected = "ANY"
    val newContext = NewContext("any")
    val fakeEnvironment = ExecutionFlowFake(newContext)
    val emitterFlowReceiverFake = EmitterFlowReceiverFake()
    val processState: ClientFlow = ClientFlowTransform(fakeEnvironment, emitterFlowReceiverFake)

    processState.executeFlow()

    val result = terminateEnvironmentFake.getLastMessage()

    println(expected == result) // Output: True
}

Simulation structure of the emitter from the legacy code.

In this structure, the data will be created externally to the class. This allows us to define the call manually, without the need for a connection with the legacy code. This allows the structure to be completely independent of its implementation.

It’s also possible to implement it directly; however, in the example, a concrete class was used to demonstrate pragmatically and directly that it works for any object-oriented language.

Reception or emission:

Moving on to the second phase of the test, a fake will also be created for the data output.

interface EmitterFlow {
    fun emitFlow(data: String)
}

class EmitterFlowReceiverFake : EmitterFlow {
    
    private var lastMessage = String()

    override fun emitFlow(data: String) {
    lastMessage = data
    }

    fun getLastMessage(): String = lastMessage

}

Implementation which receives the message.

It’s worth noting that in the test structures, which are not the real structures, we can create helpers to validate what was actually received by the class. In the example shown, the variable and method were created to allow validation of what was actually received by the fake implementation that was created.

Production structure:

In the production structure, we have the transformation, execution, and validation methods. In our example, we will only have a converter to transform the String into uppercase letters. Finally, its emission will be a plain String, without encapsulation in new data objects.

interface ClientFlow {
    fun createState()
}

class ClientFlowTransform(
    private val executionFlow: ExecutionFlow,
    private val emitterFlow: EmitterFlow
) : ClientFlow {

    override fun createState() {
        executionFlow.onScope { newContext: NewContext ->
        emitterFlow.emitFlow(newContext.param.uppercase())
    }
    }
}

Production structure

In this example, we created a simple structure, but in its real-world application, there will be various operators, and probably some methods will be involved before the emission.

Test execution

Returning to the test implementation, follow the execution logic, and it will be noticeable that everything is now controllable and easy to manipulate within the structure that interconnects the execution points.

data class NewContext(
    val param: String
)

// Manual test structure
fun main() {
    val expected = "ANY"
    val newContext = NewContext("any")
    val fakeEnvironment = ExecutionFlowFake(newContext)
    val emitterFlowReceiverFake = EmitterFlowReceiverFake()
    val processState: ClientFlow = ClientFlowTransform(fakeEnvironment, emitterFlowReceiverFake)

    processState.executeFlow()

    val result = terminateEnvironmentFake.getLastMessage()

    println(expected == result) // Output: True
}

Finally, we’re able to successfully perform our tests, and thus, we can continuously improve the structure of our code through a control pattern.

Final considerations

It’s noteworthy that we face daily challenges with old code, and careful implementation is necessary. It is common to have deadlines for development, and often there is limited time that does not allow us to make modifications to existing structures.

The principle of this pattern is to enable you, a software engineer, to create abstractions that over time allow for change. Your legacy structures won’t disappear quickly, but with small accumulated improvements in the medium and long term, results will come, and it will be simple through the interface structure to replace what was once difficult.

The pattern demonstrated here in the example is one way to implement it, and its limit is your imagination. After all, each context is unique!

Legacy code is present in almost all the software I have worked on, and I always try to study mechanisms on how to deal with it. Making the code more efficient for the team and everyone who will work in that context someday.

Create your abstractions and be an engineer who can work with various types of code.

I hope you enjoyed it! Gabriel Brasileiro.

https://github.com/GabrielBrasileiro?source=post_page—–d94753760492——————————–

https://www.linkedin.com/in/gabrielbrasileiro/?source=post_page—–d94753760492——————————–

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu