Skip to content

Instantly share code, notes, and snippets.

@shivpatel
Created October 18, 2021 15:25
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shivpatel/bb6c6f49f5ed7b04fbba365e7b85242d to your computer and use it in GitHub Desktop.
Save shivpatel/bb6c6f49f5ed7b04fbba365e7b85242d to your computer and use it in GitHub Desktop.
Programming Principles

Programming Principles

A list of programming principles I follow. A special thanks to my peers who have instilled many of these principles on me.

Structure like a chef.

Organize models, services, controllers, etc. (ingredients) by component (recipe) then type (food group). Contributors (sous-chefs) can focus on a single component (recipe) without needing to jump around the entire project (kitchen).

app/
├─ component/
│  ├─ integration/
│  ├─ order/
│  │  ├─ create.js
│  │  ├─ delete.js
│  │  ├─ filter.js
│  │  ├─ route.js
│  │  ├─ model.js
│  ├─ payment/
│  ├─ shipping/
│  ├─ user/
├─ .gitignore
├─ package.json
├─ index.js

Integrations in one place.

Code dealing with external integrations (database, APIs, AWS, etc.) can be sub-organized under a single component called integration or external. This makes it easy to find all the project integrations.

app/
├─ component/
│  ├─ integration/
│  │  ├─ aws/
│  │  │  ├─ sns/
│  │  │  │  ├─ publish.js
│  │  │  │  ├─ subscribe.js
│  │  │  ├─ sqs/
│  │  │  │  ├─ send.js
│  │  │  │  ├─ receive.js
│  │  ├─ mongodb/
│  │  ├─ stripe/

Nothing is a util.

Helper functions for timestamps belong in a time component. Array helpers belong in an array component. The list goes on. Throwing so-called "util" functionality into a utils folder obscures the project structure and creates the perfect dumping ground.

Nothing is common.

Common gives zero context as to what common includes. Everything in common does something important; otherwise it would not be in the code base. Avoid common and instead place each “common” feature under an appropriately titled component.

If you end up creating a shared dependency across multiple projects, avoid using the word common in the title. Give the dependency a descriptive name.

Log like a minimalist.

Logs are useful when they aren’t polluted. Only log on errors.

Code should be written to succeed by default. If it's important enough to be logged during success, then it should persist in a database.

Avoid unnecessary logging libraries. There's no need for libraries that append timestamps, hostname, etc. to the output when most logging services (e.g. AWS Cloudwatch, Splunk, Datadog, logz.io, etc.) take care of this.

With excessive logging you also run the risk of logging secrets or other sensitive information.

Bonus: The less fluff you log, the longer the retention period you can afford.

Avoid dependency hell.

Limit external dependencies to only the most critical, lightweight, and well-vetted options available. Be wary of nested dependencies; they introduce more attack vectors and increase time spent on vulnerability management.

In some cases, it's better to copy/paste snippets of code then to require an entire dependency.

Variable and function names should be brief and reveal intent.

// component/user/history.js

function lastPlayedSongByUser(user) { }
function lastPlayedBy(user) { } // better

let lastPlayedSong
let lastPlayed // better

The context (filename or folder name) should intuitively tells us that lastPlayed is in regards to a song. No need to pollute the variable name any further.

End function names in prepositions.

function songs(playlist) {
  // too generic; no intent revealed
}

function songsFromPlaylist(playlist) {
  // playlist is redundant
}

function songsFrom(playlist) {
  // just right
}

Prefix function names according to underlying code.

For functions that depend on integrations, prefix the function name with verbs like get or set:

function songBy(id) {
  // not clear it calls an integration
}

function getSongBy(id) {
  // more obvious it is calling an integration like DB or API
}

For all other functions, avoid prefixing:

function getYearFrom(date) {
  // misleading; no integration is being used
}

function yearFrom(date) {
  // better
}

Keep function params to 3 or less.

If you need more, consider an options or config params; split on required vs optional.

function getSongsBy(artist, length, rating, page, offset) {
  // too many params. requires length and rating to use paging.
}

function getSongsBy(artist, options) {
  const [length, rating, page, offset] = options
  // better. more options can be added without polluting params
}

Switch statements over multi else-if blocks.

Makes code easier to read.

if (action === 'INSERT') {
  doSomething()
} else if (action === 'UPDATE') {
  doSomething()
} else if (action === 'DELETE') {
  doSomething()
} else {
  throw new Error(`invalid action=${action}`)
}
switch(action) {
  case 'INSERT':
    doSomething()
    break
  case 'UPDATE':
    doSomething()
    break
  case 'DELETE':
    doSomething()
    break
  default:
    throw new Error(`invalid action=${action}`)
}

Centralize configuration.

Avoid referencing process or environment variables throughout the code. Create a config module/package that takes care of validating, enforcing, and mapping all configs in a single location. Require the config package on code init to quickly error out when invalid configs are present.

Singular vs plural.

Use both as needed to ensure folders and routes read like English. When in doubt, default to singular.

GET   /shelter/2/animals    Get all animals at shelter 2.
POST  /shelter/2/animal     Add an animal to shelter 2.
GET   /shelter/2/animal/1   Get animal 1 from shelter 2.
DEL   /shelter/2/animal/1   Delete animal 1 from shelter 2.

Dependency injection and context.

Avoid using global scope. Create a context module that can be passed around (injected) as needed. Connections, external SDKs, etc. should be included in context to make unit testing easy. Context should always be the first parameter in any function that requires it.

const ctx = {
  db: new MongoDB(uri),
  cache: new Redis(uri),
  sns: new AWS.SNS()
}

init(ctx)

Avoid inversion when possible.

Inverted statements are harder to read.

if (!playlist.isPopulated()) {
  // ok. isPopulated is also a bit obscure
}

if (playlist.isEmpty()) {
  // better
}

Keep it private when possible.

Unnecessarily exposing functions and variables increases maintenance cost. It also runs the risk of being used in other components that it was not intended for.

Avoid todos in the code.

They never get addressed. If it’s a critical todo, finish it right now. If not, document it in your project management tool.

Use 2 or 4 spaces for line indentations.

Easier to read and looks better in more editors.

Keep comments to a minimum.

Code should read like English to most developers. Reserve the use of comments to cases in which context could be misinterpreted or assumptions were required.

Organize imports by type then alphabetically.

External dependencies first. Internal dependencies next.

const assert = require('assert')
const aws = require('aws-sdk')
const mongodb = require('mongodb')

const createUser = require('../component/user/create')
const time = require('../component/time/format')

Use concurrency responsibly in code.

It can cause race conditions that are hard to track, reproduce, and fix. Consider letting cloud providers take on the burden by scaling horizontally instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment