👇

3.4. What you need, where you need it (WYNWYN): Context Dependence and Code Connectedness

The main rule I've adopted for coding is to write:
 
What you need, where you need it (WYNWYN)
 
Perhaps the main question this principle answers is: should you create reusable code or not?
 
If following WYNWYN, in 90% of cases you should write code that solves the problem right where it arose, and only later consider whether it needs to be extracted for reuse elsewhere.
 
And now I'll prove why WYNWYN works.

"Nobody understands DRY" or "Context Dependence"

First, few people understand "Don't Repeat Yourself".
Most developers interpret this rule as "there should be no repeating code".
Therefore, as soon as an inexperienced developer sees some code repeating (that is, 3-5 lines written identically in 2 different places), they immediately try to extract them into a separate method / function / layer / etc.
But the most important thing they forget:
The same code in different contexts is different code.
And this completely changes the meaning of DRY. I would even say the opposite: "Repeat Yourself" can be even more useful.
To understand this, let's discuss the concept of "context".

What is context

There is no exact definition of "context," I would describe it as: "the belonging of code to the business logic of a specific business process and domain area of the code."
What context can be in practice:
  1. The role of the user who triggered the business logic ("order created by administrator / client / partner")
  1. Whether this business process applies to one ("delete user by id") or multiple entities ("delete users who haven't appeared for more than a year")
  1. The type of device from which we parse data ("parsing data from a coffee machine / vending machine")
  1. etc.
The main feature of context is that it directly affects how this code will develop further. Because new features or changes to current rules are most often related to only one context.
For example, a requirement appeared: "when creating an order by a partner, emails should be sent not only to them but also to our admins" - if the code written for different roles is the same, you will have to start branching it, which can lead to a bunch of errors and degradation of the system as a whole.
If you want your code to be flexible enough, always think about the context in which you write it and whether it's worth reusing it in another context.

Code Connectedness

The second argument in favor of WYNWYN: every time you extract generalized code somewhere, you increase code connectedness.
This means that 2 pieces of code start to depend on one common piece, and therefore when it changes, we always have a chance to break dependent code bases.
Changed something to add a feature, broke something old:
// common/index.ts // This is a reusable function that increases code connectedness const someCommonFunction = (foo: string, bar: Record<any, any>) => { // ... } // first/index.ts import {someCommonFunction} from "common/index.ts" const first = () => { // ... someCommonFunction(foo, bar) // ... } // second/index.ts import {someCommonFunction} from "common/index.ts" const second = () => { // ... someCommonFunction(foo, bar) // ... }
Now let's make a change:
// common/index.ts // For example, now in the first case we need to add a postfix. // Let's do it in the worst way: add a flag const someCommonFunction = (foo: string, bar: Record<any, any>, postfixNeeded: boolean = false) => { // ... } // first/index.ts import {someCommonFunction} from "common/index.ts" const first = () => { // ... someCommonFunction(foo, bar, true) // ... } // second/index.ts import {someCommonFunction} from "common/index.ts" const second = () => { // ... someCommonFunction(foo, bar) // ... }
This is a simple change, but even it can accidentally break the application logic.
It would be a bit better to make 2 different functions, this would slightly increase the reliability of using the changed function:
// common/index.ts const someCommonFunction = (foo: string, bar: Record<any, any>) => { // ... } const someCommonLogicWithPrefix = (foo: string, bar: Record<any, any>) => { // ... someCommonFunction(foo, bar) // ... } // first/index.ts import {someCommonLogicWithPrefix} from "common/index.ts" const first = () => { // ... someCommonLogicWithPrefix(foo, bar) // ... } // second/index.ts import {someCommonFunction} from "common/index.ts" const second = () => { // ... someCommonFunction(foo, bar) // ... }
This is often called "don't change what exists, create something new" and I completely agree, only with the caveat: "don't change what exists, create something new where you will use it":
// common/index.ts const someCommonFunction = (foo: string, bar: Record<any, any>) => { // ... } // first/index.ts import {someCommonLogic} from "common/index.ts" const someCommonLogicWithPrefix = (foo: string, bar: Record<any, any>) => { // ... someCommonFunction(foo, bar) // ... } const first = () => { // ... someCommonLogicWithPrefix(foo, bar) // ... } // second/index.ts import {someCommonFunction} from "common/index.ts" const second = () => { // ... someCommonFunction(foo, bar) // ... }
And the most beautiful option, if in reality someCommonFunction changes too much depending on the context, is to simply leave one needed version in each place of the code:
// first/index.ts const someCommonFunction = (foo: string, bar: Record<any, any>) => { // ... } const first = () => { // ... someCommonLogic(foo, bar) // ... } // second/index.ts const someCommonFunction = (foo: string, bar: Record<any, any>) => { // ... } const second = () => { // ... someCommonFunction(foo, bar) // ... }
If they have different execution contexts, then such an approach will allow you to develop them independently of each other and not be afraid that a change in one place will break another.

OOP encourages violation of WYNWYN

This is one of my biggest complaints about OOP.
The problem is that it motivates developers to extract code into some generalized places, violating both context dependence and adding code connectedness:
class First { someLogic() { // ... // And here in the middle of the code some reusable logic // ... } } class Second { someLogic() { // ... // And here in the middle of the code some reusable logic // ... } }
I've seen people try to extract this logic into a separate class to "adhere to SOLID":
class CommonLogicService { commonLogic() { // ... } } class First { constructor(private commonLogicService: CommonLogicService) {} someLogic() { // ... this.commonLogicService.commonLogic() // ... } } class Second { constructor(private commonLogicService: CommonLogicService) {} someLogic() { // ... this.commonLogicService.commonLogic() // ... } }
This case is fixed quite simply, but people need to understand that there is no problem in duplicating code:
class First { // Again, instead of extracting into a separate class, we just create // a private method and duplicate it in the Second class private commonLogic() { // ... } someLogic() { // ... this.commonLogic() // ... } } class Second { private commonLogic() { // ... } someLogic() { // ... this.commonLogic() // ... } }
And thus we solve the issue with context dependence and connectedness.
But what's much worse in OOP is what is its main advantage: absolutely all methods related to the class will be on that class.
// For example class User { private id: string private email: string private age: number changeEmail() { // ... } changeAge() { // ... } }
So what's the problem?
Now imagine that we want to make it possible to change email but for an admin (that is, with modified logic) and following the best practice above, we'll make a separate method:
// For example class User { private id: string private email: string private age: number changeEmail() { // ... } changeEmailByAdmin() { // ... } changeAge() { // ... } }
And then the same for age and with different roles and extraction of common logic:
// For example class User { private id: string private email: string private age: number private changeEmailCommonLogic() { // ... } changeEmail() { // ... } changeEmailByPartner() { // ... } changeEmailByAdmin() { // ... } private changeAgeCommonLogic() { // ... } changeAge() { // ... } changeAgeByAdmin() { // ... } changeAgeByPartner() { // ... } }
So 2 methods with the development of the application and the emergence of greater differences in context rules turn into 8. But the worst thing is that I am willing to bet that almost all of these methods are called in just 1 specific place... and at the same time they clutter up the file with the User codebase...
And when I think about it, the hairs on my half-buttocks stand on end...
It's even more amusing when another developer didn't understand that this is a context-dependent function, decided to use it, and then, when his context stopped fitting the current one, added a bunch of flags...
Yes, the advantage of this approach is that we know all operations on the user entity, but in my opinion the disadvantage is much greater - when ALL operations on a user, which are called just once in the code, clutter up 1 common file.
And this is one of my main complaints about OOP along with excessive atomicity from the chapter
☠️
2.3. The Unfixable Problem of OOP
.

Life hacks

Here are a couple of life hacks that help me decide on code reuse:

i. Rule of 3

For myself, I have chosen one very simple rule by which I understand whether to extract something or not:
I extract code for reuse only if I repeated it 3 or more times
Again! Everything will depend on the context, but there's a good chance that in 2 out of 3 cases the context will repeat.

ii. Axioms

Everything that is an axiom (a proven and unbreakable rule) can lie in a separate reusable area.
For example:
  1. Mathematical formulas and data types (vector and its addition function, formula for calculating the speed of a falling object, etc.)
  1. Interface for accessing third-party services, that is, everything that represents an SDK / API library.
  1. Formulas defined by GOSTs (for example, for calculating the minimum required quality index of a material, based on an analysis of its content)
  1. Specific rules of your business area (for example, we cannot send numbers larger than 8bit to our devices, so the functions for creating or validating these numbers can be moved to a common area)
  1. Description of the structure of your database tables (if a service has access to this database and these tables, it means it has the right to simply see what data structures exist there)
For even greater simplification: if code can be made into a library or SDK, put in public access or in your private repository and it would make sense, then this code can be moved to a reusable area.

Conclusion

Perhaps this article contradicts everything you've been taught, but that's the whole complexity of coding: you start as a blank slate and just write code, then you think "my code is too stupid, it shouldn't be like this" and learn various "patterns" to make it "smart", and in the end, having struggled with this "smart" code, you realize that you were right initially.
notion image
Why is it important to go through this path? Because at the beginning you write stupid code, not choosing it, but from ignorance, and then you write stupid code, choosing it from knowledge.
Also, this chapter may not be very clear, because it's all very difficult to write properly and I don't think I could have done it right the first time, so please: leave a comment so I can make it more obvious.