Create a Forward-compatible API

Minimize Fixed Points

Problem and Solution Overview

This recipe just states how to execute a solution. Read our Legacy Newsletter blog post: DevOps #6 – Compile Independently With a Forward-compatible Component to understand the specific problem we are solving and the solution approach.

These recipes help you compile your component separately from the monolith in a way that minimizes design ripples across the component boundary.

First are the four recipes that isolate code from API changes:

Next are three common highly fixed code constructs and how to reduce their fixed points in an API.

Finally, three less-common antipatterns. I have included design summaries for these. If you need a recipe, ask on the Code by Refactoring Slack Community.

Isolate code from API changes

Fixed points in a design make code easier to reason about. Therefore, often both the client and the component will prefer higher-fixed-point designs internally. Only the API boundary itself should use low-fixed-point constructs.

The 4 isolation recipes allow you to keep a high-fixed-point construct on one side of the API boundary while changing the boundary itself to use more variable constructs. Each introduces an adapter that will contain any code needed to adapt the original construct to the more variable one.

The result is that the rest of the code on that side of the interface remains unchanged. It continues to use the simpler, more fixed design. All changes in the subsequent refactorings push compensating code into the adapter and no further.

In essence, the adapter operates as a change firewall.

Isolate client from API class changes

Use this if your library exposes a class that the client finds to be a useful abstraction. The client would like to keep the class abstraction, while we would like to reduce fixed points on the library API.

Steps:

  1. Split class, moving all methods and all data into the new class.
    • Do not have the new class maintain a back-reference to the existing class.
    • Name the new class Applesauce.
    • If your IDE does not have a safe Split Class refactoring, you can accomplish this by:
      1. Extract method the entire bodies of all public methods. Move the original methods to the top of the class definition.
      2. Create a new internal class named Applesauce.
      3. Copy the entire contents of the original class, except for the original public methods at the top. Include all constructors.
      4. Paste that as the body of Applesauce.
      5. Add a new field to the original class. Name it _impl and give it type Applesauce.
      6. In the original class, replace the entire body of each constructor with a single line that calls the same constructor on Applesauce and stores the instance as _impl.
      7. Delete everything in the original class except: the original public methods (which are now one-liners that won’t compile), the constructors (which are one-liners), and the _impl field.
      8. Update all the original public methods in the original class to call the corresponding method through the _impl field.
  2. Move the original class out of the component library and into the client.
    • This step will fail if the class was instantiated by the library. If that is true, then extract the code that instantiates the class and move that method to the client.
    • If you can’t extract and move that code, then you have lifetime control coupling and this approach will not work.
    • Fixing lifetime control is beyond the scope of these recipes. Ask for help on the Code by Refactoring Slack Channel if you need it.
  3. Rename Applesauce to have the same name as the original class (but in the component’s namespace).
  4. Commit.

Isolate component from API class changes

Use this if your library exposes a class that the library finds to be a useful abstraction internally.

Steps:

  1. Split class, moving all methods and all data into the new class. Do this exactly like when isolating the client from a class change.
  2. Copy the name for the original class, then Rename it to add the suffix _API.
    • For example, StoppedClock would become StoppedClock_API.
    • We give the sub-optimal name to the API class because your next recipe is going to transform the API class into something else. It will get a new name soon anyway.
  3. Rename Applesauce to have the same name as the original class.
  4. Commit.

Isolate client from API function changes

Use this if the client wants to keep using a function while the API is changing to a more variable calling construct.

Steps:

  1. Extract Method the entire body of the method. Name the new function Applesauce. Make it public.
  2. Move Method the outer method to the client codebase.
  3. Rename Applesauce to have the same name as the outer method, but in the component’s namespace.
  4. Commit.

Isolate component from API function changes

Use this if the library wants to keep implementation with a function while exposing a more variable calling construct.

Steps:

  1. Extract Method the entire body of the method. Name the new function Applesauce. Make it public.
  2. Copy the name of the original method, then Rename it to add the suffix _API.
    • For example, RemoveLetter would become RemoveLetter_API.
    • We give the sub-optimal name to the API function because your next recipe is going to transform the API function into something else. It will get a new name soon anyway.
  3. Rename Applesauce to have the same name as the outer method.
  4. Commit.

Fix antipattern: calling a method on an object

There are many code constructs with fewer fixed points than a method call on an object. This recipe arranges them in a sequence. Each step transforms from one construct to another, where the second has 1-2 fewer fixed points. Start at the step that matches your current code construct and continue as long as eliminating the next fixed point would be useful.

The sequence of code constructs is:

  1. Member method call
  2. Function call
  3. Event + callback
  4. Three Events
  5. Three Messages

Replace member method call with function call

Removes fixed points:

  • Coupling between method and other methods on the class. Change one method signature and you have to recompile code that uses others.

Steps:

  1. If desired, isolate client code from losing the object method we are about to eliminate.
  2. Extract Method on the entire method body. Make the new method internal and name it Applesauce.
  3. Convert Method to Static on the original method, adding the object as a new first parameter.
  4. Move Method the now-static, original method to the right place.
    • Make it a free function if your language supports them.
    • Otherwise put it on an API class that serves as a namespace — or even into its own single-method class.
  5. Rename Applesauce to have the same name as the original method.
  6. Commit.

Replace function call with event and promise

Removes fixed points:

  • Execution sequence coupling. The function no longer needs to run before the calling code continues.
  • Signature coupling. The function can now change names and even some variables without the caller changing.
  • 1:1 call count coupling. A single call can now execute zero to many functions.

Steps:

  1. If desired, Isolate the client from the function change.
  2. Isolate the component from the API function change.
    • You now have Foo_API which calls Foo.
  3. If you have not already, use TDD to create a ReturnToPromiseConverter.
    • Sample code is below.
  4. Pick a stable location to put the event. It will probably be a member of a class, so pick a class that rarely changes. Prefer a class without any public methods.
  5. Create a new event with the same signature as the existing function.
  6. Add a promise parameter to the event. This should be a promise for the return type of the original function.
  7. In the library set-up code, bind the event to a ReturnToPromiseConverter bound to Foo.
    • Write a test that the binding is set correctly.
    • You don’t need to execute the code to test the binding. just make sure the event’s handlers contain an instance and it is bound correctly.
  8. Commit.
  9. Write a temporary pinning test for Foo_API that ensures it calls the event.
    • This test will currently fail.
    • The test can simply bind the event to a handler on the test and make sure that handler is called with the right args.
  10. Change Foo_API‘s one line from a call to Foo to a call to the event.
    • The test will now pass.
    • Have Foo_API await the promise and return the result.
  11. Commit.
  12. Modify the existing test to provide and assert a return value. The test should pass.
  13. Modify the test to throw an exception in the handler and make sure you can catch it properly in the test. The test should pass.
  14. Inline Foo_API.
  15. Delete the temporary pinning test.
  16. Commit.
Sample ReturnToPromiseConverter
public class ReturnToPromiseConverter< TRet, T1, T2, ..., Tn >
{
  private Func< TRet, T1, T2, ..., Tn > _impl;
  public ReturnToPromiseConverter(Func< TRet, T1, T2, ..., Tn > impl) => _impl = impl;
  public void Call(T1 arg1, T2 arg2, ..., Tn argn, Promise< TRet > result)
  {
    try
    {
      result.Resolve(_impl(arg1, arg2, ..., argn));
    }
    catch(Exception e)
    {
      result.FailWith(e);
    }
  }
  public bool IsBoundTo(Func< TRet, T1, T2, ..., Tn> func) => _impl == func;
  public string ToString() => $"ReturnToPromiseConverter({ _impl })";
}

Note: the code currently behaves identically with its prior state. However, the API now allows variability that could break clients. Specifically, the client is not expecting the promise to resolve multiple times. However, the library could have multiple handlers bound to the event, and each could resolve the promise. This can easily result in undefined behavior.

Make sure you continue to 2 events if you want the library to return multiple values. Additionally, enhance your client code to handle the event independence. Those changes are outside the scope of these recipes.

Replace event and promise with 3 events

Removes fixed points:

  • Coupling between event handler and result generator. These can now be separate pieces of code in the component.
  • 1:1 relationship between requests and responses. The component can now call the outbound event whenever desired, irrespective of calls to the inbound event.
  • 1:1 relationship between responses and handling code. The client can now respond to results with multiple independent handlers.

Not a recipe:

This transformation cannot be done as a provable refactoring in the general case. Two aspects of the client code may require remodeling in order to work:

  • Additional local variables used from the call site when processing the result.
  • Handling errors and exceptions.

This step transforms the client’s execution sequence. A client calling an API with an event + promise can still be written sequentially. It is now async, but it can use async and await to appear linear. Because the library is still operating in a request / response model, the client can hold open the code that made the request, and then handle the response in that context. That allows it to, for example, propagate exceptions up the callstack from the code that makes the request and awaits the promise.

A multi-event system does not follow a request / response cycle. Although the component may currently only call some events after the client has called a particular event, that is a variable design point. The client must be updated to handle this variation point.

This means you will have to decompose the current client program flow into at least three completely independent flows, then add one new flow:

  • The flow that leads to firing an event for the component to handle.
  • The flow resulting when the component fires the event related to a normal condition.
  • The one or more flows resulting when the component fires the event related to exceptional conditions.
  • A new flow to act when the component never responds. What should the client do if it fires an event but the component never fires either a response or an error?

As a result, this part of the client will need to shift from a sequential program towards an event-based one.

General remodeling steps:

First prepare a dual-mode system.

  1. Find usages to locate all calls to the event. See if any of them have request / response interactions. Example interactions include:
    • The code that responds to a normal result uses variables from the scope of the function that made the request.
    • Any code up the call stack from a request combines request data with a result.
    • Any code up the call stack depends on timing, such as knowing that the component has completed execution.
  2. If you have any such interactions, then you will need to create a RequestInfo object. This is opaque to the component and allows the client to associate the response with a request’s context.
    • The RequestInfo can’t just contain a promise or other awaitable, because the response event might now be invoked 0 to many times with a given request info. Thus the result processing code must be re-entrant, and an awaiting stack frame is not.
    • You will use this with the various events. The original event takes it, the component passes it around, and any call to an outbound event (regular or exceptional) provides matching request info.
    • Update all callers for the original event to pass null as their RequestInfo, so they keep working.
    • Note that this will limit design changes in the library. It maintains a relationship between calls and responses, even if that relationship is now zero to many.
  3. Expose two new events from the library. One represents the normal result case and the other is the exception case.
    • Remember to include RequestInfo if your client code needs it.
  4. In the component, whenever you resolve the promise, add code to also fire the corresponding event.

Now switch one caller at a time from the event + promise mode to the 3 events mode.

  1. Pick one call to the original event. Find usages to locate one.
  2. Extract Method all code after the await resultPromise to the end of the function.
    • This should be a function that takes the result value and possibly other local variables.
  3. Use Create Parameter Object to collect all other local variables needed when processing the result.
  4. Move construction of the parameter object to before the call to the library, then pass it in as the request info. Continue using the local when processing the result.
  5. Test and commit.
  6. Bind the function that processes the normal result to the new event for a normal result. Use the request info and result from that event. Remove that function from the promise handler; the promise should now no-op when it receives a result.
  7. Test and commit.
  8. Repeat the sequence for error paths.
    • Explicit exception handling is easy to address: extract the body of catch statements and move them from being called by the promise failing to being called by the exceptional result event.
    • Implicit exception propagation up the stack is beyond the scope of this recipe. It is always hard and is sometimes impossible to make work.
  9. Commit.
  10. Add an error path for non-response.
    • This is new code. TDD it appropriately.
    • Consider how you would best detect a non-response in the case of this caller. Often it will be a timeout, but some contexts will call for other solutions.
  11. Commit.

Now remove the (unused) promise mode.

  1. Remove the promise from the original event’s arg list. This will cause all callers to stop compiling.
  2. Go to each and manually remove the promise. Make sure that you have already updated it to use the new events, which you can tell because the promise’s response will be a no-op.
    • If you missed any, then revert your removal of the promise and go back to the last step.
  3. Commit.

Replace 3 events with 3 messages

Removes fixed points:

  • Process coupling. Client and component may be in different processes or hosts.
  • Event name coupling. Client and component must only agree on message names. Adding new messages or will not cause issues. Even changing names will only result in the other side missing messages and executing the timeout path.
  • Event data coupling. Messages can contain arbitrary data. Receivers will ignore anything they don’t understand.

Steps:

  1. TDD a Message Bus wrapper, if you haven’t already.
    • It has two classes: dispatcher and receiver.
    • Dispatcher supports the same call interface as the events in your language.
    • Receiver supports the same subscribe API as events in your language.
    • Construction-time configuration with a message name & payload type.
    • Serializes the call to a message, which is JSON-serialized object with a name and payload.
  2. Replace each of the events with your message bus. Expose only the half (dispatch or receive) that clients use.

Fix antipattern: exposing a class for them to subclass and use

Common examples:

  • UI component that exposes a Button class and clients override OnClick.
  • Server component that exposes a server class and consumer overrides OnConnect and OnRequest.
  • Web service library that asks you to implement a subclass of some Controller and then define functions like OnGet and OnPost, or named methods like DomainSpecificAction that it then exposes.

