Some obstacles to clear and concise code.
# Noncritical function design
We call functions for many reasons. Two very common reasons are "object creation" and "object modification".
## Creation functions
A creation function promises to produce some new object. If it cannot produce the new object, it should fail. Please note that this is not limited to constructors. It can be a function, for example, with a signature like this (C++): `auto read_file(std::filesystem::path const& filename) -> std::vector<std::byte>;`
## Modification functions
A modification function promises to make a certain change to an existing object. If it cannot make the promised change, it should fail. The target object should either be in its original condition, or it should be considered to be in a valid-but-unknown state after the call, and the caller should probably just destroy it.
Each modification function should pick one of these techniques for maintaining the target object after failure. In C++ land, these are called the "strong exception guarantee" and the "basic exception guarantee", respectively. Don't let the word "exception" get in the way though. The principles apply even when you replace "exception" with "error".
The "functional update" technique can be used to re-frame modification functions as creation functions. This is cool and useful.
## Takeaway Questions
Understand the purpose of each function that you write. Are you creating an object? Are you modifying an object? Are you doing something else?
If you are creating an object, are you doing anything other than that? Does your function signature suggest you are doing something other than that?
If you are modifying an object, what happens when the function fails? Are you implementing a strong or basic guarantee for your target object? Does your function signature make it clear that this is a modification function? Does it make it clear what object is being modified?
# Noncritical class design
We use classes for many reasons. But most classes can be considered "aggregates" or "invariants".
## Aggregate classes
Data grouping classes make no promises about the relationships between their members. They exist to be composed, and to be passed into and out of functions. In C++, they are best implemented as structs with nothing but member declarations.
## Invariant classes
An invariant class is one that maintains a set of truths. The responsibility for maintaining those truths is shared between all the non-const member functions of the class. It is easiest to implement invariant classes when they maintain a minimal set of truths, and when they have a minimal set of non-const member functions.
To reduce the number of non-const member functions: provide free functions implemented using non-const member functions. After all, if none of the non-const member functions used to implement the free function violate the invariant, then the free function also doesn't violate the invariant! If your language doesn't support free functions, or you otherwise dislike them: consider making a wrapper class that provides what would otherwise be free functions as member functions that operate on a private instance of the invariant type. The wrapper type will not have private access, so it cannot violate the invariant.
Invariant classes are extremely important. They are a powerful mechanism for increasing a program's level of abstraction.
NOTE: Private functions are often a hint that a class is doing a little too much on its own. When possible, private functions should be made into free functions, and internal state to should be explicitly passed into those free functions. As a bit of motivation for this stance: How do you write a unit test for a private function?
## Takeaway Questions
Understand the type of class you are making. Are you just listing members? Are you maintaining an invariant? Are you doing something else?
If you are maintaining an invariant, are you certain the invariant holds whenever any of your non-const functions fail? How many non-const member functions do you have? Could some of those be free functions? How many member variables do you have? Could any of those be function-local variables? What troubles are you resolving with this class? What functions does your class provide that could be considered part of a higher-level abstraction? Does your class have a lot of private member functions? Can any private member functions be reimplemented as free functions?
# Handling failed operations
The section on "modification functions" already explains that a function needs to leave the program in a actionable state after failing, and that this is true regardless of the error-reporting mechanism.
Other than that: manual error propagation limits attempts at keeping code concise. Rely on exceptions or use language-provided syntax sugar for propagating errors.
## Takeaway Questions
How much of your code is error handling?
# Nonexclusive mutable references & Implicit data stuctures
Function authors generally expect that reference arguments are different objects, and that they are the only ones modifying them.
To retain a reference to an object is to create an implicit data structure. If A must refer to B: A and B should be parts of an explicit data struture, C. C should provide B to A when A needs it. This is already true at the process level. But invariants are much easier to maintain at the class level.
If you hesitate to replace pointers (or references) with ids because ids can be fabricated and used to break encapsulation: Understand that the same can be done with pointers. Basically, a private id into a pool is as good as a private instance. The difference being that an id can only be used when given a separate access mechanism.
## Takeaway Questions
How complicated is the implicit data structure maintained by your application process?
We call functions for many reasons. Two very common reasons are "object creation" and "object modification".
## Creation functions
A creation function promises to produce some new object. If it cannot produce the new object, it should fail. Please note that this is not limited to constructors. It can be a function, for example, with a signature like this (C++): `auto read_file(std::filesystem::path const& filename) -> std::vector<std::byte>;`
## Modification functions
A modification function promises to make a certain change to an existing object. If it cannot make the promised change, it should fail. The target object should either be in its original condition, or it should be considered to be in a valid-but-unknown state after the call, and the caller should probably just destroy it.
Each modification function should pick one of these techniques for maintaining the target object after failure. In C++ land, these are called the "strong exception guarantee" and the "basic exception guarantee", respectively. Don't let the word "exception" get in the way though. The principles apply even when you replace "exception" with "error".
The "functional update" technique can be used to re-frame modification functions as creation functions. This is cool and useful.
## Takeaway Questions
Understand the purpose of each function that you write. Are you creating an object? Are you modifying an object? Are you doing something else?
If you are creating an object, are you doing anything other than that? Does your function signature suggest you are doing something other than that?
If you are modifying an object, what happens when the function fails? Are you implementing a strong or basic guarantee for your target object? Does your function signature make it clear that this is a modification function? Does it make it clear what object is being modified?
# Noncritical class design
We use classes for many reasons. But most classes can be considered "aggregates" or "invariants".
## Aggregate classes
Data grouping classes make no promises about the relationships between their members. They exist to be composed, and to be passed into and out of functions. In C++, they are best implemented as structs with nothing but member declarations.
## Invariant classes
An invariant class is one that maintains a set of truths. The responsibility for maintaining those truths is shared between all the non-const member functions of the class. It is easiest to implement invariant classes when they maintain a minimal set of truths, and when they have a minimal set of non-const member functions.
To reduce the number of non-const member functions: provide free functions implemented using non-const member functions. After all, if none of the non-const member functions used to implement the free function violate the invariant, then the free function also doesn't violate the invariant! If your language doesn't support free functions, or you otherwise dislike them: consider making a wrapper class that provides what would otherwise be free functions as member functions that operate on a private instance of the invariant type. The wrapper type will not have private access, so it cannot violate the invariant.
Invariant classes are extremely important. They are a powerful mechanism for increasing a program's level of abstraction.
NOTE: Private functions are often a hint that a class is doing a little too much on its own. When possible, private functions should be made into free functions, and internal state to should be explicitly passed into those free functions. As a bit of motivation for this stance: How do you write a unit test for a private function?
## Takeaway Questions
Understand the type of class you are making. Are you just listing members? Are you maintaining an invariant? Are you doing something else?
If you are maintaining an invariant, are you certain the invariant holds whenever any of your non-const functions fail? How many non-const member functions do you have? Could some of those be free functions? How many member variables do you have? Could any of those be function-local variables? What troubles are you resolving with this class? What functions does your class provide that could be considered part of a higher-level abstraction? Does your class have a lot of private member functions? Can any private member functions be reimplemented as free functions?
# Handling failed operations
The section on "modification functions" already explains that a function needs to leave the program in a actionable state after failing, and that this is true regardless of the error-reporting mechanism.
Other than that: manual error propagation limits attempts at keeping code concise. Rely on exceptions or use language-provided syntax sugar for propagating errors.
## Takeaway Questions
How much of your code is error handling?
# Nonexclusive mutable references & Implicit data stuctures
Function authors generally expect that reference arguments are different objects, and that they are the only ones modifying them.
To retain a reference to an object is to create an implicit data structure. If A must refer to B: A and B should be parts of an explicit data struture, C. C should provide B to A when A needs it. This is already true at the process level. But invariants are much easier to maintain at the class level.
If you hesitate to replace pointers (or references) with ids because ids can be fabricated and used to break encapsulation: Understand that the same can be done with pointers. Basically, a private id into a pool is as good as a private instance. The difference being that an id can only be used when given a separate access mechanism.
## Takeaway Questions
How complicated is the implicit data structure maintained by your application process?
Comments
Post a Comment