1. Introduction
Software development has grown increasingly dependent on external libraries that are built by other companies. While it provides convenience in development, it significantly increases the cost of software maintenance, especially for the changes that involve the dependency with libraries [
1]. The use of external libraries also requires significant overheads, such as extra code to be imported and compiled, resulting in performance bottlenecks. Additionally, external libraries can introduce security vulnerabilities unknown to the developers using those libraries. These issues are exacerbated if the external libraries are open source and maintained by the community, resulting in inconsistent updates and lack of maintenance from the original developers.
Component frameworks (e.g., Spring [
2]) help mitigate development cost. A key feature of component frameworks for object-oriented programming (OOP) is dependency injection (DI). DI is a pattern of sending (“injecting”) necessary fields (“dependencies”) into an object, instead of requiring the object to initialize those fields itself. Existing literature [
3,
4,
5,
6] suggests that the use of DI can help improve the maintainability of software systems. On the other hand, there are also warnings against using DI due to possible negative effects [
7,
8].
A software quality metric often used to measure maintainability is coupling between objects (CBO) [
9]. CBO is the total number of couplings present within the software system, or the sum of the system’s afferent couplings (CA) and efferent couplings (CE). CA counts how many other classes use the class being analyzed, while CE counts how many classes the class being analyzed uses. Therefore, when a coupling exists between two objects, the object being depended on will increase its CA value by 1, and the object depending on the other object will increase its CE value by 1, generating an overall CBO value of 2. Generally, higher CBO yields lower maintainability because of the increased complexity of the system.
In this paper, we present two novel metrics, dependency injection-weighted afferent couplings (DCE) and dependency injection-weighted coupling between objects (DCBO), to analyze DI and assess the impact of DI on software maintainability and its tool support, CKJM-Analyzer [
10], which is an extension of the CKJM tool [
9]. DCE weighs each efferent coupling depending on whether it is soft-coupled (e.g., with DI) or hard-coupled (e.g., with the
keyword, or with using an object generator that requires parameter information from the user). DCBO utilizes DCE in place of CE as a weighted metric of overall coupling. CKJM-Analyzer is a cross-platform command line interface (CLI) with two primary goals—(i) develop a standard operating procedure to iteratively analyze Java projects for CKJM metrics and (ii) count the instances of the DI pattern in Java projects to determine the DI proportion. We validate the metric and tool with a set of open-source Java projects.
The remainder of the paper is organized as follows.
Section 2 presents a background on DI.
Section 3 describes DCBO and its algorithmic approach implemented in the CKJM-Analyzer tool.
Section 4 describes the evaluation of CKJM-Analyzer on experimentally generated projects and open-source projects.
Section 5 discusses the results with regards to the impact of DI in maintainability, the effect of DCBO on coupling analysis, the limitations and potential future work.
Section 6 gives an overview of the related work on the effect of DI in software systems, as well as work in measuring coupling weight.
Section 7 concludes the paper with discussion on future research work.
2. Dependency Injection
DI is a specific form of the
dependency inversion principle [
4], which is a pattern that suggests that higher-level objects should dictate most of the complex logic in the system and also create dependencies for lower-level objects to use. DI is a subset of this principle because it highlights how lower-level objects should rely on higher-level objects for their dependencies. DI is a design pattern to improve the maintainability of software systems by reducing developer effort in adding coupling through injecting dependencies in classes using an external injector which is a class object or file (e.g, an XML-based configuration file in Spring Framework [
2]). As coupling is reduced, consequently the complexity of classes is also diminished. DI also makes it easier to pinpoint dependency-related errors as dependency injection is localized in one place (viz. the injector).
A dependency is typically injected in four ways [
4]: (i) via constructor parameters, which is known as
constructor no default (CND); (ii) via method parameters, which is known as
method no default (MND); (iii) via constructor parameters or a default object (using the
new command), which is known as
constructor with default (CWD); and (iv) via method parameters or via a default object, which is known as
method with default (MWD). Consider the code snippets below. The
Dog class has no dependency injection, the
DogPenCND class implements CND, the
DogPenMND class implements MND, the
DogPenCWD class implements CWD, and the
DogPenMWD class implements MWD. Note that CWD extends on CND functionality, and MWD extends on MND functionality (not shown in the code snippet for brevity).
public class Dog {
Dog() {}
}
public class DogPenCND { public class DogPenMND {
Dog dog; Dog dog;
DogPenCND(Dog dog) { void AddDog(Dog dog) {
this.dog = dog; this.dog = dog;
} }
} }
public class DogPenCWD { public class DogPenMWD {
Dog dog; Dog dog;
DogPenCWD() { void AddDog() {
this.dog = new Dog(); this.dog = new Dog();
} }
} }
Typically, the implementation of services is specified in the injector, which is often used as a clue for the use of DI. When a change needs to be made in the service, it is done through the injector without changing the client. In this way, the client remains unchanged, and thus, the code becomes more flexible and reusable. Consider the code snippet below. The DogPenGeneratorCND class acts as the injector, injecting the Dog class into each DogPenCND object. In this way, each different DogPenCND object does not need to generate its own Dog dependency. This is particularly helpful when the same dependency is injected in multiple different objects. The CND and MND structures follow this benefit.
public class DogPenGeneratorCND() {
Dog dog = new Dog("Dog1");
DogPenCND pen1 = new DogPenCND(dog);
DogPenCND pen2 = new DogPenCND(dog);
DogPenCND pen3 = new DogPenCND(dog);
}
In contrast, the CWD and MWD structures do not entirely follow the injector benefit. Consider the code snippet below. If developers use the “default” constructor or method available in the CWD/MWD structures, it requires the object itself to generate its own dependency object.
public class DogPenGeneratorCWD() {
DogPenCWD pen1 = new DogPenCWD();
DogPenCWD pen2 = new DogPenCWD();
DogPenCWD pen3 = new DogPenCWD();
}
In our work, we also analyze a fifth way of injecting dependencies using beans provided by Spring Framework [
2]. Beans represent concrete classes implementing an interface and are configured in an XML file. Any class can inject those beans directly into its constructor or setter functions. Consider the example XML configuration snippet below, where a concrete class “ConcreteClass” implements “AbstractClass” is wired up to an identifier “object”.
<?xml version="1.0" encoding="UTF-8"?>
<!--spring.xml file-->
<beans ...>
<bean id="object" class="path.to.ConcreteClass"/>
</beans>
Consider the code snippet below. Any application can inject that bean using the identifier “object”, simplifying object injection. Additionally, developers can create other concrete classes implementing the “AbstractClass” interface and switch out the original “ConcreteClass” through the XML file alone, avoiding unnecessary compilation and code changes.
ApplicationContext appContext = new ClassPathXmlApplicationContext
("spring.xml");
AbstractClass obj = (AbstractClass) appContext.getBean("object");
DI comes with a few technical hindrances. Firstly, it requires all the dependencies to be resolved before compilation if the compiler is not configured to recognize injected dependencies. That is, the compiler cannot recognize the presence of injected dependencies unless it is configured. Secondly, the frameworks built upon DI are often implemented with reflection or dynamic programming, which can hinder the use of IDE automation, such as reference finding, call hierarchy displaying, and safe refactoring [
11].