Problematic fixed points:

  • Cross-method coupling. Any change to a method forces a recompile of all code that uses any method on the class, plus all subclasses.
  • Protected method coupling. All members designed to be reached from within the subclass become part of the public API. Changing them forces edits to the subclasses.
  • Data coupling. Changing data fields or their types will force recompiles and possibly edits.
  • Component coupling. Client code is intimately locked to your component. They can’t easily swap in a different component library. This prevents you from ever ending support.
  • Testing coupling. Client code cannot be verified separately from the component’s code. This introduces painful dependencies (usually UI or network).

Design considerations:

With this antipattern, the component library is using inheritance to allow the client to do two things:

  • Specify their API. Define what kinds of messages you want to support receiving.
  • Implement handlers. Define the code that should execute in response to each message.

Some component libraries that with inheritance-based APIs have a fixed set of events, such as a Button which supports only OnClick and Draw. In this case, specifying the API means allowing the client to choose which parts of the API to override, such as when most clients implement OnClick but accept the default implementation for Draw.

Several sensible API design options exist here, depending on the degree of variation you want to allow.

  • Use a class with events.
    • Clients instantiate the component class, then bind each event to their handler(s).
    • Works well when the possible set of message is fixed, such as for a UI control. Clients are just choosing which they want to receive.
    • Changes to your component class will force a recompile. They only force an edit of code binding an event that you change.
  • Use client-named messages and a clear calling protocol.
    • Client calls a subscribe method to match a message name + payload type + handler.
    • Works well with client-sourced messages, such as the React or Vue architectures.
    • One example protocol is pure functions over immutable state. Each message handler takes a previous state plus a payload and generates a new state.
    • Component library changes will only force a client change if you change the calling protocol. Be especially wary of exposing timing considerations, such as React’s lifecycle events.
  • Use a resource structure and client-named messages.
    • Client defines a resource structure, such as a UI component tree or a URL resource hierarchy.
    • Client calls a subscribe method to match a resource + message name + payload type + handler.
    • Works well when the component is managing a large system that can be divided into well-defined resources, and those resources are mostly independent in the client’s perspective.
    • This is generally used with user-sourced and component-sourced messages.
    • Usually has an implicit and extremely simple protocol. The most common is that the component will send messages at arbitrary times, and the client manages all state.
    • Provides almost total change isolation between client and component.
  • Use a resource structure and uniform messages.
    • Like resources + named messages, except that each resource exposes exactly the same set of messages. Client chooses which to support.
    • Works well when mapping to a standardized domain, such as HTTP.
    • Changes will only impact the client if an existing message changes.

Steps:

First we transform the client classes. We change them from inheriting our class to composing an instance of our class. This requires us to also transform virtual overrides into dispatches via events.

  1. Extract Method the entire body of each method in your base class.
    • Name all the wrapper methods with a _Wrap suffix and keep the original names for the inner methods.
    • Put all the _Wrap methods at the top of the class, followed by everything else.
  2. Search the code except the _Wrap methods for the text _Wrap. Inline any calls from a non-_Wrap method to a _Wrap method.
    • Do not inline the _Wrap method entirely; just inline the one call.
  3. Commit.
  4. Convert any virtual methods to non-virtual + abstract.
    1. Client code shouldn’t have to choose whether to call the base class implementation.
    2. Use a template method in the base class and as many abstract methods as needed for the places that subclasses override.
  5. Commit.
  6. Create a new BindHandlers method in the base class with an empty body. Call it from every base class constructor.
  7. Convert all abstract method calls to be through an event. For each abstract method:
    1. Create a new event. Use the same signature and name as the abstract method, but with the suffix _Event.
    2. In BindHandlers, bind the event to call the abstract method.
    3. Find usages of the abstract method in your base class. Update each to call the event instead.
    4. Commit.
  8. Split class in the base class to move all fields, all events, and all methods except _Wraps, abstracts, and BindHandlers to the new class.
    • The new class should not have a back-reference.
    • The outer class should have a field reference to the inner class. Name that field _impl. Set the field reference in BindHandlers.
    • Use the recipe in Isolate client from API class changes if your tool does not have a safe Split class refactoring.
    • Name the new class the original name, and update the outer class to have a _DeadBase suffix.
  9. Rename all the events to remove the _Event suffix.
  10. Commit.
  11. Inline all the _Wrap methods. The base class should now contain only constructors, virtuals, BindHandlers, and _impl.
  12. Push members down to push _impl and BindHandlers into all the subclasses.
    • Update construction to take the impl instance and call BindHandlers.
  13. Commit.
  14. Delete the abstract method declarations. Make the implementation methods no longer virtual.
    • This will fail to compile if you missed a step earlier. If so, revert, fix the problem, and try again.
  15. Commit.
  16. Delete the now-empty _DeadBase class.
  17. Remove the inheritance relationship from all subclasses. Follow the compiler errors to get them all.
  18. Commit.

Next update to the desired design.

  1. Clean up any data access problems.
    • Converting inheritance to composition often exposes unintended API surfaces. These may be methods or fields (properties count as fields).
    • Reduce the API surface as much as possible. This may involve moving data out of the component, perhaps into an Entity component.
    • See recipes in Extract Your Code So You Can Edit Independently.
  2. For each method, follow the recipe to reduce fixed points in a function call.
  3. For each exposed event, follow the same recipe to reduce fixed points until you meet your design’s target level.

Introduce a resource structure if needed.

  • Use TDD to create an API that the client can call to specify the resources.
  • Prefer dynamic configuration over static configuration.
    • Do not have the client define classes in order to make resources.
    • One option is to take a single config document that defines the entire resource graph. This makes it easy to comprehend the entire web of resources.
    • One option is a fluent API, where the client issues one call per resource or per subgraph. This allows decomposing the client program and extending it independently.
    • Prevent binding errors, such as a typo in the name of a reference while attempting to bind a message handler. This can be done with either the document or fluent approach.

Finally, standardize the method names if needed.

  • Use Rename refactorings to make the changes.
  • Implement an error-checking mechanism to ensure that all code uses only valid message names.
  • Commit after each name change.

Fix antipattern: exposing an interface for them to implement

Common examples:

  • Common language frameworks, such as IDisposable and IObservable.
  • Pattern-oriented libraries, such as a graph library that implements traversal and exposes interfaces to activate a node and to access related nodes.

Problematic fixed points:

  • Cross-method coupling. Any change to a method signature forces a recompile of all code that uses or implements any method on the interface.
  • Implement at once coupling. Clients cannot compose the functionality for the interface from bare functions or multiple unrelated objects. They must define the whole interface in one class or hierarchy.
  • Component coupling. Client code is intimately locked to your component. They can’t easily swap in a different component library. This prevents you from ever ending support.
  • Testing coupling. Client code cannot be verified separately from the component’s code. This introduces painful dependencies (usually UI or network).

Design considerations:

There are two options. The first is basically the same as the prior antipattern: exopsing a class for them to subclass. The second option is to use a typeclass.

Background – Interfaces vs Typeclasses

Most developers are so used to using interfaces that it can be hard to see the underlying problem they solve. Let’s explore the IDisposable interface and pattern as an example.

IDisposable is defined and used by a component that does garbage collection. Some objects need to take special action when they are deleted. The garbage collector needs to know which objects need special action, and then needs to tell them when to take that action. The garbage collection component describes this intention by defining the IDisposable interface.

The underlying problem is to allow two pieces of code to agree on a shared intention and protocol. One component defines what it needs. The other component exposes a class. We need to somehow define how that class can meet that intention.

Interfaces solve the problem if the creator of the second component is aware of the intention and designs for it. While this is the normal case for an interface defined by the language framework, it is a problematic constraint for independent components.

Typeclasses are designed to solve the same problem, but without either component knowing about the other. The typeclass is an adapter, written by a third party. One component defines the intention and the code that uses it. One component defines a class that could meet the intention – but isn’t aware of it. A third component wires the two together with adapter code that translates the intention into calls to the public API of the class.

Typeclasses are natively implemented in some languages and can be manually implemented in many others. Here are some common implementations:

  • Haskell: typeclasses (built-in).
  • C++: templates, especially with C++20’s concepts and constraints.
  • JavaScript / Python: duck typing.
  • Java / C#: Interface + Adapter class with composition.

Steps:

