Home What's Hollywood Principle
Post
Cancel

What's Hollywood Principle

🌑 Outline

Architect Weaver is always interested in building modular, maintainable, and testable applications. One concept that plays a pivotal role in achieving these goals is the Hollywood principle.

It states, “Don’t call us, we’ll call you,” which reflects the industry’s belief that production companies contact actors only when a particular role is available. In the programming world, “A component should not directly call its dependencies. Instead, it should rely on a system to provide and manage those dependencies.” Which ensures loose coupling and enhancing modularity.

🌥️ What is Inversion of Control?

It controls certain aspects of a program, such as object creation and dependency management.

In the traditional approach to writing code, each component is responsible for creating and managing its dependencies and deciding when certain actions should occur. As a result, control rests entirely within the program written by the developer.

However, with Inversion of Control (IoC), components focus solely on their core logic, while an external framework or container manages their dependencies.

Transfers control of objects and their dependencies from the main program to an external source.

🌓 Why Named “Inversion of Control”?

The term “Inversion” highlights the shift in responsibility. In Traditional Flow, a component controls everything — object creation, dependencies, and flow. With the new approach, a framework takes over control of these aspects, while the component focuses on its primary functionality.

🌕 Common implementations of Inversion of Control

Inversion of Control is a principle, not a concrete design solution. It provides guidelines for implementation, and the solutions that adhere to these guidelines are considered its implementations.

Some popular solutions that follow the Inversion of Control principle include:

    Dependency Injection (DI)
A special container called a Dependency Injection (DI) takes care of managing dependencies between objects. It is responsible for creating the objects and carefully connecting them by analysing the configuration you provide in your application, ensuring that each object receives exactly what it needs to function properly.

    Service Locator Pattern
A legacy approach to resolve the component dependencies. It provides a centralised object from which components can retrieve their dependencies. Changing or replacing a service does not impact the clients directly. Since the clients are decoupled from the service creation, only the configuration or binding of the new service needs to be adjusted, leaving the client code unaffected. However, components directly depend upon the centralised object.

    Event-Driven Programming
In an event-driven architecture, components communicate by sending events to each other. A framework manages these events and runs the appropriate code when they occur. An event is a message sent from one component to another, and the receiver takes action based on the message. The sender doesn’t need to know which receiver will handle the event, nor does it need to create or manage the receiver.

    Strategy Design Pattern
The Strategy Pattern allows a class to choose an algorithm at runtime. It encapsulates different algorithms or behaviours in separate classes, making them interchangeable. This way, the main class doesn’t depend directly on a specific strategy. The strategy choice can be managed by an external framework, and the strategy can be injected into the main class. This enables the application to switch strategies dynamically without tightly coupling the classes.

Advantages of Inversion of Control

    Loose Coupling
It decouples the components in an application, objects no longer directly create or manage their dependencies. This makes the components independent of each other, enabling easier updates and replacements without affecting other parts of the system.

    Improved Testability
As dependencies can be easily swapped with mock objects to test-specific implementations during unit testing. This allows for isolated testing of components without relying on complex setups.

    Enhanced Maintainability
It promotes modularity by separating concerns, making it easier to maintain and extend the application. You can modify, add, or remove functionality with minimal risk of breaking the existing code.

    Flexbility
It allows you to dynamically change or configure the behaviour of your application without modifying its core logic. For example, switching a database connection or logging framework becomes easier.

🌚 Code Playground (Swift)

Dependency Injection:

Network Service Interface:

1
2
3
protocol NetworkService {
    func fetchData(from url: String) -> String
}

Network Service Implementation:

1
2
3
4
5
6
class DefaultNetworkService: NetworkService {
    func fetchData(from url: String) -> String {
        // Simulate a network call and return some mock data
        return "User data from \(url)"
    }
}

User Service Class:

This class depends on NetworkService to fetch user data. It takes NetworkService as a dependency through the constructor.

