User:DKinzler (WMF)/Software Design Practices

Lemmas

 * Two components that have a (public) cyclic dependency (directly or indirectly) do not behave like separate components, but like a single component: when changing one, we can not be sure that the other does not need changing too.
 * Modularization can be achieved by removing cyclic dependencies between components.
 * Most objects should be either immutable values or "stateless" services. Auxiliary types of objects are mutable value objects, builders, and cursors (which include iterators and streams). Some commons kinds of services include factories/registries and persistence services.
 * Operations offered by services should be idempotent (with the exception of persistence services).
 * Only value objects should be "newable", services should never be instantiated directly by application logic. Cursors should be obtained from factory services.
 * The code for instantiating services constitutes the "wiring" of the application, and should be isolated as much as possible from application logic.
 * Dependency injection: Services shall ask for all required configuration and all required services in their constructor. Optional services and configuration that has defaults may use setters. Services should not rely on global state, any reference to a global variable or call to a non-pure static function is technical debt.
 * Interfaces make bad extension points, because they cannot be changed without breaking implementations; use abstract base classes instead.
 * Only pure functions shall be static methods (or global or namespaced functions).
 * Subclassing should never be used just to share code. Use composition and traits for code sharing instead. Static composition (one service-like object directly instantiating another service-like object in its constructor) is acceptable, but injection is generally preferred.
 * Classes from another component should never be extended (subclassed), unless this is explicitly allowed in the documentation of this class. This restriction means that protected members of a class are not per default part of the public interface of the module, and that the behavior of instances of the class are fully under the control of the module.
 * Service containers should never be injected into service objects, because doing so introduces a dependency on all services. Generic configuration objects should never be injected into service objects, because doing so introduces a dependency on all settings.
 * Lazy initialization in value objects is acceptable and sometimes necessary, but should not be taken lightly: hidden cost and hidden failure modes can cause hard to find issues. There should always be a non-lazy alternative (e.g. a plain value implementation).
 * There shall be no cyclic dependencies between modules, public or private. That is, for code to be split into a separate module, all of the dependencies it has on the code that remains in the old module needs to be removed.
 * Cyclic runtime dependencies between modules are ok, but should be handled with care, and should be documented when expected/desired.
 * There shall be no cyclic public dependencies between any components (modules, namespaces, classes, etc).
 * Cyclic private dependencies between components are acceptable (but still undesirable) if the components are members (sub-components) of the same higher level component (e.g. classes in the same namespace may depend on each other internally, methods of the same class may call each other internally, etc).
 * In the namespace hierarchy, it's also acceptable for classes in (transitive) parent namespaces and (transitive) child namespaces to depend on each other internally (but never publicly).
 * In the namespace hierarchy, classes in a parent namespace should have no public dependency on classes in a child (or descendant) namespace. Classes in a child namespace may however have public dependencies on classes from a parent (or ancestor) namespace.
 * All classes are considered part of their defining module's public interface, unless they are marked as internal. Internal classes are still part of the public interface of their namespace. They however must not be referenced from outside the module.
 * Knowledge locality (aka single source of truth) improves code stability. This means for instance that, while separation of concerns tells us that there should be separate interfaces for serialization and deserialization, a concrete class implementing a specific serialization format may well implement both interfaces, so the knowledge about ancoding and decoding a given format resides in a single place, rather than in two classes.
 * To provide confidence in interfaces, the documented contract of interface methods should document the expected interaction between methods. For instance, the contract of a put method on an interface for hash maps may specify that the value passed to the put method is guaranteed to be returned by the get method when called with the same key.
 * Interface contracts should be enforced by a compliance test. Traits can be used to conveniently apply the same test cases to all implementations of an interface.
 * TBD: Amorphous parameters and extensibility

Definitions

 * Component: a collection of code with a well defined public interface. A typical example of a component are classes, but namespaces, modules, and even individual functions can be considered components.
 * Module (aka package, aka library, aka bundle): a set of components that share a versioning history and release cycle. In practice, this usually means things that are in a single git repo. In some cases, a single repo may contain multiple modules (or proto-modules) with the intention to move them into separate repos in the future.
 * Public dependency: any mention of (or knowledge of) another component in the public interface of a component. Public dependencies constitute tight coupling. Note that protected methods shall be considered part of the public interface of a class. The constructor signature of a class is considered part of the public interface if the class is considered "newable".
 * Private dependency (internal dependency, implementation dependency): mention of (or knowledge of) another component merely in the private code or declarations of a component.
 * Runtime dependency: once component using another component at runtime.
 * Newable classes: classes that application logic may instantiate directly, as opposed to requesting an instance from a factory or from a service container.
 * TBD: Static entry point:
 * TBD: Service container:
 * TBD: Service locator:
 * TBD: Internal class:

Strategies
To resolve a dependency of component X that is to be moved to a new module N on components from the old module M, we can:


 * factor out: we move any components used by both X and M to jet another new module K. Now, both M and N can depend on K. This creates a tight coupling of M to N (and of both N and M on K).
 * abstract out: for the relevant component C in M that is needed by X, introduce an interface C' that C implements. This interface may then live in the new modules N, or in a module K that both M and N depend on. This creates a tight coupling of M to N (and potentially K).
 * translate: for the relevant component C in M that is needed by X, introduce an alternative D for use in N. Code in M now has to translate C to D (and vice versa) when calling code in N. This creates a loose coupling, providing isolation of vocabularies on the domain boundary, but also means writing and touching more code. This should be used when M and N model distinct domains, rather than M using the domain modeled by N directly.
 * hide mutability: A mutable class may implement an immutable interface that can be exposed to consumers that must not be able to modify the object. Such consumers may also operate on a copy of the state exposed by the immutable interface. Note however that such an interface only guarantees immutability in one direction: it guarantees that the consumer cannot modify the object, but it does not guarantee to the consumer that the object cannot change due to side effects triggered by actions the consumer takes. Code that requires such a guarantee would have to create an immutable copy. For this purpose, such immutable interfaces should always have a truly immutable implementation as well as the original, mutable implementation.