Blog Infos
Author
Published
Topics
, , , ,
Published

www.gabrielbrasileiro.dev

When you start a new job or work with older or hard code, you may encounter poorly structured or unintelligible structures.

Legacy code is a common problem in companies with a long history. Rigid structures often emerge when teams work without a clear architecture or consistent patterns in the project.

So, what many engineers think about hard code?

Common team responses are:

  • We need to refactor this structure;
  • We need do a new structure;
  • We can’t evolve this class or method because we don’t know what it does;
  • In this god class only we can do is add code, never update or change!

As a software engineer, it’s essential to update code and develop new algorithms to evolve and enhance the software. Sticking with outdated code is not a good choice; eventually, you’ll need to remove it, along with all the associated classes or methods, often without fully understanding their functions.

Often, time to market doesn’t allow us to refactor or improve that class right away. It’s costly, and we need to be careful. So what can we do? How can we change this without adding more complexity?

After all we have a good ways to update these god classes with security!

If you’re looking for another pattern to help decouple and improve your code, check out the Functional Replacement Pattern. It’s a method for modifying the control flow of legacy code and rigid structures.

 

Humble Object:

This pattern is a good way to scale your code, testing and doing progressive changes in current hard structure. The pattern was born with the XUnitPatterns like one way to extract one “impossible dependency” to can test and improve code.

Simplified component diagram:

Humble Object Example

 

The default version only considers an object with an “impossible” dependency, such as asynchronous structures or UI components, assuming the class scope is hard to modify.

However, we will use this pattern for a different purpose: to start testing complex and hard structures.

Now, we need to decouple the new algorithm from the scope of the god object. How we can do this?

Example:
// Legacy/God class
class HumbleObject {
    
    // Legacy class strucutres...

    fun calculate(sum: Int) {
        val godClassProperty = getFirstValue()

        // Can be injected when creating the instance, if possible.
        val simpleSum = SimpleSum() 
        val result = simpleSum.getSum(godClassProperty, sum)

        // ...
    }

}

// SimpleSum.kt - The testable object
class SimpleSum {

    // Your new function
    fun getSum(a: Int, b: Int): Int {
        return a + b
    }

}

// HumbleObjectTest.kt
class HumbleObjectTest {

    @Test
    fun testSum() {
        val humbleObject = HumbleObject()
        val result = humbleObject.getSum(2, 3)
        assertEquals(5, result)
    }

}

 

This example is very simple but demonstrates how you can start testing the new algorithm. The structure hasn’t been fully tested yet, but we know the previous algorithm works, which is sufficient to avoid recommending a refactor to the product team and lose the time.

When working with legacy code, we need to be careful because the value of the product often lies within the algorithm itself. We don’t have room for mistakes!

In Android, it’s common for teams to lose control when using dependencies like Context or View. When these become large, updates become difficult, and many engineers see rebuilding as the only solution — which can be costly.

This set of techniques to improve the code without high costs I call Progressive Code Dismemberment.

Method Object

Sometimes we need a deeper level of analysis, and a simple method or structural class is not enough.

In those situations, the Method Object pattern becomes a strong option, because it allows us to choose which objects will be passed into it, and the instance exists only for the duration of that method execution.

This pattern is a technique described in the book Working Effectively with Legacy Code.

Class diagram:

Method Object Example

The class diagram gives us a generic view of the structure, but we will go through the steps in a concrete example.

Legacy or god Class example:
// Entities
data class Company(
    val id: String,
    val name: String
)

data class Invoice(
    val id: String,
    val amount: Double
)

data class Payment(
    val id: String,
    val value: Double
)

data class AccountingSummary(
    val totalInvoiced: Double,
    val totalPaid: Double,
    val totalOpen: Double
)

// Legacy/God class
class AccountingGodObject {

    // Hard global fields (legacy)
    private val repository = LegacyRepository()
    private val logger = LegacyLogger()

    // Legacy codes
    // ...

    private fun getInvoices(): List<Invoice> {
        // Algorithms to get invices
        
        return invoices
    }

    private fun getPayments(): List<Payment> {
        // Algorithms to get payments

        return paymens
    }

    // God method: too long, too many responsibilities
    fun calculateCompanyTotals(company: Company): AccountingSummary {
        var totalInvoiced = 0.0
        var totalPaid = 0.0
        var totalOpen = 0.0

        // Invoices loop
        getInvoices().forEach { invoice ->
            totalInvoiced += invoice.amount
        }

        // Payments loop
        getPayments().forEach { payment ->
            totalPaid += payment.value
        }

        // Compute open value
        totalOpen = totalInvoiced - totalPaid

        // Logs and database calls mixed into logic
        logger.log("Totals calculated for ${company.name}")

        repository.save(company.id, totalInvoiced, totalPaid, totalOpen)

        return AccountingSummary(
            totalInvoiced = totalInvoiced,
            totalPaid = totalPaid,
            totalOpen = totalOpen
        )
    }

    // Other methods
}

 

The method object:
// Method Object Class
class AccountingTotalsCalculator(
    private val invoices: List<Invoice>,
    private val payments: List<Payment>
) {

    fun calculate(): AccountingSummary {
        val totalInvoiced = sumInvoices()
        val totalPaid = sumPayments()

        return AccountingSummary(
            totalInvoiced = totalInvoiced,
            totalPaid = totalPaid,
            totalOpen = totalInvoiced - totalPaid
        )
    }

    private fun sumInvoices(): Double {
        var totalInvoiced = 0.0 

        invoices.forEach { invoice ->
            totalInvoiced += invoice.amount
        }
        
        return totalInvoiced
    }

    private fun sumPayments(): Double {
        var totalPaid = 0.0

        payments.forEach { payment ->
            totalPaid += payment.value
        }

        return totalPaid
    }
}

 

