Dependency injection
≈ 14 min read
Names in other languages
No names in other languages are held for this entry yet.
Article bodies are English and are not machine-translated. A name in another language is not a translation of the article.
In software engineering, dependency injection is a programming technique in which an object or function receives other objects or functions that it requires, as opposed to creating them internally. Dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs. The pattern ensures that an object or function that wants to use a given service should not have to know how to construct those services. Instead, the receiving "client" (object or function) is provided with its dependencies by external code (an "injector"), which it is not aware of. Dependency injection makes implicit dependencies explicit and helps solve the following problems: * How can a class be independent from the creation of the objects it depends on? * How can an application and the objects it uses support different configurations? Dependency injection is often used to keep code in-line with the dependency inversion principle. In statically typed languages using dependency injection means that a client only needs to declare the interfaces of the services it uses, rather than their concrete implementations, making it easier to change which services are used at runtime without recompiling. Application frameworks often combine dependency injection with inversion of control.…
In software engineering, dependency injection is a programming technique in which an object or function receives other objects or functions that it requires, as opposed to creating them internally. Dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs. The pattern ensures that an object or function that wants to use a given service should not have to know how to construct those services. Instead, the receiving "client" (object or function) is provided with its dependencies by external code (an "injector"), which it is not aware of. Dependency injection makes implicit dependencies explicit and helps solve the following problems: How can a class be independent from the creation of the objects it depends on? How can an application and the objects it uses support different configurations? Dependency injection is often used to keep code in-line with the dependency inversion principle. In statically typed languages using dependency injection means that a client only needs to declare the interfaces of the services it uses, rather than their concrete implementations, making it easier to change which services are used at runtime without recompiling. Application frameworks often combine dependency injection with inversion of control. Under inversion of control, the framework first constructs an object (such as a controller), and then passes control flow to it. With dependency injection, the framework also instantiates the dependencies declared by the application object (often in the constructor method's parameters), and passes the dependencies into the object. Dependency injection implements the idea of "inverting control over the implementations of dependencies", which is why certain Java frameworks generically name the concept "inversion of control" (not to be confused with inversion of control flow). Because dependency injection separates how objects are constructed from how they are used, it often diminishes the importance of the new keyword found in most object-oriented languages. Because the framework handles creating services, the programmer tends to only directly construct value objects which represent entities in the program's domain (such as an Employee object in a business app or an Order object in a shopping app). public class Client { private Service service; Client() { // The dependency is hard-coded. this.service = new ExampleService(); } } public class Client { private Service service; // The dependency is injected through a constructor. Client(Service service) { if (service == null) { throw new IllegalArgumentException("service must not be null"); } this.service = service; } } // a concrete singleton class class DatabaseConnection { private static DatabaseConnection instance; private DatabaseConnection() { // database resource initialization } public static synchronized DatabaseConnection instance() { if (instance == null) { instance = new DatabaseConnection(); } return instance; } public void executeQuery(String sql) { System.out.printf("Executing: %s%n"); } } // a tightly-coupled consumer class class OrderService { public void placeOrder(String orderId) { // hardcoded dependency lookup makes this impossible to unit test DatabaseConnection db = DatabaseConnection.instance(); db.executeQuery(String.format("INSERT INTO orders VALUES ('%s')")); } } Dependency injection avoids asking how the database is created, or how many instances exist, and only asks for the Database interface in its constructor: // an abstract interface interface Database { void executeQuery(String sql); } // implement the concrete class (no singleton logic) class SqlDatabase implements Database { @Override public void executeQuery(String sql) { System.out.printf("Executing SQL: %s%n", sql); } } // the consumer class accepts dependency via constructor injection class OrderService { private final Database db; // The dependency is injected from the outside public OrderService(Database db) { this.db = db; } public void placeOrder(String orderId) { db.executeQuery(String.format("INSERT INTO orders VALUES ('%s')", orderId)); } } public class Client { public void performAction(Service service) { if (service == null) { throw new IllegalArgumentException("service must not be null"); } service.execute(); } } public class Client { private Service service; // The dependency is injected through a setter method. public void setService(Service service) { if (service == null) { throw new IllegalArgumentException("service must not be null"); } this.service = service; } } package org.wikipedia.examples; import java.util.HashSet; import java.util.Set; interface ServiceSetter { void setService(Service service); } class Client implements ServiceSetter { private Service service; @Override public void setService(Service service) { if (service == null) { throw new IllegalArgumentException("service must not be null"); } this.service = service; } } class ServiceInjector { private final Set clients = new HashSet<>(); public void inject(ServiceSetter client) { this.clients.add(client); client.setService(new ExampleService()); } public void switch() { for (Client client : this.clients) { client.setService(new AnotherExampleService()); } } } class ExampleService implements Service {} class AnotherExampleService implements Service {} public class Program { public static void main(String[] args) { // Build the service. Service service = new ExampleService(); // Inject the service into the client. Client client = new Client(service); // Use the objects. System.out.println(client.greet()); } } Manual construction may be more complex and involve builders, factories, or other construction patterns. @Inject , for marking injectable constructors, methods, and fields @Named , for marking a string-based qualifier @Qualifier , for marking qualifier annotations @Scope , for marking scope annotations @Singleton , for marking a type subject to the singleton pattern by the injector In .NET (such as C#), the Microsoft.Extensions.DependencyInjection library is a bundled framework within .NET. Some frameworks, like Spring, can use external configuration files to plan program composition: package org.wikipedia.examples; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Injector { public static void main(String[] args) { // Details about which concrete service to use are stored in configuration separate from the program itself. BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml"); Client client = (Client) beanfactory.getBean("client"); System.out.println(client.greet()); } } Even with a potentially long and complex object graph, the only class mentioned in code is the entry point, in this case Client . Client has not undergone any changes to work with Spring and remains a POJO. By keeping Spring-specific annotations and calls from spreading out among many classes, the system stays only loosely dependent on Spring. // greeter.service.ts import { Injectable, inject } from '@angular/core'; import { DOCUMENT } from '@angular/common'; export interface IGreeterService { greet(text: string): void; } @Injectable({ providedIn: 'root' }) export class GreeterService implements IGreeterService { // Use inject(DOCUMENT) and access defaultView to get the Window object private window: Window = inject(DOCUMENT).defaultView as Window; greet(text: string): void { this.window.alert(text); } } Hello // my-controller.component.ts import { GreeterService } from './greeter.service.ts'; import { Component, inject } from '@angular/core'; @Component({ selector: 'app-my-controller', template: 'my-controller.component.html' }) export class MyControllerComponent { private greeter = inject(GreeterService); sayHello(): void { this.greeter.greet('Hello World'); } } The inject function triggers the injector to create an instance of the GreeterService and its dependencies (in this case, a Document instance to access the window object). import std; class DatabaseConnection { public: void connect() { std::println("Connecting to database..."); } }; class DatabaseService { private: DatabaseConnection& conn; public: explicit DatabaseService(DatabaseConnection& db): conn{db} {} void execute() { conn.connect(); std::println("Executing database service..."); } }; int main(int argc, char* argv[]) { DatabaseConnection db; DatabaseService sv(db); sv.execute(); } This sample provides an example of interface injection. import std; using std::expected; using std::shared_ptr; using std::unexpected; class IConnection { public: enum class Error { NO_CONNECTION, // more errors here }; virtual void connect() = 0; virtual ~IConnection() = default; }; class DatabaseConnection: public IConnection { public: DatabaseConnection() = default; void connect() override { std::println("Connecting to database..."); } }; class DatabaseService { private: shared_ptr conn; public: DatabaseService() = default; void setConnection(shared_ptr nextConn) noexcept { conn = nextConn; } expected execute() { if (conn) { conn->connect(); std::println("Executing database service..."); } else { return unexpected(IConnection::Error::NO_CONNECTION); } } }; int main(int argc, char* argv[]) { shared_ptr db = std::make_shared (); DatabaseService sv; sv.setConnection(db); sv.execute(); } Historically, because C++ lacked reflection, libraries could not "see" constructor signatures or class member types, forcing the usage of manual dependency injection or metaprogramming libraries such as Boost.DI to manually map arguments to dependency injections. This is simplified greatly using C++26's function parameter reflection and annotations. import std; using std::string; class IDatabase { /* ... */ }; class FastDatabase : public IDatabase { /* ... */ }; class BasicDatabase : public IDatabase { /* ... */ }; class Foo { private: // ... public: =Inject Foo(const IDatabase& primary, const IDatabase& secondary, const string& s, int i) { // ... } }; class Bar { private: // ... public: =Inject Bar(const IDatabase& primary, int i) { // ... } }; int main(int argc, char* argv[]) { Injector injector = Injector ::Builder() .addService("primary", std::make_unique ()) .addService("secondary", std::make_unique ()) .build(); Foo foo = injector.build ("abc", 1); Bar bar = injector.build (2); } namespace Wikipedia.Examples; using System; // Our client will only know about this interface, not which specific gamepad it is using. interface IGamePadFunctionality { string GetGamePadName(); void SetVibrationPower(float power); } // The following services provide concrete implementations of the above interface. class XboxGamePad : IGamePadFunctionality { float vibrationPower = 1.0f; public string GetGamePadName() => "Xbox controller"; public void SetVibrationPower(float power) => this.vibrationPower = Math.Clamp(power, 0.0f, 1.0f); } class PlayStationJoystick : IGamePadFunctionality { float vibratingPower = 100.0f; public string GetGamePadName() => "PlayStation controller"; public void SetVibrationPower(float power) => this.vibratingPower = Math.Clamp(power * 100.0f, 0.0f, 100.0f); } class SteamController : IGamePadFunctionality { double vibrating = 1.0; public string GetGamePadName() => "Steam controller"; public void SetVibrationPower(float power) => this.vibrating = Convert.ToDouble(Math.Clamp(power, 0.0f, 1.0f)); } // This class is the client which receives a service. class GamePad { IGamePadFunctionality gamePadFunctionality; // The service is injected through the constructor and stored in the above field. public GamePad(IGamePadFunctionality gamePadFunctionality) => this.gamePadFunctionality = gamePadFunctionality; public void Showcase() { // The injected service is used. string gamePadName = this.gamePadFunctionality.GetGamePadName(); string message = $"We're using the {gamePadName} right now, do you want to change the vibrating power?"; Console.WriteLine(message); } } class Program { static void Main(string[] args) { SteamController steamController = new(); // We could have also passed in an XboxController, PlayStationJoystick, etc. // The gamepad doesn't know what it's using and doesn't need to. GamePad gamepad = new(steamController); gamepad.Showcase(); } } package router import ( "database/sql" "net/http" "example/controllers/users" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/redis/go-redis/v9" "github.com/rs/zerolog" ) type RoutingHandler struct { // passing the values by pointer further down the call stack // means we won't create a new copy, saving memory log *zerolog.Logger db *sql.DB cache *redis.Client router chi.Router } // connection, logger and cache initialized usually in the main function func NewRouter( log *zerolog.Logger, db *sql.DB, cache *redis.Client, ) (r *RoutingHandler) { rtr := chi.NewRouter() return &RoutingHandler{ log: log, db: db, cache: cache, router: rtr, } } func (r *RoutingHandler) SetupUsersRoutes() { uc := users.NewController(r.log, r.db, r.cache) r.router.Get("/users/:name", func(w http.ResponseWriter, r *http.Request) { uc.Get(w, r) }) } Then, the private fields of the struct can be accessed in any method that is its pointer receiver, without violating encapsulation. package users import ( "database/sql" "net/http" "example/models" "github.com/go-chi/chi/v5" "github.com/redis/go-redis/v9" "github.com/rs/zerolog" ) type Controller struct { log *zerolog.Logger storage models.UserStorage cache *redis.Client } func NewController(log *zerolog.Logger, db *sql.DB, cache *redis.Client) *Controller { return &Controller{ log: log, storage: models.NewUserStorage(db), cache: cache, } } func (uc *Controller) Get(w http.ResponseWriter, r *http.Request) { // note that we can also wrap logging in a middleware, this is for demonstration purposes uc.log.Info().Msg("Getting user") userParam := chi.URLParam(r, "name") var user *models.User // get the user from the cache err := uc.cache.Get(r.Context(), userParam).Scan(&user) if err != nil { uc.log.Error().Err(err).Msg("Error getting user from cache. Retrieving from SQL storage") } user, err = uc.storage.Get(r.Context(), "johndoe") if err != nil { uc.log.Error().Err(err).Msg("Error getting user from SQL storage") http.Error(w, "Internal server error", http.StatusInternalServerError) return } } Finally, the database connection initialized in the main function can be used at the data access layer: package models import ( "database/sql" "time" ) type ( UserStorage struct { conn *sql.DB } User struct { Name string 'json:"name" db:"name,primarykey"' JoinedAt time.Time 'json:"joined_at" db:"joined_at"' Email string 'json:"email" db:"email"' } ) func NewUserStorage(conn *sql.DB) *UserStorage { return &UserStorage{ conn: conn, } } func (us *UserStorage) Get(name string) (user *User, err error) { // assuming 'name' is a unique key query := "SELECT * FROM users WHERE name = $1" if err := us.conn.QueryRow(query, name).Scan(&user); err != nil { return nil, err } return user, nil } trait Logger { fn log(&self, message: &str); } struct ConsoleLogger; impl Logger for ConsoleLogger { fn log(&self, message: &str) { println!("{message}"); } } struct UserService { logger: L, } impl UserService { fn new(logger: L) -> Self { Self { logger } } fn create_user(&self, name: &str) { self.logger.log(&format!("Creating user: {name}")); // Business logic... } } fn main() { let logger = ConsoleLogger; let service = UserService::new(logger); service.create_user("Alice"); } However, it can also be done using dynamic dispatch with trait objects: trait Logger { fn log(&self, message: &str); } struct ConsoleLogger; impl Logger for ConsoleLogger { fn log(&self, message: &str) { println!("{message}"); } } struct UserService { logger: Box , } impl UserService { fn new(logger: Box ) -> Self { Self { logger } } fn create_user(&self, name: &str) { self.logger.log(&format!("Creating user: {name}")); } } fn main() { let logger = Box::new(ConsoleLogger); let service = UserService::new(logger); service.create_user("Alice"); }
Text derived from https://en.wikipedia.org/w/index.php?oldid=1368839814 under CC BY-SA 4.0; modifications have been made.