Isolate Component Integration Complexity

Create a Port and Adapter

Problem and Solution Overview

This recipe just states how to execute a solution. Read our Legacy Newsletter blog post: DevOps #7 – Isolate Integration Complexity to understand the specific problem we are solving and the solution approach.

These recipes help you isolate the complexity involved in integrating with your component, even if your component uses high variable-point designs.

There are two recipes. Choose which to use based on whether you extracted your component from this client or not.

Extract and simplify your Port and Adapter

Good news: the client already contains code that maps from the highly fixed structures used in its main code to the highly variable structures used in your component’s API. We just need to collect them then find the Port concept to simplify them. We’ll write tests along the way.

Phase 1: Collect variability adaptations

Steps:

  1. Create a new namespace to hold the port & adapter. For now, name it the same as your component.
  2. Do the rest of this recipe for each usage of each API member (method, event, class, or message).
  3. Navigate up the call stack until you find code that is unrelated to your component.
    • Err on the side of including (skipping past) code that is tangentially related. For example, don’t stop at code that calculates data for your component or a result based on information coming from your component.
    • Stop at any code that is clearly unrelated. For example, if the code responds to data from your component by calling main_window.draw(), include the call to the draw method but don’t include the draw method itself.
    • This should include all the code that adapts from the highly variable design to the highly fixed design. If not, refactor to isolate the variable design from the code unrelated to your component.
  4.  Isolate this code.
    1. If it is a small chunk of a method, Extract Method it.
    2. If it includes several methods on a class, Split Class.
    3. If it is a pure function or whole class, move it to the adapter namespace.
    4. Move any existing tests with it.
  5. Commit.
  6. Remove duplication. Only consider strict duplication. Now is not the time to generalize.
    1. Extracting both parts as methods.
    2. Refactoring using provable refactorings until they are identical — or you can’t make them absolutely identical.
    3. Use a diff tool to verify they are character-for-character identical except for the method name.
    4. Merge the calls to both call one method.
    5. Delete the now-unused method.
  7. Commit.
  8. Add missing tests.
    • Use mock-free unit tests. If your code requires mocks to isolate it, then refactor instead.
    • Don’t worry about verifying the Adapter end-to-end. Verify each piece.
  9. Commit.

Phase 2: Consolidate and narrow integration surface

Steps:

  1. Identify any parts of the integration surface that are called only a couple of times. Pick one.
  2. Does this part have any near duplicate? Example near-duplicates include:
    • Another part of the adapter that has similar name and data.
    • Another part of the adapter that calls the same component API part.
    • Another part of the adapter that either calls or is called by this code.
  3. If so, use Extract Method and Merge to merge the duplicate parts.
  4. Identify distinct non-duplicate parts that are left. Isolate them and try to refactor them away from the duplicate parts.
    • One technique is Extract Method and then Introduce Parameter (either a function-valued variable or an interface that has the method + two implementations).
  5. Consider whether each non-duplicate part should be inside the adapter or the client’s main codebase. Move it accordingly.

Phase 3: Define Port

Look at the remaining integration surface. Try to find an abstraction or metaphor. Your goal is to find the way that the client thinks of job done by the component – not the way the component author thinks of the problem.

Common examples include:

  • Source of commands/events: a thing that tells the client what to do when.
  • Information sink: a place to report some kind of information that the client computes.
  • Domain part: something relevant to the domain, such as “purchase a cart” or “price an equity”.
  • Store & Retrieve: a place for the client to put objects / data and get them back later.

Each of these examples is too abstract. Narrow it correctly for your specific code and domain.

For example, a UI with a human behind it may be both a source of commands and an information sink, while a batch API may be just a source of commands. But both the UI and the batch API may send the same commands – such as transfers between accounts. In that case, I might create 2 ports:

  • AccountTransferCommandSource – something which can tell the client to execute transfer actions. Client has 2 adapters to this port – one implementing it using the UI, the other using the batch process.
  • AccountVisualization – an info sink for all the info related to accounts, including history. Client likely has many adapters for this, including the UI – and probably several user reports, management reports, and governance / oversight calculations.

Once you have found the right Port(s) for your client + component pair, then rename classes and methods on your integration surface to make it obvious. You may also need to merge classes or move methods around – whatever it takes to make code that uses the Port become shockingly obvious.

Reach out on the Code by Refactoring Slack Community if you are struggling to come up with a good metaphor and names for your problem.

Phase 4: Remove unwanted complexities

The Port definition will often work for most methods and most parameters, but have some things that don’t fit. These are almost always due to complexities the client would rather ignore.

For example, the AccountVisualization port might have the following:

  • Methods that throw timeout exceptions when a receiving process becomes unavailable.
  • An incoming message related to configuration – stating what the receiver wishes to receive and when.
  • Authentication and authorization.

None of these fit the narrowest-possible definition of an AccountVisualization. They can all be bent a little in order to fit, but they don’t fit as naturally as, say, the RequestTransactionHistory method.

These can be very easy to spot if you have multiple implementations for the same Port, because they often differ between implementation. Often their names also use a different domain / metaphor (clock, wallet, cart), a computer science term (cache, exception, draw, sink), or a meaning-free word (manager, utils, handler).

Do one of the following with each complexity:

  • Move it inside the Port. Do this if it is a complexity of this particular component, which the client would rather ignore. Examples include exceptions, sequencing concerns, or unused variability options.
  • Move it outside the Port. Do this if it is more related to a different responsibility. This might be a second responsibility of the same component if the client would like to treat it separately – such as the two parts of a human UI. Examples include data transformations between two components and helper computations.
  • Emphasize it. Do this if the client cares about the complexity, it is related to the component, and it just doesn’t fit the metaphor. Since it won’t fit naturally, make it explicitly intentional. This is rare and each one is a special case.

Reach out on the Code by Refactoring Slack Community if you need help with any of these transformations.

Define your Port then TDD an Adapter

You will use this recipe only when integrating your component to a second client.

Option 1: Copy and modify a Port used by a different client

The second client to use a component, however, may need to TDD a Port into existence. I recommend that you start by examining the Port used by prior clients. If any of those metaphors make sense for your client, then copy that implementation and tweak from there. The main challenge is to delete things you don’t need.

Steps:

  1. Copy their entire implementation, including tests, into your codebase.
  2. Commit.
  3. Hook up at least 1-2 usages from your client and make sure it is easy to use.
  4. Commit.
  5. Delete anything from the Port that you don’t see a clear usage for in your client right now. Also delete the tests & Adapter.
    1. If there are any that you think you probably will need, keep them for now.
  6. Commit.
  7. Now delete one thing that you think you will probably need. Delete Port, tests, and Adapter in the same commit.
  8. Commit.
  9. Repeat step 7 until you have gotten rid of them all and are down to only what you are currently using.

You can always go back and recover any deleted capabilities from source control if your client needs them in the future.

Option 2: TDD a new Port

Use this when your client uses the component in a different way from that used by prior clients. The key is to create the Port first and incrementally. Use the port in the client code and make it as simple as the client wishes the world worked. Don’t implement any Adapter until your client is using the Port in a way that feels easy and right. Now TDD the Adapter implementation without making any changes to the Port.

You may need to use Mocks to test your code that uses the Port. That’s OK for now. A future newsletter will show a better way, using contract testing plus simulators.

Ask on the Code by Refactoring Slack Community if you need help creating a Port and Adapter from scratch.