The god class updated:
// Legacy/God class improved
class AccountingGodLegacyObject {

    private val repository = LegacyRepository()
    private val logger = LegacyLogger()
    
    // Legacy codes
    // ...

    private fun getInvoices(): List<Invoice> {
        // Algorithms to get invices
        
        return invoices
    }

    private fun getPayments(): List<Payment> {
        // Algorithms to get payments

        return paymens
    }

    fun calculateCompanyTotals(company: Company, invoices: List<Invoice>, payments: List<Payment>): AccountingSummary {
        val summary = AccountingTotalsCalculator(invoices, payments).calculate()

        logger.log("Totals calculated for ${company.name}")
        repository.save(company.id, summary.totalInvoiced, summary.totalPaid, summary.totalOpen)

        return summary
    }

    // Other methods
}

 

Method Class Test:
class AccountingTotalsCalculatorTest {

    @Test
    fun shouldCorrectlySumInvoicesAndPayments() {
        // Given 
        val invoices = listOf(
            Invoice("I1", 100.0),
            Invoice("I2", 200.0)
        )
        val payments = listOf(
            Payment("P1", 50.0),
            Payment("P2", 80.0)
        )

        // When
        val result = AccountingTotalsCalculator(invoices, payments).calculate()

        // Then
        assertEquals(300.0, result.totalInvoiced)
        assertEquals(130.0, result.totalPaid)
        assertEquals(170.0, result.totalOpen)
    }
}

 

Now that the code is better organized, we can move forward.

When applying this pattern, it’s important to evaluate the dependencies you pass to the Method Object as parameters. The goal is to extract only what is necessary, keeping the constructor simple and focused.

If you pass objects that contain too much internal coupling, the new class may become harder to test, and later it may be difficult to move or reuse it in another package or module.

Adapter

And finally, the Adapter! I really like this pattern because it often lets us take a complex, hard-coupled instance and turn it into something simple — especially when it becomes injectable or directly consumable.

The concept is straightforward: we begin by defining the contract that the consumer will rely on, and we express it through an interface.

Simplified component diagram:

Adapter Example

Now let’s move on to a more difficult code structure, commonly found in legacy projects without architecture or design patterns.

In this example, I’m demonstrating a common case of a legacy or god class.

Legacy or god class:
// LegacyGodDatabase.kt
object LegacyGodDatabase {

    // Simulating a static legacy database
    private val database = Database.getInstance()

    // Complex function: returns a raw Triple<> and performs mixed logic
    fun fetchAccountDataById(accountId: Int): Triple<String, Double, String>? {
        println("Legacy DB → Running raw query for accountId=$accountId with static context")

        val result = database[accountId] ?: return null
        val adjustedBalance = if (result.second < 0) 0.0 else "%.2f".format(result.second).toDouble()
        val normalizedValue = if (result.third == "USD") adjustedBalance * 5.15 else adjustedBalance

        return result.copy(second = normalizedValue)
    }

    // Other functions
}

 

A deeply coupled structure is usually hard to consume. The main issue is that we often cannot modify the legacy class immediately, either due to time constraints or product priorities. So the question becomes: how can we make the consuming code easier to test and prepare it to eventually replace the hard class when a new version is available?

Use the Adapter!

The adapter interface:
// Entity
data class AccountData(
    val owner: String,
    val balance: Double,
    val currency: String,
)

// AccountAdapter.kt
interface AccountAdapter {
    fun getAccount(id: Int): AccountData?
}

 

If you’re using Clean or Hexagonal Architecture, consider defining the adapter interface in the domain layer and consuming it through a use case. This allows the use case to remain structured, testable, and safe for engineers to work with, while the legacy class can continue to be maintained or refactored independently.

The adapter implementation:
// Implementation
class LegacyAccountAdapter : AccountAdapter {
    override fun getAccount(id: Int): AccountData? {
        val legacyResult = LegacyGodDatabase.fetchAccountDataById(id) ?: return null

        // Mapping raw Triple to clean model
        val (name, balance, currency) = legacyResult
        return AccountData(
            owner = name,
            balance = balance,
            currency = currency
        )
    }
}

 

Here we are isolating the remnants of the legacy class.

In most cases like this, the adapter implementation itself won’t be testable, but the real benefit is that the consumer becomes fully testable. Later, with more time, you can replace the legacy implementation with a properly structured class without affecting the consumer.

And to finish our consumer:

// Implementation
class LegacyAccountAdapter : AccountAdapter {
    override fun getAccount(id: Int): AccountData? {
        val legacyResult = LegacyGodDatabase.fetchAccountDataById(id) ?: return null

        // Mapping raw Triple to clean model
        val (name, balance, currency) = legacyResult
        return AccountData(
            owner = name,
            balance = balance,
            currency = currency
        )
    }
}

 

Legacy code is hard to work with, but with a bit of imagination, we can work some magic to decouple its structure.

We cannot avoid working with legacy code. Sometimes code evolution cannot happen quickly, but we can decouple it in the future.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

Final Overview of the Refactoring Patterns:

Rumble Object: Used for simple and newly created structures that can be consumed directly.

Method Object: Suitable for more complex cases where an algorithm needs to be executed using legacy data.

Adapter: Applied when introducing new contracts or abstraction layers, allowing legacy code or code planned for deprecation to remain isolated.

If you would like to see other articles and experiments access my posts.

Feedbacks, notes and improvements are appreciated.
Gabriel Brasileiro.

This article was previously published on proandroiddev.com

Menu