Hone logo
Hone
Problems

Vue Dependency Collection Manager

Imagine you're building a complex Vue application where different components and services need to share and access common resources or functionalities. Manually managing these dependencies can lead to tight coupling and make your application harder to maintain. This challenge asks you to implement a robust dependency collection system in Vue using TypeScript.

Problem Description

Your task is to create a DependencyManager class that allows you to register, resolve, and manage dependencies within a Vue application. This system should be flexible enough to handle various types of dependencies, from simple values to complex class instances, and should integrate seamlessly with Vue's reactivity system.

Key Requirements:

  1. Registration: The DependencyManager should have a method to register dependencies, associating a unique key (string) with a dependency. Dependencies can be registered as:
    • Singletons: A single instance of the dependency is created and reused every time it's resolved.
    • Factories: A function that is called each time the dependency is resolved, potentially creating a new instance.
  2. Resolution: The DependencyManager should have a method to resolve a registered dependency by its key.
  3. Vue Reactivity Integration: If a registered dependency is a Vue Ref or Reactive object, the DependencyManager should track changes and allow components to react to those changes when the dependency is resolved.
  4. Error Handling: The system should handle cases where a dependency is requested but not registered, or if there are issues during factory execution.
  5. TypeScript Support: The implementation must be in TypeScript, leveraging type safety for better developer experience and robustness.

Expected Behavior:

  • Registering a dependency with a specific key should store it correctly.
  • Resolving a singleton dependency should always return the same instance.
  • Resolving a factory dependency should execute the factory function and return its result.
  • When a resolved dependency (especially a reactive one) changes, any component consuming it should automatically update.
  • Attempting to resolve an unregistered dependency should throw an appropriate error.

Edge Cases:

  • Registering a dependency with a key that already exists.
  • Resolving a dependency before it's registered.
  • Dependencies that are not reactive.
  • Dependencies that are null or undefined.

Examples

Example 1: Simple Singleton

// Assume a Vue context is available for reactivity if needed later

class MyService {
  getData() {
    return "Hello from MyService!";
  }
}

// --- Input ---
const manager = new DependencyManager();
manager.registerSingleton('myService', new MyService());

// --- Output ---
const resolvedService = manager.resolve<MyService>('myService');
console.log(resolvedService.getData()); // Expected: "Hello from MyService!"
const anotherResolvedService = manager.resolve<MyService>('myService');
console.log(resolvedService === anotherResolvedService); // Expected: true

Explanation: A MyService instance is registered as a singleton. When resolved, the same instance is returned, and its getData method is called.

Example 2: Factory and Reactivity

import { ref, Ref, reactive } from 'vue';

// --- Input ---
const manager = new DependencyManager();

const counter = ref(0);
manager.registerFactory('counter', () => counter); // Registering a Ref

// Imagine a component that uses this:
function useCounter() {
  const currentCounter = manager.resolve<Ref<number>>('counter');
  return currentCounter;
}

const counterRef1 = useCounter();
console.log(counterRef1.value); // Expected: 0

counter.value = 5; // Simulate an update to the registered ref

const counterRef2 = useCounter();
console.log(counterRef2.value); // Expected: 5
console.log(counterRef1 === counterRef2); // Expected: true (since it's the same Ref object)

// Example with a reactive object
const userSettings = reactive({ theme: 'dark' });
manager.registerSingleton('userSettings', userSettings);

const settings = manager.resolve<{ theme: string }>('userSettings');
console.log(settings.theme); // Expected: 'dark'

settings.theme = 'light';
const updatedSettings = manager.resolve<{ theme: string }>('userSettings');
console.log(updatedSettings.theme); // Expected: 'light'
console.log(settings === updatedSettings); // Expected: true

Explanation: A ref and a reactive object are registered. When the original ref's value is updated, resolving it again returns the updated value. Similarly, changes to the reactive object are reflected upon resolution.

Example 3: Error Handling

// --- Input ---
const manager = new DependencyManager();

// --- Output ---
try {
  manager.resolve<string>('nonExistent');
} catch (error: any) {
  console.error(error.message); // Expected: "Dependency 'nonExistent' not found."
}

Explanation: Attempting to resolve a dependency that hasn't been registered throws an error.

Constraints

  • The DependencyManager class should be the core of the solution.
  • The registerSingleton, registerFactory, and resolve methods must be implemented.
  • The implementation should be fully typed using TypeScript.
  • Consider performance for frequent resolutions, though premature optimization is not the primary goal.
  • Assume the availability of Vue's ref, Ref, and reactive for reactivity integration if you choose to implement that aspect deeply. You might not need to run Vue, but your types should reflect its usage.

Notes

  • Think about how to handle different types of dependencies and how to infer their types correctly during resolution.
  • The concept of "dependency injection" is closely related to this challenge.
  • For reactivity, focus on how your resolve method can return a Ref or a reactive object in a way that Vue components can consume and react to.
  • You might want to consider an interface or a discriminated union for the registered dependency type to distinguish between singletons and factories.
Loading editor...
typescript