Skip to content

Instantly share code, notes, and snippets.

@ccdle12
Last active June 17, 2020 23:49
Show Gist options
  • Select an option

  • Save ccdle12/74fcc8d6f882165ac75459063d68cd9a to your computer and use it in GitHub Desktop.

Select an option

Save ccdle12/74fcc8d6f882165ac75459063d68cd9a to your computer and use it in GitHub Desktop.
A document outlining my coding conventions.

This document will be used as my reference and will be updated to include the coding conventions I will implement on solo-projects as well as suggestions on working with a team.

  1. Introduction
  2. Conventions
  3. Language Specific

Goals of Programming

It's important to put into perspective some of the goals of Programming:

  • Solve a particular problem that creates value shudders, sorry couldn't think of a better word to an end user.

Defining an end user

Defining an 'end user' is important because they could be...

  • Another programmer using your library
  • An everday consumer using your software to communicate, track data or shop online.
  • A business that needs a very particular set of logic to be proccessed and presented to enable it's business to function.

Defining the goals of a program

Understanding an end user, code/programs then need to be able to:

  1. Work dependably and repeatedly.
  2. Be able to maintained as well as enhanced.

Both points 1 and 2 indicate code/programs need to have a level of 'engineering', 'discipline', 'accuracy'.

If code/programs need maintanence and enhancement then it indicates that there are people and machines behind the curtain making sure all the plates are spinning. So these people and machines need to be able to...

  1. Understand and read your code/programs
  2. Build on top/refactor existing code/programs.

This document deals with the very specific topic of keeping code/programs readable and maintable for humans.

This is not a final document but a continuous work in progress, refining the best practices.

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." - Bob Martin

Conventions

Documentation

  • Code should be documented but should not be overly verbose.

  • Structs/Enums/Classes should all have comments explaining it's purpose.

  • "Non-obvious" functions/classes that are not "utility" should have comments as the business logic would not be clear to a new programmer.

  • Here is an example taken from LND:

// StaticFeeEstimator will return a static value for all fee calculation
// requests. It is designed to be replaced by a proper fee calculation
// implementation. The fees are not accessible directly, because changing them
// would not be thread safe.
type StaticFeeEstimator struct {
    // feePerKW is the static fee rate in satoshis-per-vbyte that will be
    // returned by this fee estimator.
    feePerKW SatPerKWeight

    // relayFee is the minimum fee rate required for transactions to be
    // relayed.
    relayFee SatPerKWeight
}

The above is a good example of documenting a struct/class that is very specific to the domain problem and explaining what it is trying to solve. To a new programmer that is getting familiar with the code base, they would ask...

what is a "StaticFeeEstimator"? what is a "feePerKW" and what in the hell is a "relayFee"?!

These are good names! but they are very specific to the problem domain. For a new programmer joining the team, without the comments, it would be difficult to piece together how all the logic fits in to place to solve FeeEstimation per hops on the Lightning Network!

File System Naming

These are the naming conventions that should be followed when interacting with the file system.

File Handlers

When opening a file, and being returned a file handler its common that f or fs or fd is used.

For now, favour

file_handler := open(some-file)

Functions

  • Functions should be short
  • Should have a singular purpose (as much as possible)
  • If the language allows, favour snake case over camel case fn some_func():
  • Code in functions should be separated by a new line to identify a collection of logic.
In the below function, we separate different operations/collections of logic in to 'paragraphs', 
identified by a new line.

pub fn open(path: &Path) -> Result<KvStore> {
          create_dir_all(&path)?;

          let mut path_buf = PathBuf::from(&path);
          path_buf.push("log");
          path_buf.set_extension("txt");

          OpenOptions::new()
              .write(true)
              .create(true)
              .open(&path_buf)
              .expect("failed to create file using path_buf");

          Ok(KvStore {
              store: HashMap::new(),
              path_buf,
          })
      }

Naming

The name of a function should contain one verb and be as concise as possible.

write.write(something)

If the language allows, use inner functions as part of the technique 'Extract Function' to allow the reader to read what the function does before they look at the implementation detail.

In non-statically typed languages use the convention doing_something_for(an_object).

def enum_value_for(a_car):
...

Iterators

If the language allows, favour iterators over imperative loops.

Of course, this is all within reason, for more complicated or "involved" logic and imperative loop would make sense.

For simple functions using a functional iterator would be appropriate.

Example:

