The Doctrine of Useful Objects
This guide describes the standards and practices for implementation design that are used to build the Eventide toolkit and the examples provided for it by the Eventide team.
Having a basic understanding of the useful objects doctrine will facilitate understanding the design patterns and principles that are commonly employed throughout the various libraries and examples.
A useful object:
- Is usable immediately upon initialization without any nil reference errors resulting from uninitialized dependencies (eg:
NoMethodError, undefined method for nil:NilClass)
- Doesn't have any logic in its initializer other than assigning the value of initializer parameters to the object's instance variables
- Formalizes the difference between initializer arguments and setters, and the circumstances when one is used rather than the other
- Doesn't require a foreign mechanism outside of the class's own namespace (including inner namespaces) to initialize it and its dependencies (a.k.a.: an Inversion of Control container, etc)
- Doesn't invite the passing nils or dummy values to its initializer for the purposes of setting up the object for testing
- Doesn't rely on test doubles (stubs) to disengage dependencies that would cause undesirable side effects while exercising or otherwise testing it
- Doesn't rely on test doubles (mocks, spies) to be used to inspect an object's execution path
An object's dependencies are initialized by default to a safe, and inert (in terms of side effects) implementation of the dependency's interface.
An object's class interface provides a means of constructing an instance of the object, including the initialization of its dependencies to active, operational implementations of the dependencies' interfaces, eg: an active database connection, an active payment gateway client, etc.
An object may record telemetry about its execution as it is executing. The activation of telemetry instrumentation is optional.
The useful objects repository on GitHub includes an example implementation:
A dependency has an interface. An object that conforms to that interface (and its semantics) is substitutable for the dependency. That object is a substitute.
There is no notion of primary or secondary substitutes. All values that can be assigned to a dependency attribute are substitutable for each other. No single substitute has precedence over the other.
There is no real instance of a dependency versus a fake instance. These perspectives are purely circumstantial, and should be drummed out of the designer's mind as quickly as they threaten to alight.
Substitutability guarantees that all implementations of an interface that respect the interface's contract and intended semantics are no more valuable than any other implementation of the interface, and no less real than any other. All substitutes are real substitutes by the very nature of substitutability.
Null Object Dependencies and Useful Objects
The most basic safe and inert substitute for a dependency is a null object.
A null object substitute assigned as a default value of a dependency allows that dependency to be actuated without causing a nil reference error to be raised - which is a foundational tenet of useful objects.
A null object substitute is a legitimate substitute if all uses of the dependency don't cause a nil reference error to be raised.
Null objects can be either weak or strict. A weak null object responds to any invocation sent to it. A strict null object conforms to a specific interface and will raise no-method errors for any invocations that don't conform to that interface
class Something attr_reader :some_value attr_reader :some_other_value dependency :some_dependency, SomeDependency def initialize(some_value, some_other_value) @some_value = some_value @some_other_value = some_other_value end def self.build(some_object) new(some_object.some_value, some_object.some_other_value).tap do |instance| SomeDependency.configure(instance) end end end
# Using the initializer results in null object dependencies something = Something.new(some_value, some_other_value) puts something.some_dependency.class # => #<Class:0x007f9dcc0886f0> # Using the constructor (factory method) results in operational dependencies something = Something.build(some_obj) puts something.some_dependency.class # => SomeDependency
Primitive Initializer and Complex Constructor
The initializer typically accepts precisely the primitive data the object depends on directly to perform its behavior.
Any destructuring of more complex objects is provided by a factory method, or constructor, provided by the class interface (the build method, above).
By the time the initializer is invoked, the exact data needed by the object in order to do its work is supplied to the initializer. The initializer is not required to do any other work other than capturing the data as the object's instance variables.
The class constructor provides a convenience interface. It's the interface used to make invoking the initializer easier for the developer by not requiring that all of the initializer's individual arguments.
Secondary, Optional Dependencies
some_dependency dependency is not an appropriate initializer argument. It's a collaborator dependency (a better term would be service dependency, but that term is too overloaded to be helpful).
An instance of the
Something class needs the
some_dependency in order to fulfill its obligations at runtime, but it is not necessary to provide the operational implementation of the dependency in all cases (any substitute implementation - due to substitutability - is also a permissible value).
some_dependency can be optionally set to an operational instance of
SomeDependency, or to a substitute
Optional Dependencies and Default Null Object Substitutes
By default, the
some_dependency will be assigned a strict null object that is constructed to conform to the
A direct invocation of the initializer will not leave the
some_dependency attribute uninitialized as
The dependency will be assigned the default null object substitute that is constructed dynamically as a function of the
The value of
some_dependency that is needed in live operation (the operational implementation) is provided by the
build constructor (factory) method.
This constructor (
build) allows for the most convenient use of the class without commingling construction conveniences and machinations with the initializer, and leaves the initializer free of any responsibilities other than capturing only the essential data needed for the object's operation.
It's important to see substitutes as substitutes (in the substitutability property sense) rather than as stubs, or any other test double.
A stub is a concern of testing. A substitute is a bona fide concern of a class's operational design.
While stubs and substitutes may find themselves in similar roles at test time, the perspective that leads a design toward stubs is very different than the design perspective that leads to substitutes.
The goal of a stub is to remove undesirable side effects from the course of execution during a test. The goal of a substitute is to allow an object to be useful upon initialization of the object, without needing to use any other tool setup but the class's own interface.
The use of test doubles relegates the properties of usefulness and transparency to an afterthought of testing.
A test that uses a stub framework is explicitly calling out the design requirement to allow for an object's dependencies to be safe and inert substitutes. These are legitimate use cases of the object and its dependencies that should be accounted for by the design, and the exercise of them in this way is a priori concern of the design.
Configuration, not "Configuration"
The term configuration here does not refer to the kind of preference or settings data used to provide system-wide variables during start up of an application or service.
The use of the term configuration here refers to the configuration of an assemblage of collaborators. This is closer to the meaning of configuration more common to the actor model and actor systems. In this context, it refers to the assignment of collaborator dependencies to the objects that depend on them.
The job of configuring operational dependencies falls to the class's constructor (or, factory method), implemented above as the
Furthermore, it's the job of the dependency's class to decide how the dependency should be constructed, and how it should be assigned to the object that depends on it.
This line of code from the example above is where configuration is happening:
This form allows
SomeDependency to decide for itself how it should be constructed and to be assigned.
The simplest implementation would be:
class SomeDependency def self.configure(receiver) instance = new receiver.some_dependency = instance end end
This pattern is also an example of Tell, Don't Ask, which is a helpful pattern in preserving encapsulation, which in turn help limit the effects of some of the more harmful kinds of coupling.
In effect, tell the dependency's class to get an instance of itself (in whichever way it does that, which is not the business of the user of the class), and to assign it back to the instance of the object that has the dependency.
While this property might seem negligible, spread over the breadth of an application or system, and over the weeks, months, or years of a work on a system of objects, the cumulative effects of uncontrolled coupling accounts for much of the productivity slow down and increased costs that teams commonly experience.
Minimizing the Configuration Implementation
The above configuration implementation can be minimized with a macro.
Here is the
SomeDependency class's configuration minimized by use of a
class SomeDependency configure :some_dependency, constructor: :new end
Be warned, however, that doing this makes the class more obscure to first-glance intelligence - especially in situations where the configuration must be specialized or is more complicated.
It's arguable that pursuing this minimal implementation reduces the usability of this object even while the mechanical details of the boilerplate are reduced.
Objects are Behaviors First, and Data Second
Most objects in your system should be behavioral. This is a fundamental tenet of Object-Orientation, and really of most programming paradigms.
Behaviors aren't really supposed to be bolted on to data objects (a.k.a.: entities) as secondary features of data objects. But it's often the result when designers and developers see the system's data as the purpose of objects. This is more of a common mental quirk than an aspect of design.
The pervasiveness of Object-Relational persistence frameworks, and the use of objects to represent storage structures - especially rows in databases - exacerbates the realization of objects as primarily concerned with behavior rather than data.
The implication for design is that a good deal of the classes in your systems will reflect the Command Pattern. However, because this is the default use of objects, it doesn't need to be said that an object implements the command pattern. It's enough to say that an object is an object. This is the same as saying that an object is behavioral, or in other words, that an object is a command. In effect, it goes without saying that an object is a command. It should be more rare that an object is a data object or an entity (or an ActiveRecord object).
The implication is that objects rarely need to have "command" or "service" (just another name for a behavioral object) in their name. Subsequently, it's not necessary to have words like "do" or "execute" or "perform" in a class's name, or in any of its methods' names.
There are always exceptions, but they should be rare. If they're not rare, the design of the namespace should be examined for design flaws.
Unlike a data structure, an object is responsible for a single act. That act is set in motion by a single method. That method is termed, "actuator".
It's not necessary to give this method a name like "run", "perform", etc. Ruby already provides for implementing actuators with a method named
call. This built-in can be invoked implicitly as
some_object.call() will also work).
Behavioral objects have actuators. The objects' classes also have actuators. This duality parallels the relationship between a class's initializer and its
Once an object is instantiated, it is ready to be actuated, ie: to have its
call method invoked. As a convenience, the class also provides a
call method. The class actuator constructs the object, and invokes the instance actuator.
The above example elaborated with actuators:
class Something attr_reader :some_value attr_reader :some_other_value dependency :some_dependency, SomeDependency def initialize(some_value, some_other_value) @some_value = some_value @some_other_value = some_other_value end def self.build(some_object) new(some_object.some_value, some_object.some_other_value).tap do |instance| SomeDependency.configure instance end end def self.call(some_object) instance = build(some_object) instance.() end def call # Execute the object's raison d'etre, making use # of the object's attributes and dependencies end end
Minimizing the Primitive Initializer
The above primitive initializer is ultimately so simple that it could effectively be replaced with a macro:
class Something initializer :some_value, :some_other_value dependency :some_dependency, SomeDependency # ... end
In the above case, the
initializer macro would both generate the
initialize method and create the two attributes.
Note: While this is a possibility opened by making sure that initializers only capture initial data and do nothing else, representing an initializer and its attributes with such a macro is a personal choice. It has the drawback of being more obscure and esoteric. Such a thing should be standardized and socialized in a team so that it's self-evident for the developers who experience it.
Telemetry and Transparency
The ability for an object to provide insight into its own execution is something that should be accounted for by the design, and should be a first class citizen of design.
If the execution of an object is important enough to need to verify the execution in a test, then that transparency is a bona fide element of the design, rather than the responsibility of the user of the object (ie: test code).
Transparency is not of tests. It is used by tests. Transparency is of design.
Said otherwise: the presence of test doubles (mocks, stubs, spies) signifies that an object's design itself is not accounting for the use cases that it is engaged in. The object is being asked to do things that it's not designed for.
There's no doubt that a Ruby object can be brute-forced into such a thing, but that doesn't solve the problem of a class expressing its uses in its own code.
It should be clear to the reader of the class what the class's user are interested in terms of transparency, what telemetry is published during a class's execution, and how the instrumentation is activated.
Here's an elaboration of the implementation that includes a telemetry mechanism:
something = Something.build(some_obj) sink = Something.register_telemetry_sink(something) something.() assert(sink.recorded_something_done?)
module UsefulObjects class Something attr_reader :some_value attr_reader :some_other_value dependency :some_dependency, SomeDependency dependency :telemetry, ::Telemetry def initialize(some_value, some_other_value) @some_value = some_value @some_other_value = some_other_value end def self.build(some_object) new(some_object.some_value, some_object.some_other_value).tap do |instance| SomeDependency.configure instance ::Telemetry.configure instance end end def self.call(some_object) instance = build(some_object) instance.() end def call do_something end def do_something telemetry.record :something_done, some_value do_something_else end def do_something_else # ... end module Telemetry class Sink include ::Telemetry::Sink record :something_done end def self.sink Sink.new end end def self.register_telemetry_sink(something) sink = Telemetry.sink something.telemetry.register sink sink end end end
In addition to having dependencies being initialized to null object implementations, substitutes should also provide a means to override the inert null object with a concrete implementation of a substitute (or to specialize the null object).
Here's an example of a dependency with a concrete substitute substitute implementation (though a naive one that doesn't demonstrate a realistic case):
class SomeDependency configure :some_dependency, factory_method: :new def do_something do_some_destructive_side_effect end module Substitute def self.build SomeDependency.new end class SomeDependency < ::SomeDependency def do_something pretend_to_do_some_destructive_side_effect end end end end
Concrete Substitutes and Telemetry
If a concrete substitute also needs transparency, it can be instrumented with telemetry to record and expose the details of its execution:
something = Something.new(some_value, some_other_value) some_dependency = something.some_dependency sink = SomeDependency.register_telemetry_sink(some_dependency) assert(sink.recorded_something_done?)
class SomeDependency dependency :telemetry, ::Telemetry configure :some_dependency do new.tap do |instance| ::Telemetry.configure instance end end def do_something do_some_destructive_side_effect end def do_some_destructive_side_effect # ... end module Telemetry class Sink include ::Telemetry::Sink record :something_done end def self.sink Sink.new end end def self.register_telemetry_sink(something) sink = Telemetry.sink something.telemetry.register sink sink end module Substitute def self.build SomeDependency.build end class SomeDependency < UsefulObjects::SomeDependency attr_accessor :sink def self.build new.tap do |instance| ::Telemetry.configure instance end end def do_something telemetry.record :something_done pretend_to_do_some_destructive_side_effect end def pretend_to_do_some_destructive_side_effect # ... end end end end
Note: The registration of telemetry can also be done during the construction of the substitute.
Class Interface and Convenience Versus Object Interface and Precision
Illustrated by both the class constructor and class actuator, a class interface is a convenience affordance. It allows for instantiation or actuation of an object in a way that is most convenient to the developer.
The instance interface is structured to express the greatest extent of precision and exactness without consideration for developer convenience.
The object interface is correct. The class interface is an ease of use provision that does the work of providing more exact and precise arguments to the object interface.
The class interface insulates the object interface from the encroachment of imprecision that comes from a developer's desire to have ease of use.
For example, it's common (while imprecise and incorrect) for ruby developers to pass a hash of values to an initializer, and then copy the hash's values to the instantiated object's instance variables. This would be an example of an initializer that does not offer the precision appropriate to an instance interface.
Instead, an initializer should only receive exactly the data that will be assigned to the object's instance variables, without any destructuring of more complex objects, like a hash.
By providing a class constructor, the initializer's precision can be preserved, while also providing a convenience on the class's interface that can destructure the hash and invoke the initializer with the appropriate level of exactitude and precision.
The following example illustrates the principles:
class Something def initialize(some_arg, some_other_arg, another_arg) @some_arg = some_arg @some_other_arg = some_other_arg @another_arg = another_arg end def self.build(hash) new( hash[:some_arg], hash[:some_other_arg], hash[:another_arg] ) end end
Anticipated Objection: This Approach Causes Too Much Boilerplate Code
Boilerplate can be seen as clutter that is obscuring the deeper meaning and purpose of the implementation. However, there's a tipping point beyond which further reduction of mechanical code makes the object's operation incomprehensible.
While counter-intuitive to many developers, the reduction of in the amount of code is usually not a factor in increasing productivity. In fact, the opposite is true, and is often plainly observable through static analysis of design and code.
The unavoidable side-effect of reducing certain kinds of boilerplate is an increase in abstractness. With an increase in abstractness comes the increase of afferent coupling to abstract members, and with that comes the rigidity that has a compounding effect on the time it takes to get work done. Abstractness is not a gift that keeps on giving. It's a specific countermeasure for a specific set of problems. Used outside of those problems, it creates more problems than it solves.
It can't be over-emphasized that the things that need to be done to a design to reduce mechanical boilerplate code can be as harmful to design as they are helpful.
There is only a certain amount of boilerplate reduction that can be afforded before efforts to reduce boilerplate create more subtle, but more costly and more entrenched problems.
Seeing the same patterns of code repeatedly can trigger the Don't Repeat Yourself instinct. Deeper inspection of design factors as well as the Don't Repeat Yourself adage itself can show that boilerplate reduction is not the intention of the guidance provided by the adage.
Rather than reduce mere boilerplate code, look for variations on patterns and see if you can eliminate them. Even if the result is that the same pattern is repeated throughout your code. It's the special variation that is more costly to work with.
Any programmer at any level can reduce perceived duplication. It's not difficult, and it's not an unassailable goal of design - except when it does not create countervailing problems. And that assessment is a matter of a judgment call that has to take into effect the unique conditions of the particular system being worked.
If you reduce duplication of code patterns indiscriminately, you'll end up creating a framework from which critical business logic cannot be extricated when the framework becomes too cumbersome to continue justifying its use. While not all frameworks end up facing this fate, it's far more common than not, as framework developers struggle to maintain adoption and relevance as time goes passes by adding more features and specializations through abstraction rather than by extension (ie: plain old vendor lock-in).
In effect, the abstractions created by the pre-mature boilerplate reduction can cause the use of the programming language to diverge so far from the language's own foundations that learning the framework becomes an exercise in effectively learning a new language. This in itself is an example of the kind of special variation that must be rigorously controlled rather than automatically indulged.
While having to code boilerplate code can seen tedious and annoying, that's all it is. The avoidance of tedium is not a sufficient risk to the design's structural qualities to warrant its indulgence reflexively.
Necessary, irreducible boilerplate is just one of those things that we need to face as programmers with increased patience and a shift in focus from short term gains to long-term sustainability and continuity.
Boilerplate - as long as its instances in various classes have avoided variation, and as long as consistency is rigorously protected - is a very minimal impact on productivity and quality.