Introduction
It’s been a while since I last wrote about a design principle, so today, let’s explore the Principle of Least Astonishment (POLA), also known as the Principle of Least Surprise, and its impact on code and software engineering in general.
Although this principle can be applied to many fields, it originated as a guideline for describing computer interaction. This includes the interaction between users and computers, where the principle is applied to user interface design, and the interaction between programmers and the APIs of libraries or frameworks—where it applies directly to code.
In this discussion, I’ll focus on the latter: how to write code that adheres to POLA by aligning with expected behaviors and established object-oriented programming (OOP) principles. I’ll also illustrate various types of POLA violations through examples in code.
The Principle Of Least Astonishment
In programming, the Principle of Least Astonishment aims to minimize the cognitive load for anyone using a system for the first time. It does so by ensuring their expectations, shaped by prior experiences with similar systems or programming paradigms, are met. These expectations are often grounded in widely accepted conventions or established principles.
When POLA is violated, the consequences don’t typically affect the original author of the code. Instead, they impact others who must read, modify, or use the code. This is why POLA violations are common among new programmers or individuals unused to collaborative work environments. These developers may prioritize speed over code readability, inadvertently making their work harder for others to understand.
The benefits of adhering to POLA are most evident in team environments, where investing time in writing intuitive code improves the productivity of the entire group. Similarly, when developing libraries or frameworks, adhering to POLA reduces the time required to explain API intricacies, making it easier for users to adopt and work with the system.
POLA Violations
Command Query Separation
A common violation of the Principle of Least Astonishment (POLA) occurs when methods perform two distinct tasks: they both return a result based on the state of some objects and simultaneously modify the state of the same or another object. This also violates the Tell Don’t Ask principle which I have discussed in detail in a previous post: The Tell Don’t Ask Principle
Ambiguous Functionality
Another POLA violation arises when methods do not align with the functionality implied by their names. This can happen if they introduce unexpected side effects or deviate from standard naming conventions.
For example, in C#, methods that transform an object without memory allocation typically use the as
prefix, whereas methods that allocate new objects use the to
prefix. Creating a method with an as
prefix that unexpectedly allocates a new object would violate POLA.
Non-Standard Coding Conventions
Every programming language or paradigm has established coding conventions followed by its community. These conventions are usually documented and publicly available. For instance, Microsoft provides a set of Common C# code conventions.
Beyond language-wide conventions, individual teams often adopt specific conventions tailored to their codebase. A new team member should make an effort to learn these conventions, either by asking questions or by studying the existing codebase, and then adapt their coding style to match the team’s practices.
Type Conversions
In my post on Type Conversions And The Implicit and Explicit Operators In C# I explain when custom types should override the implicit and explicit operators. Violating these rules, such as implementing an implicit conversion that results in data loss, also constitutes a POLA violation.
Naming
Naming is one of the most challenging aspects of programming and is a frequent source of POLA violations. The issue has two main components: the meaning conveyed by the name of a variable or method, and the notation used for that name.
The second issue, notation, is relatively straightforward, as it is typically governed by language-specific conventions.
The first issue, however, is far more complex. A name that seems clear to the original developer, who understands the code’s context, may confuse others who encounter it later. Countless books provide guidelines for selecting meaningful names, and while programmers improve with experience, naming things correctly remains an ongoing challenge. It’s difficult to achieve perfect clarity 100% of the time.
Code Examples
For the remainder of this post, I will provide or link to code examples that violate the Principle of Least Astonishment and explain the consequences of each violation.
Different Behavior Depending On The Order Of Operations In Short-Circuit OR Boolean Operator
As mentioned earlier, violating command-query separation can negatively impact our code. One example is a method that performs a query and returns a boolean value while also modifying the state of an object. Such a method can introduce unexpected behavior in the program, especially when used in a short-circuit OR (||)
operation. The behavior will vary depending on whether the method is used as the first or second operand.
Here’s an example:
public class Person
{
private readonly DateTime _dateOfBirth;
public Person(DateTime dateOfBirth) => _dateOfBirth = dateOfBirth;
public int Age { get; private set; }
public bool IsAdult()
{
Age = DateTime.Today.Year - _dateOfBirth.Year;
if (_dateOfBirth.Date > DateTime.Today.AddYears(-Age))
Age--;
return Age >= 18;
}
}
The IsAdult
method in this example performs both a command—by calculating the Age
of the Person
and updating a field of the object, and a query, by checking if the age is greater than 18. When used as the first operand in a short-circuit OR
operation, its behavior can lead to unexpected side effects:
Person Bob = new(new DateTime(2000, 1, 1));
if(Bob.IsAdult() || true)
Console.WriteLine(Bob.Age);
If Bob.IsAdult()
is used as the first operand, we will get the expected result: 24
. However, if it is the second operand and the first operand evaluates to true
, the Age
will never be calculated, resulting in a value of zero.
This behavior is entirely unexpected, as one would reasonably assume the result to be the same regardless of the position of Bob.IsAdult()
in the operation. This is a clear violation of the Principle of Least Astonishment (POLA).
Implied If Statements
Another example of a POLA violation, and a personal favorite of mine, is code that relies on implied if statements. These are conditional statements that don’t exist explicitly in the code but only in the programmer’s mind. Any logic that is not explicitly written in the code is a violation of POLA, as nothing should ever be implied. Code should always be explicit, with everything clearly written, not in comments, but directly in the code itself.
For a more detailed explanation of implied if statements, you can read my post: What Are The Implied If Statements In Code
Overload VS Default Parameter
In C#, a POLA violation can occur when default parameters are used instead of method overloads in code compiled independently from the code that consumes it, such as in a library.
Default parameters are “baked into” the caller’s code at compile time. Any changes to these default values will not take effect unless the consuming code is recompiled, even if no changes have been made to the consuming code itself.
For more information on why this can be problematic, you can read more in Prefer overloading than default parameters and Optional Parameters In Constructor Vs Init Properties
Command VS Strategy
Another example of code that violates the POLA can be found in my post about the Command vs Strategy pattern.
Each programming pattern has its own conventions, and when implementing the strategy pattern, using objects named SomethingCommand
and implementing an ICommand
interface with a method called Execute
will confuse anyone familiar with these patterns. They will expect to find an Invoker class, which won’t exist.
This violates the established naming conventions used in traditional patterns. While it may not violate naming notation or the descriptions of the roles of each object or method, using names that are traditionally associated with other well-known principles, patterns, or methodologies to describe operations unrelated to them will confuse anyone reading your code.
Execution Order Of Awake And OnEnable Undefined
Here’s another POLA violation that can occur in the Unity game engine: the order in which the Awake
and OnEnable
event methods execute for different objects is undefined. Not only is the order of execution undefined, but it can also vary between builds or even different versions of the engine.
Relying on the order of execution between these methods may work in your current build, but it will undoubtedly surprise anyone trying to build the game when it stops working. You can read more about this issue in Execution order of Awake and onEnable for different scripts in Unity is undefined
Awaitable VS AwaitableTask
Another example of a POLA violation, this time related to OOP rather than C#, can be found in my previous post, How To Create Custom Async Return Types In Unity.
There I create a type AwaitableTask
to be used as a return type from an async method. This type encapsulates a Task
operation. The same code could have worked by returning Awaitable
, but having a type that will have a different behavior, without its user have a way to know what this behavior will be, in this case mutating the behavior of the Awaitable
type from always respecting Unity’s game loop, to sometimes respecting it and sometimes not, will confuse and violate the expectations of its users.
Unity’s Vector3 Equality
Finally, let’s look at a real-world violation of the POLA. In Unity’s API, the Vector3 type, which represents a three-dimensional vector with float parameters, has an overload for the == operator: Vector3.operator ==. The description says that this operation “Returns true if two vectors are approximately equal. To allow for floating point inaccuracies, the two vectors are considered equal if the magnitude of their difference is less than 1e-5.”
Overloading the ==
operator for floats
or Vector3
’s in this way is mathematically unsound and can lead to serious bugs.
We know that checking equality with floats isn’t reliable due to precision issues, so instead, we check if values are “close enough”. However, this approximation IS NOT true equality.
Equality has a transitive property: if a == b
and b == c
, then a == c
. By overloading the ==
operator, Unity has created a concept of equality that lacks this transitive property. Anyone who hasn’t read this specific implementation in the Unity manual, unaware that “equals” here doesn’t really mean equals might end up confused when a != c
in his code.
This is precisely why floating-point numbers don’t override the ==
operator to check for “near enough” but instead leave this decision to the programmers.
Here’s a code example:
public class Vector3Equality : MonoBehaviour
{
private Vector3 a = new(0, 0, 0);
private Vector3 b = new(0, 0, 0 + 0.000009f);
private Vector3 c = new(0, 0, 0 + 2 * 0.000009f);
private void Start()
{
Debug.Log(a==b); // true
Debug.Log(b==c); // true
Debug.Log(a==c); // false !!!!!
}
}
Obviously this cannot change now, because it will be a breaking API change that will break a lot of projects. It should never have been there. The correct approach would have been to have an ApproximatelyEquals
method or maybe overload the Equals
method, which still I wouldn’t be a fun of, but overloading the ==
operator that for mathematical types is reserved for mathematical equality is a huge violation of math, let alone POLA.
Bad Unity! Bad!
Conclusion
As you’ve seen in this post, the Principle of Least Astonishment is not an isolated principle, it relies on the correct application of conventions, rules, and principles that are already well established and expected to be followed in any codebase. Even when a shortcut seems like it might save you time, always consider whether it violates the POLA. Such a violation could confuse your colleagues or users of your API, ultimately wasting their time.
As always, thank you for reading, and if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.