I frequently have relatively contained components (services) that do specialized things. These are also almost always immutable to make changing them less error prone. Note the below is within one relatively big project, not small services in separate processes, but that doesn't change much.
Take this system as an example:
- Input: cardboard package that contains red books
- Take a book
- Do some complex processing such as:
- If book has less than 50 pages, reject with message "Too short"
- Otherwise, read pages 15, 21 and 38
- Find all words that start with X on pages 15 and 21
- If we have more X-words on page 15, find the number of occurrences of the word "play" on page 38, otherwise read the last word on that page. Make that be A
- Write a new book where you change all the words "some" to A. If there are no words "some" at all, issue a warning message "No matching words found"
- Print it and color the covers blue
- Output: plastic package that now contains blue books and various rejection or warning messages as needed
Consider that we now have a lot of services that perform things in the 2.2 part above. E.g. there would be a service that writes a book in step 2.2.5.
Consider also that the rejection or warning messages are to be propagated across all services. In the above simple case that might not sound as much, but consider that this is usually much deeper than the above example. E.g. we might have something like:
[one thread] book service -> chapter service --> title service --> paragraph service ---> sentence service ----> word service --> footnote service [another thread] printing service -> font chooser service -> cover coloring service
This means that either we keep global state (yuck) or create gazillions of separate objects (SentenceServiceOutput, PrintingServiceOutput, ...) and make each of the services return all pieces, i.e. write book would return
(book, reject messages, warning messages) tuple and then wire them as needed in some parent service.
The 2nd solution has some drawbacks in that it:
- Pollutes the code significantly and make it very hard to read and trace through. E.g. instead of a service (2.2.3 above) that returns a list of words found, it would return an object
([words], [reject message], [warning message])
- Makes the code significantly longer and more convoluted - basically wherever we had
[words]we now have to unpack
([words], [reject message], [warning message]), change a small piece and then re-pack into a new
([words], [reject message], [warning message]).
- All this makes it significantly harder to keep the overall project structure in your head, since the number of names (e.g. class names) is increased dramatically and due to their number names become longer (you cannot call it just
Output, since you'll mix
FontChoosingServiceOutput), making the code even longer and harder to read
- While it still makes it significantly easier to reason about smaller pieces than using global state, makes the code changes significantly slower and harder to audit, test & such
Are there any patterns which allow for mitigation of any or all of the above issues?