Safe Coding

CSC-430

Phillip Wright

Safe Vs Secure

When talking about writing safe code, we are not talking about secure code.

Though safe code will often make it easier to write secure code, because it is easier to write things correctly and clearly.

Safe Code

Safe code expresses the idea of code which is easy to write, understand and maintain without introducing unexpected behavior.

Our Brains Are Small

A lot of the problems we run into when writing code is that we simply are not smart enough to maintain large amounts of code, states, etc. in our minds.

Proper Abstractions

To limit the amount of code that we need to keep in our heads at any given point in time, we should strive to use the proper abstractions so that our code can be treated as a collection of small units glued together.

Proper Abstractions II

By doing this, we can usually focus on one of two things:

  • The logic inside a unit of code
  • The glue code between them

Proper Abstractions III

The abstractions should, of course, really be abstractions!

If changes within a unit of code lead to code changes elsewhere, then your abstraction is probably leaky!

Immutability

Even within a small unit of code, mutability can lead to an incomprehensible number of possible states that must be considered.

Immutability II

If, on the other hand, we liberally use final variables and immutable data types, then this overhead is significantly reduced.

Immutability III

In an immutable data structure, you know that the data will not change, so you can hold that constant in your mind.

Immutability IV

Combined with a properly scoped abstraction, this will often mean that we have constant state and a very small number of code paths that need to be understood!

Immutability V

Immutable data structures are also inherently thread safe, which makes writing concurrent code much easier!

Immutability VI

Beware, though, that sometimes you can fool yourself!

public class NotImmutable {
  private final List<String> data;
  // ...
  public List<String> get(){
    return data;
  }
}

This is not immutable!

Final

The data field itself is indeed immutable, but the List itself is not!

Since we expose it in the getter, other classes can modify the list.

In other words, final only affects references, not data types!

Side Effects

A common cause of leaky abstractions is side effects. Essentially, you should prefer code where all mutations, computations, etc. have a clear cause.

Side Effects II

We don’t want to call a method called doX that actually does X and Y.

For instance, if we have a method that sums a list of integers, it should not modify the list in the process.

Prefer Functions

Accordingly, you should prefer functions that take inputs and compute an output without manipulating the input.

Prefer Total Functions

Even better is to use total functions.

In other words, functions where every input is guaranteed to have a valid output.

Total Functions

This, for instance, means we don’t return null values!

Indeed, the Optional type can actually be seen as a tool for turning partial functions into total functions.

Favor Determinism

The more deterministic your code is, the easier it is to understand and the easier it is to reproduce and debug issues when they arise.

Faking Determinism

When non-deterministic behavior is required, abstract it away so that you can swap the source of non-determinism with a deterministic source for testing.

Random

For example, instead of using Random in your class directly, abstract it!

public class RandomStuff {
  private final Supplier<Integer> randomInts;
  
  public RandomStuff(Supplier<Integer> randomInts){
    this.randomInts = randomInts;
  }
}

Random II

// in production
final var reallyRandom = new RandomStuff(Random::nextInt);

// in tests
final var notRandom = new RandomStuff(()->42);

Fail Fast

It’s good to validate data and cause your code to fail as soon as possible if it must fail.

This makes it easier to find the real source of a problem.

Fail Fast II

For instance, instead of putting null values in a class and having the null propagated through your code until someone needs it, fail in the constructor when a null is provided.

Limitations

Note that the things we have been talking about all have costs in addition to benefits and you will likely find that you can not adhere to all of these rules of thumb.

This is an ideal we want to aim for, not a concrete requirement.