Bad:
result = 0
for fee in fees:
   result += fee['price']
   
return result

Good:
return sum(x['price'] for x in fees)

Stubbing and Mocking

Used for when needing to develop a skeleton framework or to mock an implemenation. Should ONLY be used as a placeholder for future development or if it's needed for another part of the program to work for now, but the implemenation detail is not warranted at the moment.

Stubs shoudl raise an error if the implemenation is called identifying it is a stub.

def placeNewOrder():
   raise NotImplementedError

Single Origin

Is a princple that for variables or 'injected' information into a codebase should have a single origin.

An example would be a .env file or a const.

This means that if we have to refactor the code to change the value of a variable, we can just change it at the source and NOT have to go through the codebase to change all instances of it.

This allows the codebase to be modified, refactored or updated more easily without worrying about breaking it and saving time.

Variables

Naming

Naming conventions for variables which should override all language specific conventions unless it is a language restriction.

Shortened Variables

Favour the following shortened variables. <favoured-convent> | <not-favoured>

msg | message
req | request

Longer Variables

Favour the following longer varibales instead of shorter variables <favoured-convention> | <no-favoured>

error | err

Language Specific

Programming/Coding conventions specific to a language.

Golang

Functions

  • Language Implemenation - Must be CamelCase: func GetFoo() (Foo, error) {...}
  • Language Implemenation - Open functions start with a capital letter, closed functions start with a lower case letter.
Open:
func GetFoo() (Foo, error) {...}

Closed:
func getFoo() (Foo, error) {...}

Python

Error Handling

Create a custom error for each class or file scope.

  1. Inherent from the base error
  2. Create custom errors from the custom base error
  3. When checking for errors, only check the custom base error
class MyException(BaseException):
  pass

class NonPositiveIntegerError(MyException):
  pass
  
class TooBigIntegerError(MyException):
  pass 

...

try:
  average(1, -1)
except MyException as e:
  print(e)

Iterators

Favour iterators when appropriate. (simple, straight forward logic)

Sum Numbers

For adding or summing a collection of numbers use the sum iterator.

fees = [5, 43, 12, 4, 125]

return sum(x for x in fees])

Functions

  • Functions should be snake case
def get_foo():

Style/Linting

Stubbing and Mocking

Stubbing style for Python:

def placeNewOrder():
   raise NotImplementedError

Rust

Functions

  • Should be snake case pub fn get_foo() -> Result<Foo> {...}

Errors

At the moment I'm handling errors by creating an errors.rs file. In the errors.rs file we create a public enum Error which holds all the types for the project.

Implement the From trait for each error type to convert them from their error type to the custom one for the project.

E.g. See IOError and Serde

The exported type pub type Result<T> = std::result::Result<T, KvStoreError>; creates a Result type of some generic T and the custom error type. This allows the ? to be used anywhere in the code where an error can be thrown inside of a Result function.

The second code example shows the import of the default Result type and its use.

  /// The custom error type for this project. Each different error type will be
  /// added as an enum variant.
  #[derive(Fail, Debug)]
  pub enum KvStoreError {
      /// Used for errors that are miscellaneous and/or cannot be explained.
      #[fail(display = "An unknown error has occurred")]
      UnknownError,

      /// Error for a key not found in the key value store.
      #[fail(display = "Key not found")]
      KeyNotFoundError,
 
      /// Serde serialization and deserialization errors.
      #[fail(display = "{}", _0)]
      Serde(#[cause] serde_json::Error),

      /// Standard Input/Output errors.
      #[fail(display = "{}", _0)]
      IOError(#[cause] std::io::Error),
  }

  impl From<serde_json::Error> for KvStoreError {
      fn from(err: serde_json::Error) -> KvStoreError {
          KvStoreError::Serde(err)
      }
  }

  impl From<std::io::Error> for KvStoreError {
      fn from(err: std::io::Error) -> KvStoreError {
          KvStoreError::IOError(err)
      }
  }

  /// Shorthand alias for Result in this project, uses the concrete implementation
  /// of KvStoreError.
  pub type Result<T> = std::result::Result<T, KvStoreError>;

In main.rs:

use crate::{KvStoreError, Result};

...

self.write_cmd(&Command::Get { key }, self.log_file_append_only()?)?;

Stubbing and Mocking

Stubbing style for Rust:

pub fn some-function() -> Result<()> {
   eprintln!("unimplemented")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment