You’ll see

  • Basic modular knowledge
  • Meaning of interface separation
  • Explore dependency injection, service locator patterns
  • Swinject is basically used
  • Thinking about what a good project architecture would look like
  • Based on iOS + Swift, but not limited to that, peel it off and think from a software engineering perspective

About Modularity

  • Modular design refers to the design method of dividing and designing a series of functional modules based on the functional analysis of different functions or products with different performance and specifications of the same function within a certain range. Different products can be formed through the selection and combination of modules to meet the different needs of the market
  • I think the most important reason for adopting modularization is to facilitate testing. After modularization, each module is not dependent on each other, so we can take out separate modules for testing, and it will not be inconvenient to test because of mutual dependence

Dependency injection

  • SOLID is the five basic principles of object-oriented design, designed to improve the maintainability and extensibility of programs

  • Dependency injection is an implementation of the dependency inversion principle

conception

  • We first try to distinguish between dependency inversion, inversion of control, and dependency injection

Dependency Inversion Principle (DIP)

  • Wikipedia: Dependency inversion principle

  • 👆 This chart shows:

    • Figure 1 shows that A depends on B
    • Figure 2 shows dependency inversion. We no longer rely on B directly, but on an abstract interface A, which B needs to implement
    • The advantage is that A and B are no longer bound together. When we need to make A depend on other classes, we no longer need to modify the code in A. We just need to make those classes implement interface A
  • Example of lamp and button: example

Inversion of Control (IC)

  • Wikipedia: Inversion of control
  • Dependency inversion (DIP) is when A no longer relies on B to implement, but B implements A’s interface
  • Inversion of control (IOC) is when A no longer has control over B, that is, it does not initialize B in A
  • Here I think it is not so clear, there is also a view that dependency inversion has included the process of inversion of control

Dependency Injection (DI)

  • A very interesting popular science article:Discussion on inversion of control and dependency injection
    • Once you understand the concept of inversion of control, you can understand dependency injection, which is essentially a way to achieve inversion of control

    • We can understand it as follows:

      • If A depends on B, under normal circumstances, we need to make A dependent on C, and we need to modify the internal code of A to make it dependent on C
        • Now, with the abstract interface A’, we just need to inject C into A’, the new dependency
  • Differentiating three concepts: Dependency inversion (DIP), Inversion of Control (IOC) and dependency injection (DI)

Practice: Implement dependency injection using protocols

  • Before we get down to business, let’s look at no dependency injection, no decoupling

The 🌰 of 👇 comes from GitHub: Swinject

// There is no decoupling
class Cat {
    let name: String

    init(name: String) {
        self.name = name
    }

    func sound(a) -> String {
        "Miao!"}}class Person {
    let pet = Cat(name: "sd")

    func play(a) -> String {
        "I'm playing with \(pet.name). \(pet.sound())"}}let per = Person(a)print(per.play())    // Output I'm playing with Sd.Miao!

// Now, suppose I need to raise a dog called KB, whose bark is Wang, what should I do?
// In fact, in this case, I have to create/modify the Person object to make it work
// Using the principle above, now my Person depends on Cat and the two are bound
Copy the code
  • Let’s look at dependency injection using protocol coupling
// Use the protocol for decoupling
protocol AnimalType {
    var name: String { get }
    func sound(a) -> String
}

class Cat: AnimalType {
    let name: String

    init(name: String) {
        self.name = name
    }

    func sound(a) -> String {
        "Miao"}}class Person {
    let pet: AnimalType

    init(pet: AnimalType) {
        self.pet = pet
    }

    func play(a) -> String {
        "I'm playing with \(pet.name). \(pet.sound())"}}let catPerson = Person(pet: Cat(name: "sd"))
print(catPerson.play()) // Print "I'm playing with sd.miao"

If I want to get a dog, what should I do?
class Dog: AnimalType {
    let name: String

    init(name: String) {
        self.name = name
    }

    func sound(a) -> String {
        return "wang"}}let dogPerson = Person(pet: Dog(name: "kb"))
print(dogPerson.play()) // output "I'm playing with kb.wang"

// In this case, my Person only relies on the abstract interface AnimalType, and any PET that meets this interface will work
// This reduces the coupling and inverts the dependency
Copy the code

Combat: Use Swinject for dependency injection

  • Swinject is Swift’s lightweight dependency injection framework

Dependency injection (DI) is a software design pattern that implements inversion of control (IoC) to resolve dependencies. In this mode, Swinject helps split your application into loosely coupled components that can be more easily developed, tested, and maintained. Swinject is supported by the Swift common type system and first-class capabilities to easily and smoothly define application dependencies.

import UIKit
import Swinject

// MARK: - Defines classes, protocols, and containers
protocol Animal {
    var name: String? { get}}class Cat: Animal {
    let name: String?

    init(name: String?). {self.name = name
    }
}

protocol Person {
    func play(a)
}

class PetOwner: Person {
    let pet: Animal

    init(pet: Animal) {
        self.pet = pet
    }

    func play(a) {
        let name = pet.name ?? "someone"
        print("I'm playing with \(name).")}}The Container class represents a dependency injection Container that stores the registry of the service and retrieves the registry of the service that injected the dependency
class DIContainer {
    static let container: Container = {
        let con = Container(a)// First, register a service and component pair into a Container
        // In the container, the component is created as a factory by the registered Closure
        // In this example, Cat and PetOwner are component classes that implement the Animal and Person service protocols, respectively
        con.register(Animal.self) { _ in Cat(name: "sd") }
        con.register(Person.self) { r in
           PetOwner(pet: r.resolve(Animal.self)! }return con
    }()
}
// MARK: - Actual use
class TestViewController: UIViewController {
   
     let container = DIContainer.container
       override func viewDidLoad(a) {
           super.viewDidLoad()
            // Then get the instance of the service from the container.
           let per = container.resolve(Person.self)!
           per.play()
       }
}
Copy the code

Service Locator Pattern

  • Before moving into the service locator pattern, let’s clarify our understanding of abstract interfaces, which I’ll use throughout this article to implement the pattern concepts I’m going to describe
    • An abstract interface is something like the protocol in Swift, which simply states the functions that need to be implemented by the person who follows the protocol, but note that this is not necessary. Although it is a common practice, it does not mean that the service locator pattern, dependency injection, and so on need to be packaged with the abstract interface
    • Dependency injection, for example, is implemented in three common ways:
      • Constructor injection
      • Setter injection
      • Interface injection
    • Interface injection is just one way of doing this. It is not required, please note

concept

  • This section describes the Service Locator Pattern
  • We can imagine a singleton, hypothesis is called the service center, whenever an instance object is we need, we send our requirements to the center (abstract interface), and the instance objects returned by the service center to us, that is, between each other is not dependent on our class, all of the class depends on the service center, through its handover to contact each other
  • 👇 I will illustrate the situation with an example of 🌰 assembling a car, which will be used throughout the article

Example: Assemble a car using the service locator pattern

  • Let’s simplify the composition of a car by assuming that it consists of only tires, doors, and lights

  • Now we want to start with the tire assembly, and the tire is made up of tread glue, wire ring, pad glue and steel ring

  • Now we want to start with tread glue, which is made up of raw materials A, B and C

  • Assuming that raw materials A, B and C are no longer separable, our service center is like A raw material market at this time. There are no finished products such as tires and doors, nor semi-finished products such as tread glue and steel wire bands, but only the most basic raw materials A, B, C and D…

  • The process of assembling the car is to ask the service center for raw materials again and again, first to spell out the tread glue, and then to assemble the raw materials to spell out the steel wire belt, so that one layer from bottom to top, continuously gather the raw materials to spell out the final car. All of the components in this process really don’t depend on each other, reduce the coupling, and all of the components, at whatever level they depend on or indirectly depend on, are the raw material markets

  • Now, we have received a request to add an openDoor method to CarDoor. The requirement is to open the door and turn on the car lights at the same time. For example, we need to call an openLight method in CarLight. Since CarDoor does not rely on CarLight, it needs to bridge with Market (CarLight is simplified here for the convenience of drawing, assuming that it is composed of raw materials D, E and F).

  • Since only the lowest raw material D, F and D are directly related to our Market, we need to know the internal implementation of CarLight to achieve this effect
  • In programming world terms, the service center contains too much stuff, too low-level, too abstract for us to call. Use this car for example, in order to realize turn on the light, we may need to wick, filament, bulbs are operated separately, turn on the light can only be achieved in this matter, and now just open the door turn on the light, if we give the tyres to join the function of the tire rolling lights, the need to resume the process over again [unless we make tires to rely on light, use the function of the lamp, But this is a departure from the principle of minimizing dependency.

Using an Assembly

  • Now, we introduce a new concept, abandon the previous concept of service center, and use a more standardized, standard set of processes to implement the car example, which I personally think is a better modular implementation

Agreed terms

  • XxxInterface: indicates an abstract interface
  • Servicexxx: Integrate abstract interfaces
  • XxxAssembly: The xxxAssembly
  • The Container: the Container

Example: Build a car using an assembler

  • We still have to assemble the car, but now we introduce the concept of a frame, which integrates the tires, the doors, the lights into a car

  • It doesn’t look like anything has changed, does it? Because even though the frame helps us integrate the parts, the parts have to come together from the bottom
  • There is a very important idea in modularity, which can be compared to the t-diagram of compilation principles.

  • We may think of one material as a T, but several T forms will also form a new T, and the new T can also be used as a component to be assembled with others. In the case of the car, the car has a frame, the tires have a tire frame, the doors have a door frame, and they’re all layered together from top to bottom

  • Forget about the abstract interface, add the container and look at the full implementation of the raw material level:

  • Let’s forget about abstract interfaces and containers for a moment and consider the benefits of the shelf concept:
    • The logic of assembling a car has become top-down, with clear and intuitive logic (as an engineer, he must hope to assemble the car from the perspective of the car in front of the design drawing, rather than a series of raw materials such as steel and rubber, and build a car from the bottom up).
    • The association between various components is more specific, and it is no longer necessary to schedule low-level raw materials (in other words, if we want to realize the above mentioned function of opening the door and turning on the light, we do not need to use Container for bridging, but CarAssembly for bridging, using specific functions).
    • Modularity is more thorough
      • A good project architecture should be like lego, where each component is a building block and can be inserted and removed at will
      • We can make a four-wheeler, we can make a six-wheeler, and the wheels can be filled with rubber, or we can make them with wood
  • Such an architecture from a vehicle perspective can still not see the advantage of where, but if we are assembling a Transformer level project, such an architecture is very necessary 🌝

Modularity from the perspective of software engineering

In this module, we will introduce the concept of abstract interface, and talk about modularity and interface separation in a more macro way, instead of using the example of car 👆, because the diagram is too complicated for me to understand 🌚

Optional dependence

Before software can be reusable, it first has to be usable.

  • The original code has no limitations. Each class can depend on whomever it wants. The code is about once, and adding a function may not be as easy as rewriting it because of the high coupling

  • Such code has little reuse because there are so many dependencies that if you want to use one module of code, you need to accept the same dependencies. Similarly, if you want to change one part of the code, you also need to change the code of the dependent parts

Out of dependence

If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization.

  • Therefore, we began to try to separate the dependency. Each module no longer directly depends on other modules, but first declares its own required dependency interface, and relies on external dependency injection

  • To be continued (when writing, I found myself feeling is not enough, still need to practice)