When we talk about "enterprise patterns", they mean patterns that are needed to adopt to reduce complexity across the code bases.
It is a philosophical and fundamental shift.
It is a diligent application of first principles.
"The biggest problem in the development and maintenance of large-scal software systems is complexity - large systems are hard to understand." - Ben Mosely, Peter Marks
The iron triangle of programming:
You need to ask yourself how to handle all of these things with the least code possible.
Think of a time where you checked in code and you were not 100% sure that it was not going to break anything.
If you've done that YOLO commit, you are slowly stepping into "purgatory". Then it begins to get worse.
If you've ever inherited a brownfield project and you have no idea what is happening, it is purgatory.
Complexity has a direct correlation to the level of purgatory that you are in.
This is incredibly dangerous. How can you possibly test something that relies on external state?
The example given is a shopping cart being used by multiple people sharing cart state. The ability to figure out why the shopping cart might have multiple things is hard to debug.
How are we communicating state and events in the context of our application from the component level (how are components are communicating with each other) to a massive, distributed level? How can you maintain consistency across those?
When you fail to manage state and control flow properly, your ability to reuse code is greatly diminished. This leads to duplication or increased code volume.
The example given to demonstrate the issue with shared mutable state:
class Inventory { ledger = { total: 1200 }; } class ItemsComponent { ledger: any; constructor(private inventory: Inventory) { this.ledger = inventory.ledger; } // ! ISSUE: mutates shared state add(x) { this.ledger.total += x; } } class WidgetsComponent { ledger: any; constructor(private inventory: Inventory) { this.ledger = inventory.ledger; } // ! ISSUE: mutates shared state add(x) { this.ledger.total += x; } }
Imagine an application now with 50 or 60 of these components. We are not hardwired as humans to be able to see this complexity at scale.
Complexity is broken down into 3 parts:
Questions to ask:
Answer no's above will lead towards what is referred to as the The Axis of Evil
.
"It is impossible to write good tests for bad code."
There is too much friction to writing tests. Why? More often than not, they're not writing testable code.
The ring-leader of the axis of evil is "hidden state".
class Whatever { reCalculateTotal(widget: Widget) { // the hidden state `this.mode` is the culprit and makes the function impure switch(this.mode){ case 'create': // ... break; case 'update': // ... break: // ... } } }
Hidden state violates the single-responsibility principle
. If you say a function does this AND ... you need to stop because you are violating the principle.
To fix the above, we can simply abstract to a parameter:
class Whatever { reCalculateTotal(widget: Widget, mode: string) { // now our function can be pure switch(mode){ case 'create': // ... break; case 'update': // ... break: // ... } } }
This is known as dependency injection.
Finally, the logic within the switch can be abstracted to a method/function for EACH case. That enables it to be far more testable and reusable.
After the abstraction, there was one method referred to as "air-traffic control" to handle the assignment of properties:
onCoursesUpdated(course, mode) { this.courses = this.updateCourses(this.courses, course, mode) this.totalCost = this.updateTotalCost(this.courses) }
You always want to move your code towards a state of being "fine-grained".