1
2
3
4
5
6
7
8
9
10
11
12
13
class UserService {
    private let networkService: NetworkService

    // Constructor Injection: We inject NetworkService via the initializer
    init(networkService: NetworkService) {
        self.networkService = networkService
    }

    // Method to get user data
    func getUserData(from url: String) -> String {
        return networkService.fetchData(from: url)
    }
}

Service Locator Pattern:

The Service Locator will act as a central registry for retrieving services:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ServiceLocator {
    static var services: [String: Any] = [:]
    
    static func register<T>(service: T, forType type: T.Type) {
        let key = String(describing: type)
        services[key] = service
    }
    
    static func resolve<T>(type: T.Type) -> T? {
        let key = String(describing: type)
        return services[key] as? T
    }
}

Register Network Service in ServiceLocator.

1
2
3
// Register the NetworkService implementation in the Service Locator
let networkService = NetworkServiceImpl()
ServiceLocator.register(service: networkService, forType: NetworkService.self)

Updated UserService class to use central registery:

1
2
3
4
5
6
7
8
9
10
class UserService {
    func getUserData(from url: String) -> String {
        // Retrieve NetworkService from Service Locator
        guard let networkService = ServiceLocator.resolve(type: NetworkService.self) else {
            return "Network service not available"
        }
        
        return networkService.fetchData(from: url)
    }
}

Here, UserService directly depends upon ServiceLocator.

Event-Driven Programming:

Events and Listeners Interface:

1
2
3
protocol Event {
    var name: String { get }
}
1
2
3
4
5
6
7
8
9
10
struct ChatMessageEvent: Event {
    let name: String = "ChatMessage"
    let sender: String
    let message: String
    
    init(sender: String, message: String) {
        self.sender = sender
        self.message = message
    }
}

Listener:

1
2
3
protocol EventListener {
    func onEvent(event: Event)
}

Events Manager:

It is responsible for managing the subscriptions of listeners to events and dispatching events to all listeners when triggered.

1
2
3
4
5
6
7
8
9
10
11
class EventManager {
    private var listeners: [String: [EventListener]] = [:]
    
    func subscribe(listener: EventListener, eventName: String) {
        listeners[eventName, default: []].append(listener)
    }
    
    func dispatch(event: Event) {
        listeners[event.name]?.forEach { $0.onEvent(event: event) }
    }
}

Chat Room: It is responsible for sending messages. When a message is sent, it dispatches a ChatMessageEvent to notify all listeners through EventManager. Here, the event manager is passed into the constructor.

1
2
3
4
5
6
7
8
9
10
11
12
class ChatRoom {
    private let eventManager: EventManager
    
    init(eventManager: EventManager) {
        self.eventManager = eventManager
    }
    
    func sendMessage(sender: String, message: String) {
        let event = ChatMessageEvent(sender: sender, message: message)
        eventManager.dispatch(event: event)
    }
}

Event Listener:

Finally, the listener listens to the event sent by the ChatRoom. In this case, the User object listens for messages sent by the ChatRoom and takes the corresponding action. However, there is no direct dependency between the User and ChatRoom objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
class User: EventListener {
    let username: String
    
    init(username: String) {
        self.username = username
    }
    
    func onEvent(event: Event) {
        if let chatMessageEvent = event as? ChatMessageEvent {
            print("\(username) received a message from \(chatMessageEvent.sender): \(chatMessageEvent.message)")
        }
    }
}

Conclusion

Architect Weaver believes that Inversion of Control simplifies development, enhances maintainability, and improves testability. By transferring control to a framework, developers can focus on building robust, modular applications. Whether you’re working on a small or large-scale project, it sets the foundation for better design and improved collaboration.

This post is licensed under CC BY 4.0 by the author.

Simplify Factory Method (Creational Pattern)

Problem - Count Set Bits In An Integer

Comments powered by Disqus.