My previous post discusses the use of interfaces and base classes to break coupling between c lasses. Although it touches on it briefly it does not discuss in depth the creation of instances. This is an important topic in its own right. Done incorrectly you will get no decrease in coupling between classes. However there is not a single approach that is applicable to all type categories within a single application and the requirement to eliminate coupling likewise varies between these categories.
From the perspective of instantiation we can consider types to be organised into a number of key categories. The first is what I call structural types. This category covers the elements such as services and repositories that form the static structure of the application. These types are typified by being stateless and for commonly having dependencies on other structural types. The arrangement of these types may generally be determined by configuration or code introspection and in most cases does not change significantly over the lifetime of the application process.
Dependency injection is the preferred mechanism for instantiating structural types. This offloads potentially involved dependency creation to a framework specifically tailored to this purpose. This frees types from any knowledge of the implementation of their dependencies, allowing them to deal with the contracts only. This reduces coupling and has significant benefits for testability. It also allows for advanced scenarios such as interception. Modern dependency injection frameworks will support autowiring and registration of types through reflection which significantly reduces the configuration burden of establishing the software structure.
If you cannot use dependency injection you may instead chose to create structural types using the Gang of Four factory patterns. This has the advantage that it places all the construction logic into a cohesive location and will only support the construction of valid instances. It does however suffer from increased complexity and coupling over using dependency injection and will generally require more code to achieve the same result.
Direct instantiation of structural types suffers from very high coupling and poor testability. It is to be avoided and can cause significant maintainability concerns in larger systems.
The second category I identify are persistent types. These are objects that store state that has a significant lifetime, generally covering more than one operation. The state may not be kept in the same object instance for this entire period but is recognised to be the same data throughout its lifetime. The canonical example is a domain object that is mapped to a persistent data store such as a database. Over its lifetime a domain object can expect to be retrieved multiple times from the database into different object instances. This may occur in different processes or on different systems but is always considered to be the same domain instance.
The responsibility for creation of a persistent type instance varies depending on the stage in the lifecycle of the underlying state. A new entity at the beginning of its lifecycle may be created through the object constructor or through the use of factory patterns. This choice may be made on the complexity of the construction process and whether the object is an aggregate root. It is appropriate to create these objects directly as the creation will be performed from data available in the current thread of execution (for instance from received user input). Involvement of a dependency injection framework would not add any benefits to this type of object and may impose unacceptable restrictions.
A persistent object that represents existing state should be instantiated by infrastructure code. Requiring application code to perform such construction is generally a sign that the infrastructure is inadequate or being improperly used. For instance a domain object representing state persisted to a data store should be created by the data access infrastructure. Generally this should be an ORM. Custom code to create these objects is inefficient as the ORM should possess all the information required to rehydrate the data from the data store.
My final primary category are transient objects. These are objects used in the course of an operation and then discarded. Creating these directly via their constructors is in most cases a perfectly valid way of operating. Factories may also be used but are rarer for this class of object.
There are some special cases with transient objects. For instance many forms of data transfer object may be best created by mapping infrastructure such as AutoMapper. This does not significantly reduce coupling in the system but does represent significantly reduced effort when using multiple data representations as is common in layered systems.