Solution to Design Challenge 003: Collection Pipeline & ACL
Welcome back to The Pragmatic Developer! In our previous issue, we explored the challenge of generating a context menu from a legacy API response. If you missed it, you can catch up here. Today, I'll share my approach to solving this challenge by refactoring the code for improved readability and maintainability.
Shout out to Pablo Alonso for his commitment of this series, and as always he shared his solution in gist. Overall I think that’s in the right direction to clean up the code, and used the pattern we’re going to discuss in this issue.
Using Collection Pipeline to Replace Looping Logic
Replace Loop with Collection Pipeline: This pattern involves transforming a collection of data step-by-step using operations like map
, filter
, and reduce
. This approach replaces traditional loops with a more declarative and readable style of data transformation.
Here’s how the refactored code looks:
const IssueContextMenu = ({ data }: { data: IssueResponse }) => {
return (
<DropdownMenu>
{data.operations
.map(convertOperationsToMenuItemData)
.filter(filterLegacyFields)
.map(convertMenuItemDataToUI)}
</DropdownMenu>
);
};
For a given operations list, we:
Map: Transform the data into a shape that we care about, trimming unused fields and renaming fields for the UI.
Filter: Remove legacy fields that don’t need to be rendered.
Map: Convert the list into UI components (
ButtonMenuItem
orLinkMenuItem
).
Each function in this pipeline is straightforward and easy to test.
Convert Operations to Menu Item Data
This function reshapes the item and ensures the URL is correctly overridden:
const URL_OVERRIDE_FIELDS: Record<string, string> = {
"comment-issue": "/issues/comment",
"assign-to-me": "/issues/assign-to-me",
};
const convertOperationsToMenuItemData = ({
name,
url: givenUrl,
internal_legacy_field: field,
}: Operation) => {
let url = givenUrl;
if (Object.hasOwn(URL_OVERRIDE_FIELDS, field)) {
url = URL_OVERRIDE_FIELDS[field];
}
return { name, url: url ?? "", field };
};
Filter Legacy Fields
Replace if-else
logic with Array.filter
:
const SKIP_FIELDS: string[] = ["log-work"];
const filterLegacyFields = ({ field }: { field: string }) =>
!SKIP_FIELDS.includes(field);
Convert Menu Item Data to UI
Convert the filtered data to UI components:
const DIALOG_FIELDS: string[] = ["assign-issue", "comment-issue"];
const convertMenuItemDataToUI = ({ name, url, field }: MenuItemData) => {
return DIALOG_FIELDS.includes(field) ? (
<ButtonMenuItem key={name} name={name} />
) : (
<LinkMenuItem key={name} name={name} url={url} />
);
};
Advanced Refactoring: Anti-Corruption Layer
Anti-Corruption Layer (ACL): This pattern helps manage interactions between systems with differing models, ensuring that legacy data formats do not "corrupt" the newer systems. It acts as a protective barrier that translates and adapts the data from the legacy system into a format suitable for the modern system.
I have discussed the pattern in my recent book React Anti-Patterns as well, with a few more detailed example in it.
Here’s an example using a class to encapsulate the logic:
class MenuItemModel {
constructor(operation: Operation) {}
get name() {}
get field() {}
get isLegacy() {}
get isButtonMenu() {}
}
const IssueContextMenu = ({ data }: { data: IssueResponse }) => {
return (
<DropdownMenu>
{data.operations
.map((operation) => new MenuItemModel(operation))
.filter((model) => !model.isLegacy)
.map((model) =>
model.isButtonMenu ? (
<ButtonMenuItem key={model.name} name={model.name} />
) : (
<LinkMenuItem key={model.name} name={model.name} url={model.url} />
),
)}
</DropdownMenu>
);
};
This approach makes the pipeline more readable and maintainable by encapsulating related logic within the MenuItemModel
class.
Bonus Section
I've also created a video demonstrating this refactoring step-by-step. Check it out if you prefer a video format, and don’t forget to subscribe to the channel for more design tips and code snippets.
If you have other ideas or patterns you'd like to see applied in real-life applications, or if you’re facing challenges in your projects and aren’t sure how to solve them elegantly, please let me know in the comments.
Conclusion
By refactoring the code using a collection pipeline and considering advanced patterns like the Anti-Corruption Layer, we’ve improved the readability, maintainability, and testability of our component. Stay tuned for the next issue, where we’ll tackle another interesting design challenge.