These vary by language. However, the general strategy is as follows:

  1. In the component that exports the interface definition, define and export an identical typeclass.
  2. Instantiate the typeclass to wrap the interface.
    • The two are identical, so the instantiation just makes direct calls.
    • Put this instantiation in the code that calls the API methods with classes from the other component.
  3. Commit.
  4. Define a parallel API in the component that uses the typeclass instead of the interface.
    • Create this via Extract method on the entire body & then Change Signature for each consuming method.
    • Have the interface-using method call the typeclass-using method.
    • At this point the component will be exposing both the Interface definition and the Typeclass definition, as well as a corresponding pair of methods for each API method that consumes the intention.
  5. Commit.
  6. Inline the interface-using API method.
  7. Commit.
  8. Move the interface from the current component to each component that defines the classes.
    • Each interface will have the same name but a different namespace.
    • Duplicate the typeclass declaration once for each interface.
  9. Commit.
  10. Ensure that the only code now using the interface is the typeclass instantiation.
    • Ignore any code in the component that defines the class. The only problem is code outside of that component.
  11. Commit.
  12. Duplicate the typeclass instantiation to create one per class that implements the interface. Remove the typeclass instantiation for the interface itself.
  13. Commit.
  14. Go through each typeclass instantiation. Inline every method call. Do not inline the method for all callers, just for the typeclass.
    • Commit after each successful Inline.
    • Any Inlined method may fail to compile because it uses non-public class members. If so, revert and then update the method’s implementation to use only public members.
  15. Make the interface internal to the component that defines the classes.
    • Delete it if its only remaining usage is in class implementation clauses.
  16. Commit.

Fix antipattern: exposing rich operations

Common examples:

  • SOAP web services.
  • UI frameworks that use Controllers.

Problematic fixed points:

  • Message names and payloads. This provides a strong pressure because clients may never update.
  • Business operations. Any change in business requirements forces a change in the interface code.

Design considerations:

Usually this arises because the design is treating its data as special cases. Each operation is richly defined to have a specific business intent, and then given customized data to meet that intent.

A good data abstraction can lead to a better design. For example, RESTful web services abstract the data by stating that everything operates on a resource and that any resource can be represented as a single object which can be serialized in a standard format. Then the only allowed operations are to create, read, update, or delete a resource. All the intelligence is encoded by defining the meaning of a resource.

A similar analysis of UI interactions can lead from MVC to the Vue architecture. Here the only operations are “create next state,” “generate UI from state,” and “replace UI with new UI.” The data abstraction is about application state. It is abstracted as a time-sequence of states. Each state is individually immutable, and can be generated from a prior state + a message. UI can be generated purely from state, and simply generates messages that can be applied to the same state that generated the UI.

If you want help finding a good resource abstraction for your case, reach out on the Code by Refactoring Slack Community.

Fix antipattern: consuming or producing whole values

Common examples:

  • API method arguments and return values are objects with one or both of:
    • Member methods
    • Static data values (fields or properties)

Problematic fixed points:

  • Type coupling to API function. Changing the data going in or out of the function requires changing the type’s members, which cascades edits and compiles broadly.
  • API coupling to non-related methods. Altering a member method passed into an API function forces a recompile of code that doesn’t use that member method.
  • Data coupling. Changing how data is structured or grouped requires updating both API method calls and implementations.

Design considerations:

Not all data is required for every operation. Nouns are useful within components, but not necessarily at boundaries. API entities should remain small and flexible, which usually means that they will not match problem domain-driven designs.

Large nouns are especially common in components that started by extraction from an OO system. Any good OO design will have strong nouns, and those nouns will be used in many operations. This creates a strong object graph. While the graph is good at conveying information, it also contains many fixed points that will cascade change across an API.

Consider breaking apart existing nouns.

  • Reduce graph size by removing references to other objects.
  • When methods only use half the data on an entity, split that entity up. This is very common with functions that started as member methods.
  • Convert whole values to Plain Old Data types – data structures that have no operations.
  • Identify optional data and convert it from fields or properties to a dictionary of name/value pairs.
    • Provide as many defaults as you can to minimize API coupling.
    • Consider isolating clients from this class change, so that the rest of the client code can use a type-safe object. That object maps its data to/from the dictionary.

Reach out on the Code by Refactoring Slack Community if you want recipes.

Fix antipattern: sharing mutable data across calls

Common examples:

  • Multiple API methods that use the same object.
  • APIs implemented as member functions on an object.
  • Mutable data structures. They could be cached or accessed asynchronously.

Problematic fixed points:

  • Timing coupling. Different parts of the system may make different assumptions about when data could change.
  • Cache coherency. It is unclear for how long a given data value is valid.

Design considerations:

Consider each of the following possible improvements.

  1. Replace reference semantics with value semantics
  2. Replace reference with immutable data
  3. Replace long-lifetime data with single-call data

Reach out on the Code by Refactoring Slack Community if you want recipes.