DEV Community

Cover image for Organizing source file contents
Jonathan
Jonathan

Posted on • Updated on

Organizing source file contents

We developers often focus on how we organise our files into folders in the file system. But perhaps just as important is how we organise the internal contents of our source files.

I've evolved a blueprint for how I order the contents of each source file, based on what seems to work for open-source code, team mates' code, and, of course, my own experiences.

I think it's good to stick to a consistent pattern of ordering. It is easier for beginners to understand source code if the files are well structured. It is also easier for more experienced folks to quickly parse a file and find whatever they're looking for.

Principles

I use three principles as a general guide to how I order file contents:

  1. More abstract before more concrete.
  2. Wider scope before narrower scope.
  3. Definition before usage.

Based on these principles, I've developed a rough outline for how I order my files.

Note: These examples are written in Typescript and React, but you could easily apply the concepts to other major languages and frameworks.

So without further ado...

The blueprint

Here's a summary of the blueprint:

  1. Comment header
  2. Compiler directives
  3. Imports (third-party)
  4. Imports (app-scope)
  5. Imports (folder-scope)
  6. Interface and type declarations
  7. Utility functions and constants (file-scope)
  8. Central code of the file
  9. Exports

And here's the full example code (click to zoom):

Screenshot of the entire example, with arrows pointing to where each of the sections are placed

Now let's go through each section in detail.

1. Comment header

Depending on who I work for or which open-source project I'm collaborating on, I may need to include a comment header with some important content, such as licences, copyright notices, etc. I keep these right at the very top, before anything else.

This is because a comment header is global to the file ("Wider scope before narrower scope"), so the best place to put it is right at the top, where it's likely to be read first.

Example:

/*****************************************************************************
 * ListApp, Copyright (c) 2019, Jonathan Conway
 * All rights reserved.
 *
 * ListApp is licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 * ListApp includes source code licensed under additional open source
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 * this source code distribution or the Licensing information page available
 * at runtime from the About dialog for additional information.
 *****************************************************************************/
Enter fullscreen mode Exit fullscreen mode

2. Compiler directives

Anything I need to tell the compiler or transpiler generally goes first, prior to any code. In C++, this would be pre-processor macros. In C#, #define macros. In Javascript, directives.

This is because compiler directives define behaviour that is logically prior to application code ("Definition before usage"), so I would prefer to read them before reading any code that uses them.

Example:

"use strict";
Enter fullscreen mode Exit fullscreen mode

3. Imports (third-party)

I keep all imports of libraries that are third-party or external to my application together here.

This is because third-party imports have the widest scope of all my imports – they are accessible to my entire application ("Wider scope before narrower scope").

I'm generally not too concerned about ordering them any particular way, e.g. alphabetically, because ideally, I want to keep this list very short, so that there's no need for ordering. If this list is long, I see that as a bad smell, indicating that the module has too many responsibilities.

However, I generally will put the most globally-relevant imports toward the top, and the more specific imports lower down.

Example:

import * as React from "react";
import { Component } from "react";
Enter fullscreen mode Exit fullscreen mode

4. Imports (app-scope)

Next come the imports of modules that are scoped to my entire application, such as global types, global state or general utilities.

This is because app-scope imports have a narrower scope than third-party imports, but a wider scope than folder-scope imports ("Wider scope before narrower scope").

(Side note: I try to use barrel-style imports for these, just because it's cleaner and more encapsulated.)

Example:

import { Item } from "../types";
Enter fullscreen mode Exit fullscreen mode

5. Imports (folder-scope)

Finally (for imports) are any imports that are scoped to the same folder as the current file.

This is because folder-scope imports have the narrowest scope of all imports ("Wider scope before narrower scope").

Example:

import { ItemCard } from "./ItemCard";
Enter fullscreen mode Exit fullscreen mode

6. Interface and type declarations

Any interfaces and type declarations are declared here.

This is because interfaces and types are abstractions ("More abstract before more concrete") and because they must precede the code that implements them ("Definition before usage").

Example:

interface ItemsListProps {
  readonly items: readonly Item[];
  readonly fetchItems: (range: { from: number, to: number }) => void;
}
Enter fullscreen mode Exit fullscreen mode

7. Utility functions and constants (file-scope)

Utility functions and constants that are required by the central code in the file are placed here. These are placed in order of definition followed by usage.

This is because the utility functions are likely to be more generic than the code that calls them ("More abstract before more concrete") and because both utility functions and constants are scoped to the entire file ("Wider scope before narrower scope") and used after they are declared ("Definition before usage").

Example:

const ITEMS_PER_PAGE = 5;

const calculateRangeToFetch = (currentPageNumber: number) => ({
  from: (currentPageNumber * ITEMS_PER_PAGE),
  to: ((currentPageNumber + 1) * ITEMS_PER_PAGE)
});
Enter fullscreen mode Exit fullscreen mode

8. Central code

Most source files I create have one central or core piece of code, which everything else is built around.

This is typically a central function, component, class or other kind of structure. It will usually be the only structure that I export from the file, but I might export other structures if needed (such as interfaces or types).

Being the central part of the file, it makes sense for the code to sit in the centre, after all the code that it depends on (imports, declarations, utilities), but prior to any exports.

Example:

class ItemsList extends Component<ItemsListProps> {
  public state = {
    currentPageNumber: 1
  };

  private fetchMore() {
    this.props.fetchItems(
      calculateRangeToFetch(
        this.state.currentPageNumber));
    this.setState({
      currentPageNumber: this.state.currentPageNumber + 1
    });
  }

  public render() {
    return (
      <div>
        <ul>
          {this.props.items.map((item, index) =>
            <ItemCard key={index} item={item} />
          )}
        </ul>
        <button onClick={() => this.fetchMore()}>
          Fetch {ITEMS_PER_PAGE} more
        </button>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

9. Exports

Finally, I get to the export statements, which expose the structures declared earlier in the file to the outside world.

This is because, if I want to see which structures are exported outside the file, I can quickly scroll to the end and find them together. Also, exports always pre-suppose structures that are exportable, i.e., that have already been declared ("Definition before usage").

Example:

export { ItemsList };
Enter fullscreen mode Exit fullscreen mode

Automating the organization

Once you've decided how you'd like to organize your source file, the actual task of organizing them is an excellent candidate for automation.

In my own case, working with Typescript in Visual Studio Code, I've used Run Code Actions on Save to good effect. After enabling this setting, I simply add the ordered-imports rule to my tslint file, with grouped-imports set to true, which uses the same grouping as is given in this article.

Source files are like buildings

I like to think of a source file as analogous to a building.

A building (typically!) has clear entrances and exits, and is structured in such a way that a visitor is progressively introduced to the layout of the building. For example, the entrance may lead to a hallway with multiple labelled doors, or to a landing that overlooks a large open space from which entrances to other sections of the building can be seen.

In a similar manner, a source file should have a clear ordering of its content, so that the viewer, scanning from top to bottom, is progressively introduced to its primary structures and quickly develops a basic mental "map" of the contents of the file.

I hope you will find this idea useful and tweak it to suit your own projects!

Further reading

Discussion (1)

Collapse
conw_y profile image
Jonathan Author

⚡️ Update: Added a section on Automating your imports, using @code's Run code actions on save feature as an example.