How Will It Look in Practice?

Lets take a simplified example and put this approach to practice. For the sake of simplicity, let’s imagine that our developers will not have access to any particular IDE (specifically with nice cross-files searching tools, or other refactoring tools). Imagine the project is structured as follows:

src/

| -- module-a/

| -- | -- index.ts

| -- | -- | -- function aOne()

| -- | -- | -- function aTwo()

| -- | -- a-one.ts

| -- | -- | -- function aOneFuncOne()

| -- | -- | -- function aOneFuncTwo()

| -- | -- a-two.ts

| -- | -- | -- function aTwoFuncOne()

| -- | -- | -- function aTwoFuncTwo()

| -- module-b/

| -- | -- index.ts

| -- | -- | -- function bOne()

| -- | -- | -- function bTwo()

| -- | -- b-one.ts

| -- | -- | -- function bOneFuncOne()

| -- | -- | -- function bOneFuncTwo()

| -- | -- b-two.ts

| -- | -- | -- function bTwoFuncOne()

| -- | -- | -- function bTwoFuncTwo()

Now imagine changing of name or signature of src/module-a/a-one/aOneFuncOne() . Without any established rules, I would need to check the body of eleven other functions to see if they use aOneFuncOne() and if they need to change. Note that the difficulty of this check is not the same for functions of src/module-b/b-one compared to functions of src/module-a/a-one , since functions of a-one.ts are already in the same file as aOneFuncOne() . Listing the interactions, for the former I just have:

read the body of aOneFuncTwo() , see if changes are required, do them if they are

For the latter, the list of interactions is like this:

list all submodules of src/

list all files in src/module-b/

open src/module-b/b-one.ts

read the body both bOneFuncOne() and bOneFuncTwo() , see if changes are required, do them if they are

These interactions each take some time (and cognitive effort), since they each involve at least one click (and perhaps some scrolling and some typing). Similarly, checking other files in src/module-a is easier than checking files in src/module-b , as it requires fewer raw interactions, though it is more difficult than just checking other functions in src/module-a/a-one.ts , as then it would require more interactions.

Now if the architecture enforced the rule that files in modules other than src/module-a can only use functions defined in src/module-a/index.ts , the difficulty of the change would be reduced drastically. This of-course would have the downside that if some day I need aOneFuncOne() somewhere in module-b , I would need to also change module-a/index.ts to comply with that rule, an overhead whose probability and interaction cost we can again estimate pretty precisely.

I could even go one step further and require src/module-a/a-one.ts to explicitly mention the functions that it exports to other modules (as Typescript would require this of you regardless of your architectural design), then I could check if aOneFuncOne() is an exported function, and if it is not, the change would again be drastically cheaper to make.

Similarly, if every other file had to explicitly import aOneFuncOne() via an explicit import statement (i.e. import { aOneFuncOne } from 'src/module-a/a-one ), I would have a much easier time checking which functions I need to change. Now if I had another (unmentioned file) with that import command at the beginning, which had 10 other functions within it but only one of them actually using aOneFuncOne() , I would be better off putting that one outlier in its own file, as it would reduce the number of interactions required for detecting and making the change again.

Note that putting all of my 12 functions in src/module-a/a-one.ts would also not be a good idea, since though it would reduce the difficulty of checking all other functions, it is still a sub-par solution to checking perhaps one or five other function bodies.

With all of this in mind, we can now make much more objective judgements when deciding between two architectural designs, depending on how inter-dependent my functions would be in each design, and how close or far these inter-dependent functions would be structured. Note that this is just another way to say we should prefer an architecture that is more cohesive and loosely coupled, but this time around we have more tangible metrics to assess how much more cohesion or loose-coupling are we talking about.