Action at a Distance.
Action at a distance is an antipattern in which code in one part of a program silently changes the behavior of a distant, unrelated part.
Action at a distance is an anti-pattern in computer science in which behavior in one part of a program varies wildly based on difficult or impossible to identify operations in another part of the program.
The way to avoid the problems associated with action at a distance is a proper design, which avoids global variables and alters data only in a controlled and local manner, or usage of a pure functional programming style with referential transparency.
The term is borrowed from physics, where action at a distance means that objects influence each other without any visible intermediary.
Software bugs due to action at a distance may arise because a program component is doing something at the wrong time, or affecting something it should not. It is very difficult, however, to track down which component is responsible. Side effects from innocent actions can put the program in an unknown state, so local data is not necessarily local. The solution in this particular scenario is to define which components should be interacting with which others. A proper design that accurately defines the interface between parts of a program, and that avoids shared states, can largely eliminate problems caused by action at a distance.
Historical example: Perl’s $[
This example, from the Perl programming language, demonstrates an especially serious case of action at a distance (note the $[ variable was deprecated in later versions of Perl):
Array indices normally begin at 0 because the value of
$[is normally 0; if you set$[to 1, then arrays start at 1, which makes Fortran programmers happy, and so we see examples like this in theperl(3)man page:foreach $num ($[ .. $#entry) { print " $num\t'",$entry[$num],"'\n"; }And of course you could set
$[to 17 to have arrays start at some random number such as 17 or 4 instead of at 0 or 1. This was a great way to sabotage module authors.Fortunately, sanity prevailed. These features are now recognized to have been mistakes. The perl5-porters mailing list now has a catchphrase for such features: they're called "action at a distance". The principle is that a declaration in one part of the program shouldn't drastically and invisibly alter the behavior of some other part of the program.
— Mark Jason Dominus, Sins of Perl Revisited
Action at a distance across objects
Proper object-oriented programming involves design principles that avoid action at a distance.
The Law of Demeter states that an object should only interact with other objects near itself. Should action in a distant part of the system be required then it should be implemented by propagating a message. Proper design severely limits occurrences of action at a distance, contributing to maintainable programs. Pressure to create an object orgy results from poor interface design, perhaps taking the form of a God object, not implementing true objects, or failing to heed the Law of Demeter.
One of the advantages of functional programming is that action at a distance is de-emphasised, sometimes to the point of being impossible to express at all in the source language.
Being aware of the danger of allowing action at a distance into a design, and being able to recognize the presence of action at a distance, is useful in developing programs that are correct, reliable and maintainable. Given that the majority of the expense of a program may be in the maintenance phase, and that action at a distance makes maintenance difficult, expensive and error prone, it is worth effort during design to avoid.
Example
Here the report module changes a shared, mutable settings dictionary. Nothing in invoices.py changes, yet after the first CSV export every invoice is suddenly printed with four decimal places.
# settings.py
DEFAULTS = {"currency": "USD", "precision": 2}
# invoices.py
from settings import DEFAULTS
def format_amount(amount):
return f"{amount:.{DEFAULTS['precision']}f} {DEFAULTS['currency']}"
# reports.py
from invoices import format_amount
from settings import DEFAULTS
def export_csv(rows):
DEFAULTS["precision"] = 4 # every invoice printed afterwards changes too
return [format_amount(r) for r in rows]
The format is now an immutable value that is passed explicitly:
from dataclasses import dataclass
@dataclass(frozen=True)
class Format:
currency: str = "USD"
precision: int = 2
def format_amount(amount, fmt=Format()):
return f"{amount:.{fmt.precision}f} {fmt.currency}"
def export_csv(rows):
fmt = Format(precision=4) # affects only this export
return [format_amount(r, fmt) for r in rows]
A change in one place can no longer leak into another; shared mutable globals like DEFAULTS are what the coupling literature calls common coupling (see Tight Coupling).
Adapted from the Wikipedia article “Action at a distance (computer programming)” (revision 1329485461), available under the CC BY-SA 4.0 license; the text has been edited and examples added.