Sunday, May 12, 2024

Legacy Seam

Date:

Related stories

spot_imgspot_img


When working with a legacy system it is valuable to identify and create seams:
places where we can alter the behavior of the system without editing
source code. Once we’ve found a seam, we can use it to break dependencies to
simplify testing, insert probes to gain observability, and redirect program
flow to new modules as part of legacy displacement.

Micheal Feathers coined the term “seam” in the context of legacy systems in
his book Working Effectively with Legacy Code. His definition: “a seam is a place
where you can alter behavior in your program without editing in that place”
.

Here’s an example of where a seam would be handy. Imagine some code to
calculate the price of an order.

// TypeScript
export async function calculatePrice(order:Order)  new ShippingServices()
  

The function calculateShipping hits an external service, which is slow (and
expensive), so we don’t want to hit it when testing. Instead we want to
introduce a stub, so we can provide a canned and deterministic response for
each of the testing scenarios. Different tests may need different responses
from the function, but we can’t edit the code of
calculatePrice inside the test. This we need to introduce a seam around the call
to calculateShipping, something that will allow our test to
redirect the call to the stub.

One way to do this is to pass the function for
calculateShipping as a parameter

export async function calculatePrice(order:Order, shippingFn: (o:Order) => Promise<number>)  legacy_calculateShipping

A unit test for this function can then substitute a simple stub.

const shippingFn = async (o:Order) => 113
expect(await calculatePrice(sampleOrder, shippingFn)).toStrictEqual(153)

Each seam comes with an enabling point: “a place where you can
make the decision to use one behavior or another” [WELC]. Passing the function as
parameter opens up an enabling point in the caller of
calculateShipping.

This now makes testing a lot easier, we can put in different values of
shipping costs, and check that applyShippingDiscounts responds
correctly. Although we had to change the original source code to introduce the
seam, any further changes to that function don’t require us to alter that
code, the changes all occur in the enabling point, which lies in the test code.

Passing a function as a parameter isn’t the only way we can introduce a
seam. After all changing the signature of calculateShipping may
be fraught, and we may not want to thread the shipping function parameter
through the legacy call stack in the production code. In this case a lookup
may be a better approach, such as using a service locator.

export async function calculatePrice(order:Order) 
class ShippingServices {
  static #soleInstance: ShippingServices
  static init(arg?:ShippingServices) 
  static async calculateShipping(o:Order) 
  async calculateShipping(o:Order)   legacy_calculateShipping

  // ... more services

The locator allows us to override the behavior by defining a subclass.

class ShippingServicesStub extends ShippingServices {
  calculateShippingFn: typeof ShippingServices.calculateShipping =
     (o) => {throw new Error("no stub provided")}
  async calculateShipping(o:Order) {return this.calculateShippingFn(o)}
  // more services

We can then use an enabling point in our test

const stub = new ShippingServicesStub()
stub.calculateShippingFn = async (o:Order) => 113
ShippingServices.init(stub)
expect(await calculatePrice(sampleOrder)).toStrictEqual(153)

This kind of service locator is a classical object-oriented way to set up a
seam via function lookup, which I’m showing here to indicate the kind of
approach I might use in other languages, but I wouldn’t use this approach in
TypeScript or JavaScript. Instead I’d put something like this into a module.

export let calculateShipping = legacy_calculateShipping

export function reset_calculateShipping(fn?: typeof legacy_calculateShipping) {
  calculateShipping = fn || legacy_calculateShipping
}

We can then use the code in a test like this

const shippingFn = async (o:Order) => 113
reset_calculateShipping(shippingFn)
expect(await calculatePrice(sampleOrder)).toStrictEqual(153)

As the final example suggests, the best mechanism to use for a seam depends
very much on the language, available frameworks, and indeed the style of the
legacy system. Getting a legacy system under control means learning how to
introduce various seams into the code to provide the right kind of enabling
points while minimizing the disturbance to the legacy software. While a
function call is a simple example of introducing such seams, they can be much
more intricate in practice. A team can spend several months figuring out how
to introduce seams into a well-worn legacy system. The best mechanism for
adding seams to a legacy system may be different to what we’d do for similar
flexibility in a green field.

Feathers’s book focuses primarily on getting a legacy system under test, as
that is often the key to being able to work with it in a sane way. But seams
have more uses than that. Once we have a seam, we are in the position to place
probes into the legacy system, allowing us to increase the observability of
the system. We might want to monitor calls to calculateShipping,
figuring out how often we use it, and capturing its results for separate analysis.

But probably the most valuable use of seams is that they
allow us to migrate behavior away from the legacy.
A seam might redirect high-value customers to a different shipping calculator.
Effective legacy displacement is founded on introducing seams into the legacy
system, and using them to gradually move behavior into a more modern environment.

Seams are also something to think about as we write new software, after all
every new system will become legacy sooner or later. Much of my design advice
is about building software with appropriately placed seams, so we can easily test,
observe, and enhance it. If we write our software with testing in mind, we
tend to get a good set of seams, which is a reason why Test Driven Development is such a useful technique.



Source link

Latest stories

spot_img