AZ-structuring for source code files

Publication date: 2022-11-16

In this post I’ll tell you about my approach to structuring source code files in my projects. I called it AZ-structuring. This approach works for programming languages where source code is stored as many files in a filesystem (like Java, PHP, JavaScript/nodejs).

Premise

First of all, I want to talk about the reason that prompted me to use this structuring. The reason is refactoring.

“Code is read much more often than it is written.”© Guido van Rossum

Paraphrasing Guido we can say:

Code is changed much more often than it is written from scratch.

I don’t write code for NASA missions, I’m a web developer. There are some areas in software development where waterfall methodology is the only model that can be used (like NASA missions, for example).

Image 2

The waterfall model.

We have one only attempt to land a probe on the ground and any error can be fatal.

Web development is different. In the beginning we often do not have a clear idea of ​​what we want to get at the end. We see only the direction of development but not all the details. The spiral model is used for such cases:

Image 3

The spiral model.

The cost of errors is not as critical in web projects as in NASA projects and we can develop by trial and error here. That is, we will refactor our code at each iteration — add, remove and change. Refactoring is a common process in web development.

Boundaries

The main problem with refactoring, at least for me, is how to define the boundaries of change propagation. These boundaries are old problem and OOP (introduced over 50 years ago) is one of the attempts to solve the problem. Encapsulation — it’s just about that.

Modern web projects contain hundreds of thousands of files. To be precise, they consist of packages, which consist of files. Some of the files are only used within their package, some are used by other files in other packages. We need to know where the file we will change is used. How freely we can change the signature of a method or function.

Image 4

JavaScript is my active programming language at the moment, so I will be giving examples in that language (EcmaScript 6+) in this post.

In fact, I can freely change the code that does not go outside the file (is not the subject of an export statement). The code exported by the es6-module may be used anywhere in the project. Or maybe not. When I create an npm-package for use in projects (my own or external), I can describe what code I consider public (in main or export). The rest of the code is considered to be private to my package. When refactoring such code, I only need to worry about the calling code within the package itself. If someone used my “private” code from his npm-package — that’s his problem.

But what to do if there is a code in my package whose applicability boundaries should be even smaller? We need to have some set of features that would help us identify such cases and the boundaries of change propagation.

How do we store sources?

Our projects consist of packages managed by packet managers (like composer or npm/yarn). Each package contains everything you need to develop it — sources, tests, documentation, configuration, etc. Usually, we allocate a separate directory for sources in our package, for example: ./src/.

The further structure depends on the rules applied in the project. It can be role-based:

./doc/

./src/

./api/

./components/

./helpers/

./pages/

./test/ or feature-based:

./src/

./payments/

./products/

./users/

… I use mixed rules in my projects — first ‘by role’, then ‘by features’:

./src/

./Back/

./Mod/

./RDb/

./Front/

./Mod/

./Store/

./Ui/

./Lib/

./Route/

./Home.mjs

./Settings.mjs

./User.mjs

./Shared/

./Dto/ The first level of separation (by role) depends on the framework used (and on the abstractions of that framework), and the second level — is on the subject area (business tasks). The framework has more stability, but the subject area is changed from project to project.

In case we have a big project, we can add more levels to ’by feature’- level:

./Route/

./Settings/

./Payments.mjs

./Profile.mjs

./Security.mjs or even:

./Route/

./Settings/

./Payments/

./Payoneer.mjs

./PayPal.mjs

./Wise.mjs All these files in an (npm-)package have the same visibility — every file can import another file in general.

In some cases, we can determine boundaries of change propagation based on the role of the file (route, model, controller, helper, …). Usually, we can distinguish two types of boundaries — global & local. Global type means we can use the file anywhere in our package (like helper), and local type — any changes in the file will not affect other files in the package (route).

A-struct

“Programming is a desperate losing battle against the unconquerable complexity of code and the treachery of requirements.” © Jonathan Edwards

We can’t do anything about the treachery of requirements but we can beat the complexity with decomposition. We separate our projects into packages, sources in the package to folders and files - by role, by feature, mixed or smth. else.

Every big snippet of code (file) we can decompose into a set of smaller snippets (files). In some cases, these small snippets have boundaries of change propagation inside the original big snippet only.

For example, we have one big code snippet for a model of the Settings object in our project:

./Mod/

./Settings.mjs We can decompose this snippet as one root snippet (./Settings.mjs) and 4 nested snippets (./Settings/*.mjs):

./Mod/

./Settings/

./Payments.mjs

./Profile.mjs

./Security.mjs

./Settings.mjs Do we have a way to distinguish how many models there are here — 1 or 5? There is a sign that it is a one model with 4 dependent snippets — folder ./Settings/ and file ./Settings.mjs. It is very likely that we can refactor code in ./Settings/ and not worry about code outside this folder and ./Settings.mjs itself (boundaries of change propagation).

But what if file structure is already nested? We already have 5 models (file structure is exactly as above) and we want to decompose a big model ./Settings.mjs into smaller pieces? How can we mark boundaries of change propagation?

In my projects I use the ./A/ subfolder for these purposes:

./Mod/

./Settings/

./A/

./Snippet1.mjs

./Snippet2.mjs

./Payments.mjs

./Profile.mjs

./Security.mjs

./Settings.mjs In that case I know that all files in ./Settings/A/ are pieces of ./Settings.mjs (result of decomposition). Code from root file ./Settings.mjs can be used anywhere in the package or even project, but code from ./Settings/A/ can be used in ./Settings.mjs only. In a way, I define the private part of the code at the file level. It makes refactoring much easier for me.

If necessary, A-structure can be nested. Here is the path to one of my files in a real project:

…/Route/Settings/A/Profile/A/Password/Change/A/Evt/Change.mjs

Z-struct

There are cases when the dependent code is applied not to one file, but to a group of files — a sort of ’library for a group’. Also with its boundaries of change propagation. In this case I use z-structuring:

./Cli/

./Data/

./Z/

./ListTables.mjs

./Export.mjs

./Import.mjs

./Init.mjs I know that changes in ./ListTables.mjs do not affect the code above ./Cli/Data/.

Resume

Embedding ‘private’ scopes in the file structure of the source code makes it possible to more clearly define the boundaries of the propagation of changes, which helps with refactoring of big projects.

If you enjoyed this article, please give it a clap and follow me for more content!

Stay connected:

Thank you for your support!

Additional source-code excerpts

src/
  Back/Mod/RDb/
  Front/Mod/Store/Ui/
  Lib/Route/Home.mjs
  Shared/Dto/
Mod/
  Settings/
    Payments.mjs
    Profile.mjs
    Security.mjs
  Settings.mjs
Mod/
  Settings/
    A/
      Snippet1.mjs
      Snippet2.mjs
    Payments.mjs
    Profile.mjs
  Settings.mjs
Cli/
  Data/
    Z/ListTables.mjs
    Export.mjs
    Import.mjs
    Init.mjs