This is the description, and experience report, of an exercise I used in a team I lead. I'll describe this in the present tense as I imagine applying the exercise in similar contexts.
Context
The team has been working on a Typescript monolith application; Typescript is used exclusively on the backend and compiled with Node.js as a target.
Encapsulation and modularization have been promoted as important software engineering concepts, and some high-level architecture and modules are emerging. For example:
- a clear write and a read side in a CQRS application
- a third-parties module to isolate from external dependencies
- a top-level evolving http layer to isolate the Domain Model and the rest of the code from HTTP and other delivery mechanisms.
The team is oriented to functional-programming, modeling some domain types but also keeping related functions close to those types for cohesion. Or they could be working within the object-oriented paradigm, similarly organizing the domain model and other code with types and classes.
For the rest of the discussion, the word module applies to units of encapsulation of varying sizes. In a TypeScript monolithic codebase, this could be a folder with a index.ts file; depending on its size, it could contain other sub-modules following the same structure. This exercise is also focusing on compile-time dependencies, mostly import statements.
The types folder refers to a (anti-)pattern where shared types are collected in a top level, or very high level, folder. The types can then too easily be imported anywhere, with a thick tree of dependencies towards the types folder being established, inadvertently coupling together disparate parts of the codebase.
Learning goal
The team is picking up encapsulation and information hiding at the small scale, encapsulating functions in a folder behind a index.ts and its export statements.
We want the team to expand this process to larger scale modules, sized at 1K to 10K lines of code: how much code can be hidden inside within these modules, or added to their public interfaces to elevate it as a contract between modules?
We also want the team to think inside a monolith as we are not sure there is enough capacity to address the overhead of separating repositories and services.
Activity
The original text of the exercises follows, edited for clarity:
Our src/types/ folder acts as a catch-all for types and related functions when we do not find an encapsulated place for them to be placed into. While our architecture is often changing for the better, over time these global types remain visible everywhere in the monolith's codebase even if hiding them could help working on the code that does not need to know about them.Some example of problematic types we found when running this exercise, with real named and an general explanation:
For each of the .ts files linked here, work as an ensemble to ask the following questions before deciding any refactoring move:
- Where are the contents of this file currently used?
- Are all the usages legitimate?
- Are these file contents addressing too many responsibilities?
- Do the names reflect the intended responsibilities or usage?
Once you decided on a new location for the file (or for part of it), ask:This is not a backlog to clear: do not rush to cover them all (there are 32 in total anyway). The goal is to identify patterns for architectural refinement.
- Do the resulting import rules agree with our architectural maps?
- If we change a decision within the newly encapsulated file, how far does the change propagate?
- RecordedEvaluation, Group: used only in module A and module B that depends on A. Yet globally visible.
- GroupId: used in multiple modules that relate to writing and reading to a database, yet visible to the HTTP layer.
- EvaluationType, ArticleServer: union types of a few strings, global for convenience but also only used in a few modules.
- CommandResult: a return type and part of the contract between module C and D. Module C depends on module D. Should this be defined by C, D, or a third module to break the dependency completely?
- DescriptionPath: a branded and validated string. The same problem could be solved by ensuring validation happens in the right modules. Should this type be simplified to a string?
A small guide to barrel files (index.ts files containing only export statements) can be useful. They are often mentioned as having performance implications, but if there are any measurable drawbacks they should be traded-off with maintenance.
dependency-cruiser can be used to help map out the current dependencies of a codebase, generally at a very fine-grained level rather than top-level modules.
Retrospective
A couple of questions can be asked here in a round robin, or for lack of time written as prompts on a digital board for participants to brainstorm about for a few minutes.
As often in quick retros, one question is looking backwards into what has happened and one is looking forwards to improve our practices:
- did you see a type which looked reasonable before today, but now is a target for refactoring?
- what would you be differently tomorrow when you create a new type?
No comments:
Post a Comment