application in loosely coupled architectures

What is a data bus?

A Data Bus is a communication system that enables the exchange of data between different components in an application. It acts as a central hub for data flow, allowing components to be decoupled from each other. This means that each component can send and receive data over the bus without having direct knowledge of other components, allowing components to remain independent of each other.

How does a data bus work?

Let's take a closer look and analyze the image below:

The data bus operates according to the following basic principles:

  1. Centralized event management: The data bus manages all events in the system, providing order and control over the flow of information. This makes the system more predictable and manageable.

  2. Publisher-subscriber model: In this model, components can send (publish) and receive (subscribe to) events. Publishers send events to the data bus, and subscribers receive notifications of new events to which they are subscribed. This reduces the direct dependency between components.

  3. Transferring data by key and value: Each data packet sent or received is accompanied by a key that identifies the type or category of data. This allows subscribers to receive only the data they need by filtering it by keys, making the system more organized and efficient.

Example of using data bus

Let's look at an example in Swift where the data bus is used to manage events in an application. We invite the hungriest developers to join us at GitHub.

ShopApp is a simulation of an app for buying products. The app is written in SwiftUI and in this example I have analyzed in detail how the data bus can be used.

Note: It is important to understand what the data bus is used for in the project, this will be described in detail in the section “Using the data bus in a real project”.

Demonstration you can take a look here. And for those who are too lazy to watch the demo, the images below show an example of the application with the product catalog tab open:

Example of an application with the basket tab open:

Now that the example has been analyzed, we can move on to the narrative. Let's move on to examining the code.

Sending value by key

This code sends a signal to start the application via the data bus when the interface appears. Let's look at the code and analyze it in more detail:

var body: some Scene {
    WindowGroup {
        TabView {
            // Ваша основная логика здесь
        }
        .onAppear() {
            Bus.send(K.didLaunch, true)
        }
    }
}

Inside onAppear there is a method call Bus.send. This method sends a message or signal to the data bus. In this case, a key is used K.didLaunch and meaning true.

Description of parameters:

  • key K.didLaunch: This key defines the type of event that is sent to the data bus. In this case, it signals that the application has been launched.

  • meaning true: The value associated with this event. In this context, it indicates that an action (such as launching an application) has been successfully completed.

At every appearance TabView on the screen, the data bus receives a signal about the application launch. This allows other system components subscribed to the key K.didLaunchlearn about the event and perform appropriate actions. The presented approach centralizes event management in the application, reducing dependencies between components and simplifying data flow management.

Getting a value by key

Here the application receives a start signal via the data bus using a key K.didLaunch. Let's look at the code and analyze it in more detail:

func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: …
) -> Bool {
    Bus.receiveAsync(&subscriptions, [K.didLaunch]) { (_, v: Bool) in
        // Обработка события запуска
    }
    // Другая логика запуска приложения
}

Inside the method application(_:didFinishLaunchingWithOptions:) call is used Bus.receiveAsyncwhich allows you to subscribe to events that occur in the data bus. This method asynchronously receives data and processes it when it becomes available.

Description:

  • Parameter &subscriptions is a collection of subscriptions that stores references to active subscriptions. This is important for managing the lifecycle of subscriptions so that they do not continue to exist when they are no longer needed.

  • Array [K.didLaunch] contains the event keys that the application subscribes to. In this case, the key is K.didLaunchwhich signals that the application has been launched.

  • Closure (_, v: Bool) in: executed when the event corresponding to the key K.didLaunchoccurs in the data bus. In this case, v is a value of type Boolwhich indicates that the application was launched successfully. Inside the closure, you can define logic that will be executed when the application is signaled to start. For example, perform any actions required at startup.

Getting data in UI

This example uses the data bus to retrieve and display the number of items in the shopping cart in the UI:

struct CartView: View {
    @StateObject private var products = BusUI.Value(K.cartList, [String]())
    var body: some View {
        VStack {
            // Логика отображения корзины
        }
        .badge(products.v.count) // Отображение количества товаров в корзине
    }
}

When the quantity of items in the cart changes (e.g. the user adds or removes an item), the data bus updates the value associated with the key K.cartList. Since the object products subscribed to this key, it automatically receives updated data and updates its state. SwiftUI, in turn, detects the state change and redraws the UI, displaying the current number of products on the badge.

This code illustrates how you can integrate a data bus with a SwiftUI UI. Using BusUI.Value and a subscription to the key K.cartListyou can easily display dynamic data in the interface, ensuring that the UI is updated whenever the data in the bus changes. This simplifies state management and reduces coupling between components, since data can come from a central bus rather than directly between components.

Key organization

An important part of our implementation of the data bus architecture is the standardization of the keys used to interact with this bus. In our project, this is achieved using a special enum that centralizes key management and ensures orderly access to data.

This can be clearly seen in the example below. The following enum is used to store keys:

enum K {
    static let addToCart = "ShopApp.addToCart"
    static let removeFromCart = "ShopApp.removeFromCart"
    static let cartList = "ShopApp.cartList"
    static let initialProducts = "ShopApp.initialProducts"
    static let products = "ShopApp.products"
    static let didLaunch = "ShopApp.didLaunch"
    static let clearCart = "ShopApp.clearCart"
}

In essence, enum K acts as a single source of truth for all keys used in the data bus. This means that all application components interacting with the bus access a set of keys implemented within their module. Such centralization not only simplifies management, but also reduces the likelihood of errors associated with incorrect or inconsistent use of keys in different parts of the project.

In the context of a modular architecture, where different parts of an application can be developed and maintained by separate teams, standardizing keys via enum becomes especially important. Each module can work independently with the data bus without worrying about what keys other modules use. This makes it easy to integrate new modules or change existing ones without disrupting the entire system.

Adding new keys to enum K is easy and safe, allowing the project to grow and adapt to new requirements. At the same time, the overall structure and standards remain unchanged, which contributes to the maintainability and scalability of the system in the long term.

Now let's briefly run through the pros and cons of the data bus.

Benefits of using a data bus

  • Decoupled components: Application components are less dependent on each other, making them easier to change and test.

  • Easy event management: Centralized event management makes it easy to add and remove handlers.

  • Improved code readability: Code becomes cleaner and more understandable.

  • Flexibility in data handling: It's easy to add new events and handlers without changing existing code.

  • Reduced dependencies between components: Components interact through a common interface, making it easy to replace or update individual components.

  • Increased scalability and maintainability: Adding new features or components becomes easier and less risky.

Disadvantages of using a data bus

  • Difficulty in debugging: Debugging events can be difficult because events are processed in different parts of the application and their sequence can be difficult to follow.

  • Increased architectural complexity: Adding a data bus increases the overall complexity of the application architecture, requiring additional planning and management.

  • Increased Bus Dependency: If not designed correctly, components can become overly dependent on the data bus, making them difficult to test and reuse.

  • Implicit dependencies: Because interactions between components occur through a bus, dependencies become less obvious, making the system more difficult to understand and maintain.

Using the data bus in a real project

A little background: A while ago, our iOS team switched to using separate UIWindow for individual modules in order to reduce the influence of the visual hierarchy of some modules on others. For example, within the framework of this approach, we have Alert You can show it from anywhere, because it has its own UIWindow.

Let's take a close look at the following image:

What was the problem? There are two different modules in the application – Authorization and Login by link. Each module has its own UIWindow. In a certain scenario, it turned out that Authorization blocked Login by link, although it should have been the other way around. The most straightforward solution was to increase the modules' knowledge of each other. But I absolutely did not want to do this, so as not to accidentally catch new bugs.

At this point, the fastest and least risky solution seemed to be to use the Bus in such a way that the Authorization and Login modules would report the display of their UIWindowand the third module – Window Priority – corrected the visibility of windows depending on their priority. This approach worked. Since then, Sheena has been living in our team.

Data Bus vs Notification Center vs Delegates

Why not use something else? Like a notification center or delegates? Let's take a closer look at when to use each approach:

1. Data Bus:

  • When to use: A data bus is most effective in large systems where scalability and flexibility are important. It allows you to decouple application components and reduce their dependencies on each other, while providing centralized event management. This is especially useful in systems with a modular architecture or when working with legacy code.

  • Advantages: flexibility, centralized event management, reduced dependency between components, improved scalability and maintainability.

  • Disadvantages (discussed earlier): more difficult debugging, more complex architecture, increased bus dependency and implicit dependencies.

2. Notification Center:

  • When to use: Notification Center is suitable for simple projects where strong typing is not required and components do not depend heavily on each other. It is a quick and convenient way to send and receive notifications without having to create tight coupling between objects.

  • Advantages: easy to use, no need for strong typing, suitable for simple scenarios.

  • Flaws: can cause chaos when scaling, lack of strong typing can lead to errors, harder to manage dependencies in large projects.

3. Delegates:

  • When to use: Delegates are best suited for small systems or individual components where strong typing and direct communication between objects are important.

  • Advantages: strong typing, direct and predictable communication between objects, ease of use for small components.

  • Flaws: increases the coupling of components, which complicates changes and testing, not suitable for complex and scalable systems.

Conclusion

The data bus is a powerful tool, especially in the context of the operability of new and old code.

As the examples show, using a data bus allows you to organize and standardize data access, improve the maintainability and scalability of the system, and facilitate the integration of new modules into the project. It is important to understand that this approach requires a more complex architecture and can increase the load on performance, but if applied correctly, it significantly increases the system's resistance to change and its ability to adapt to new requirements.

Finally, remember that architectural decisions should be appropriate to the scope and requirements of the project. Use the data bus where it will bring the most benefit, and keep simplicity and practicality in simpler scenarios in mind.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *