AHdark

AHdark Blog

Senior high school student with a deep passion for coding. Driven by a love for problem-solving, I’m diving into algorithms while honing my skills in TypeScript, Rust, and Golang.
telegram
tg_channel
twitter
github

Reduce code coupling through control inversion

Perhaps you have heard while learning Spring that the container provided by Spring is called the IoC container. IoC stands for Inversion of Control, which translates to 控制反转 in Chinese. Inversion of Control is a design principle that was proposed by Martin Fowler as early as 2004, referring to the reversal of obtaining dependent objects.

Problems with Non-Inversion of Control#

Most applications are implemented through the cooperation of two or more classes or components to achieve business logic, which requires each object to obtain references to the objects it collaborates with (i.e., the objects it depends on). If this acquisition process relies on the object itself, it will lead to high coupling in the code, making it difficult to maintain and debug.

Direct Instantiation and Tight Coupling#

For example, if component A needs to call B, you might typically explicitly create the B component in A's constructor or init block and then call it.

class A {
    private val b = new B();

    public fun doSomething() {
        b.doSomething();
    }
}

This approach means that Class A has a direct, compile-time dependency on Class B. Since Class A directly creates an instance of Class B, it results in tight coupling between A and B. Class A not only needs to know about the existence of Class B but also how to create an instance of B, which may include knowing the constructor parameters of B. This coupling makes it more difficult to modify, test, and reuse A and B.

If there is a base class B that needs to add a parameter (Class C) as a dependency, two situations may arise:

  1. Adding a dependency with a default value, which creates a new dependency from B to C, increasing the project's coupling.
  2. Not adding a default value, which requires modifying all places that call B, leading to a destructive modification.

Unfortunately, this kind of coupling is very common.

Difficulty in Unit Testing#

package example

func newDB() *DB {
    return &DB{}
}

func DoSomething() {
    db := newDB()
    db.Write("Hello, World!")
}

// test
package example

import "testing"

func TestDoSomething(t *testing.T) {
    db := &MockDB{}
    DoSomething() // Cannot inject MockDB
}

Most production environment projects require detailed unit tests, and this coupling makes unit testing difficult. Because when testing A, you also have to test B, which is no longer a unit test. Moreover, if we need to inject a mock object, we would need to modify A's constructor, which violates the Open/Closed Principle.

Management Responsibility Issues#

In a non-IoC architecture, classes must manage their dependencies themselves.

This includes creating dependent objects, managing their lifecycle, and handling any dependency-related configurations. This approach requires developers to have a deep understanding of the application's structure and dependencies. In team development, this can increase communication costs among team members and raise the requirements for individual developers' capabilities.

Inversion of Control#

Inversion of Control achieves decoupling by transferring the control of object creation and binding from program code to an external container or framework. In traditional program design, the flow of the program is controlled by the program itself, while with IoC, this control is inverted: the creation and lifecycle of objects are no longer controlled by the caller but managed by the IoC container.

For example, after using Spring, the above code can be modified as follows:

@Component
class B {
    public fun doSomething() {
        println("Hello, World!")
    }
}

@Component
class A {
    @Autowired
    private val b: B;

    public fun doSomething() {
        b.doSomething();
    }
}

This approach means that Class A is no longer responsible for creating an instance of Class B; instead, the IoC container is responsible for creating and managing the instance of B. This greatly reduces the coupling between A and B, as A no longer needs to know the constructor parameters of B or how to create an instance of B. This makes A and B easier to modify, test, and reuse.

Dependency Injection#

Dependency injection is one way to implement Inversion of Control. In dependency injection, the dependencies of an object are no longer created and managed by the object itself but are created and managed by an external container. Common methods of dependency injection include constructor injection, property injection, and method injection.

The @Autowired annotation in Spring is a form of property injection. In Spring, you can use the @Autowired annotation to mark a property, and the Spring container will automatically inject an instance for that property. (As above)

The dependency injection methods often mentioned in Go are Wire1 and Fx2. Fx is a dependency injection framework I commonly use, which provides construction methods through fx.Provide and dependencies through fx.Invoke, achieving dependency injection during the global initialization phase. Wire differs from Fx in that it is a code generation tool that generates dependency injection code at compile time using the wire command, while Fx relies on reflection to achieve dynamic injection. Fx belongs to the aforementioned constructor injection method.

package example

import (
    "go.uber.org/fx"
)

// db.go

type DB struct {}

func NewDB() *DB {
    return &DB{}
}

// service.go

type Service interface {
    Write(msg string)
}

func NewService(db *DB) Service {
    return &service{db: db}
}

type service struct {
    db *DB
}

func (s *Service) Write(msg string) {
    s.db.Write(msg)
}

// main.go

func main() {
    app := fx.New(
        fx.Provide(NewDB),
        fx.Provide(NewService),
        fx.Invoke(func(s Service) {
            s.Write("Hello, World!")
        }),
    )

    app.Run()
}

Additionally, in Kotlin, the Koin framework3 can also be used for dependency injection, which uses DSL syntax and is very suitable for lightweight projects.

val appModule = module {
    single { MyRepository() }
    factory { MyViewModel(get()) }
}

Advantages and Disadvantages#

Inversion of Control can fundamentally solve the aforementioned problems by managing object creation and lifecycle through frameworks or containers, thereby achieving decoupling.

However, it also has significant drawbacks:

  1. Inversion of Control is highly dependent on frameworks, making it difficult for a project to detach from its IoC container, complicating project migration and upgrades.
  2. For developers who have not used IoC, the concept of Inversion of Control may be difficult to understand.
  3. Inversion of Control can increase project complexity, leading to longer project startup times (for IoC containers implemented with reflection).

Conclusion#

Although IoC brings many benefits, it also introduces a certain learning curve and project complexity. Choosing the right IoC framework or container, as well as designing and implementing dependency injection reasonably, is crucial for successfully leveraging IoC principles. As technology evolves, IoC frameworks and tools continue to evolve, providing more flexibility and better performance, making Inversion of Control an indispensable part of modern software development.

Whether in Java's Spring framework, Go's Fx framework, or Kotlin's Koin framework, IoC has become a key technology for achieving high-quality, maintainable, and scalable software systems. By effectively utilizing these tools and principles, developers can build software systems with higher maintainability and scalability.

Understanding and applying Inversion of Control is not just about learning a new technology; it represents a shift in thinking. It requires developers to think holistically about the architecture of applications, focusing on decoupling between components, thereby improving the quality and development efficiency of software projects. Over time, mastering Inversion of Control will become an important part of every software engineer's skill set.

References#

Footnotes#

  1. Wire: https://github.com/google/wire

  2. Go
    Fx: https://pkg.go.dev/go.uber.org/fx https://uber-go.github.io/fx/

  3. Koin: https://insert-koin.io

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.