Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I added my comments to the guidelines (only). #2

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions river-guideliness.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
# The River: Core Guidelines for Software Development

> I mark my insertions and comments as quotes. Which is unfortunately a somewhat inverse usage of the mark up.
This is not meant as a start of a discussion, but as feedback on the text. I avoided questions. Do with this feedback as you wish: Ignore it, think about it, be convinced by it, understand it as being out of scope. It is up to you. Your essay and these guidelines clearly show, that you not only have thought about this in depth, but also invested a lot of effort into expressing your thoughts.

## Fundamental Principles

### 1. Flow-Based Organization
Code should be organized around complete flows of functionality rather than arbitrary technical boundaries. Each significant feature should be treated as its own "river", a coherent path from beginning to end. The goal is to maintain locality, visibility and comprehensibility of the entire process.

> If those technical boundaries are indeed *arbitrary* I fully agree. Often they are required by the techniques used. If those are strictly necessary to achieve the goal is another discussion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Examples are requirements like

  • test-ability (might require dependency injection to get a mock where it is required.
  • Extensability (Others should be able to extend/adapt the flow at runtime)
  • Consistency across flows (10 flows should behave similar)

### 2. Complete Responsibility
Functions should aim to handle their entire responsibility from start to finish, regardless of length. Rather than fragmenting logic across multiple small functions, allow each function to tell its complete story. The key metric is not size but clarity, isolation and coherence of flow.

> Fully agree

### 3. Natural Boundaries
Systems should be divided along natural feature boundaries, like a watershed contains multiple rivers. Each feature flow should maintain its independence while connecting with others at well-defined points. These boundaries should emerge from the problem domain rather than being imposed by technical considerations.

> Feature flow is just one aspect on how do decompose/structure/model a complex system. Computer Science has to deal with the challenge to adhere to multiple decompositions at once. That is why aspect-oriented programming became a thing and why we move elements (realization of concepts) from code to annotations.

> You speak of *well-defined points* of interactions. Research will have to spend time, and experience will show what make a point *well-defined* with regard to feature flow.

> I agree that staying close to the problem domain is a favorable goal. I believe object orientation started to address that challenge. I rather see `Student` as a class name than `IMainDBObject`.
One kind of abstraction (My [professor](https://www.se-rwth.de/staff/Manfred.Nagl/) identified three kinds, which I no longer remember) leads from the problem domain (very abstract) to the computer implementation (less abstract). Once the implementation is on those less abstract layers the line between the modeled problem domain and the computer domain blurs.

## Implementation Guidelines

### Code Structure
Expand All @@ -20,28 +34,58 @@ Systems should be divided along natural feature boundaries, like a watershed con
4. Use clear section comments to guide readers through longer flows
5. Keep feature flows independent of each other where possible

> If *flow of a feature* is the only or main principle of structure, I fully agree. But, as I hopefully have commented in the main essay, I believe other principles are needed as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe your reply on this revers to the River-Guidelines yourself and that my comment means that is it not yet complete and should be extended.

My comment addresses something else. The whole "The River" approach assumes a certain main principle of structure. If I draw the program it will always be a flow-chart. My experience differs. In some applications the flow-chart is a good choice and the The-River is a good principle. It then makes sense to follow your guidelines.

But on other occasions other models are better suited to solve the problem. I have yet to elaborate on that and what other structures I mean.

### Abstraction Rules
1. Prefer local abstractions that serve immediate context over global utilities
2. Allow patterns to emerge naturally rather than creating abstractions upfront

> This is so true! I saw over-engineered code which added interfaces to every class or using a visitor pattern with only one visitor and one very concrete collection to traverse.

3. Abstract only when it genuinely improves understanding, performance or maintainability. And do it only after reflecting on the potential cost vs benefit of doing so.

> I believe the before mentioned visitor pattern was included to *improve* readability. If you are best friends with certain patterns and all other team members are as well, then using such patterns might indeed improve readability. At the time, at least one team member (me) found it way harder to understand.

4. Keep abstractions close to where they're used, preserving context
5. Accept similar patterns in different contexts rather than forcing reuse

> *Forcing* reuse does not make sense, agreed. If you scale the complexity of the problem domain, you will encounter the true *need* for reuse, because you will have to check 99 flows, if they also contain the bug you just fixed in 1 of 100 flows (I am aware that there are draw-backs on the reuse which might be harder to handle).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I accept your reply.

But, take a change in requirement. The resolution of the screen in use will change and those flows do contain actual pixel numbers (which could have been avoided in the first place, but have not). One flow contains: First column (100px) second column (150px) third column (remaining). When 500 pixels were available this is just fine. Now 1000 pixels are available. and I have to double those numbers.
Other flows handled the usage of the availble screen estate in their own independant way.

Contra Flow: How lovely it would have been if they all used a single shared parameterized screen, so I only had to change that one screen.

Pro Flow: How aweful it would have been if I had only a single shared parameterized screen to handle the requirements of 100 different flows. No one will understand the resulting parameters and any change to the screen might break many flows.

I had to deal with such a change for the Porsche Workshoptester.

### Error Handling
1. Handle errors within the flow where they occur

> In my experience error handling is hard. This is partially due to functional decomposition and abstraction, but will remain true if I have to handle an error after the river split up; if I have to pass the error over through a "well-defined point of connection" between flows.

2. Provide rich context in error messages by leveraging local knowledge
3. Design flows to contain and isolate failures
4. Make error states and recovery paths visible within the main flow

> I think the last two points have not been shown/discussed in the essay. If we were to discuss them, examples of what that means for you were required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean how to design flows to contain and isolate failiurs and making
error states and recovery paths has not been shown/discussed in the essay well
enough?

yes

### Cross-Cutting Concerns
1. Treat logging, security, and monitoring as part of the local environment

> This might hide the flow. How do you see/mitigate that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean: The flow in one of your examples is:

    // Parse the request
    // Validate the parameters
    // Query the database
    // Update the database
    // Construct the response

If you add logging, assertions, more checks for security and monitoring additions all this cross cutting concers will clutter up the function and hide the flow. I might not see the actual parsing in the code, because most of the code after the // Parse the request will no longer try to parse the request, but handle those other concerns.

2. Allow each flow to handle cross-cutting concerns in context-appropriate ways

> Nice, but I have *no* idea how to achieve that.

3. Accept that similar patterns may repeat across different flows
4. Focus on clarity and completeness within each flow rather than global consistency

> Agreed, but: Global consistency can in turn increase readability across team members and thus maintainability. Following some idioms is ok.
If global consistency is required (e.g. You have to do C before A before B) to make things work, maintenance becomes hard (because people will "forget" to do C or A). Avoiding such global consistency completely, might impose abstraction (I hide doing C and A, from whoever does B) and might obscure the flow. Where to settle for compromise between those extremes is a matter of actual situation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you did not understand my comment. Or I did not understand your reply. Let me try to explain again.

To make things work I have to do C before A before B. If I understand the flow-principle I will have multiple functions which read:

r=C():
if A(r) == failure {
   handleError();
B();

Maybe even with some flow specific stuff inbetween.
How do I ensure across a team or across myself over time (future-me might no longer know what current-me knows) that we always do C before A before B?

I could introduce a functional abstraction: A function that does the above, which I call in every flow. Problem solved.

But I have broken other flow-principles. The error handler is now further away from the context that it aims to report. I no longer have the freedom to move flow specific stuff inbetween those calls to A or B.

If it is already a well known fact to everyone on the team that of course you have to C(); A(); B(); then I might just live with the risk and call that an idiom that I might even put in some documentation or specific coding guideline somewhere.

### Growth and Scaling
1. Add new capabilities as new rivers rather than complicating existing ones

> Good and important point. I might otherwise believe to follow the above guidelines by adding some if-statements to an existing flow.

2. Maintain constant complexity by isolating it within each flow while the system grows

> I am not sure I understand: Does it mean all the statements in flow should be on the same level of abstraction, e.g. the problem domain?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should not need any
other part of the system (hopefully) to deliver the feature it is designed for.

What would I see in a function which does need other parts of the system?

I find it hard to distinguish good from bad functions by that criteria.

3. Allow each flow to evolve independently based on its specific needs
4. Focus on isolation and fault containment between flows

Expand All @@ -58,22 +102,41 @@ Systems should be divided along natural feature boundaries, like a watershed con
1. Can you understand the complete flow without jumping between files?
2. Are abstractions serving the immediate context or forcing premature generalization?
3. Is similar code truly duplicate, or serving different contexts?

> Rather: Do unified code segments server readability or just avoid code duplication?
It seems to me you assume, that the reviewer found code duplication and likes to criticize it.

4. Are boundaries between flows natural and appropriate?
5. Does the code tell a clear, coherent story?

## Anti-Patterns to Avoid

1. Breaking flows into small functions - unless with clear and worthwhile benefits
2. Creating global abstractions - unless absolutly necessary
3. Forcing reuse between unrelated contexts - unless absolutly needed
2. Creating global abstractions - unless absolutely necessary

> I propose: Creating global abstractions - unless for a concrete, well known cause.

3. Forcing reuse between unrelated contexts - unless absolutely needed

> I propose: Forcing reuse between unrelated contexts.
I wouldn't know when reuse between *unrelated* contexts would be *absolutely* needed. I guess never.
As these Anti-Pattern should only be avoided and are not strictly forbidden, I don't think an exception is required.

4. Obscuring flow with unnecessary indirection - unless with clear benefit

> I propose: Obscuring flow with indirection - unless with clear benefit
A clear benefit makes the indirection necessary and the Anti-Pattern would no longer apply.

5. Separating related functionality for the sake of "clean code" - is always bad

## Decision Framework

When organizing code, ask:
1. Does this organization make the flow more or less visible?
2. Does this abstraction serve the immediate context or is its benefits so large it is warranted?

> If my problem is realizing a flow, that is a valid question. If the flow is not the dominant problem, this question does not help.

2. Does this abstraction serve the immediate context or are its benefits so large it is warranted?
3. Is this repetition isolated or truly harmful?
4. Are these boundaries emerging from the domain or being forced?
5. Does this structure help or hinder understanding the complete story?
Expand Down