User:DKinzler (WMF)/Software Design Practices

Lemmas

 * Two components that have a (public) circular 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 is the process of decoupling by removing circular dependencies between components.
 * The two preferred types of objects are immutable values and "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.
 * Service containers should never be injected into service objects. Generic configuration objects should never be injected into service objects.
 * Lazy initialization in value objects is acceptable and sometimes necessary, but should not be taken lightly. There should always be a non-lazy alternative (e.g. an alternative implementation).
 * There shall be no circular dependencies between modules, public or private. That is, for code to be split into a separate component, all of the dependencies it has on the code that remains in the old module needs to be removed.
 * Circular runtime dependencies between modules are ok, but should be handled with care, and should be documented when expected/desired.
 * There shall be no circular public dependencies between any components (modules, namespaces, classes, functions, etc).
 * Circular private dependencies may exists between 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 not publically). There shall be no circular dependencies (public or private) between sibling or more remotely related namespaces (nor between unrelated namespaces, of course).
 * 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 the 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.