Here are a couple of alternative methods to achieve the same desired outcome:
Primary method - using a manager for entities or non-service objects
Create one or more factory services responsible for instantiating objects.
This allows for the provision of required dependencies to objects without the need for manual passing.
For instance, if you have entities structured in a class hierarchy:
abstract class Entity { }
class SomeEntity extends Entity {
...
}
You can implement an EntityManager as a service capable of constructing entities:
@Injectable()
class EntityManager {
constructor(public http: Http) { }
create<E extends Entity>(entityType: { new(): E; }): E {
const entity = new entityType();
entity.manager = this;
return entity;
}
}
It is also possible to include construction parameters (though they will lack type information due to the generic nature of create
):
class SomeEntity extends Entity {
constructor(param1, param2) { ... }
}
// in EntityManager
create<E extends Entity>(entityType: { new(): E; }, ...params): E {
const entity = new entityType(...params);
...
}
Entities can now declare the manager:
abstract class Entity {
manager: EntityManager;
}
And utilize it within entities:
class SomeEntity extends Entity {
doSomething() {
this.manager.http.request('...');
}
}
The manager is used whenever an object needs to be created. The EntityManager
itself must be injected, but entities remain unrestricted objects. All angular code originates from a controller or service, making injection of the manager feasible.
// Within any angular component
constructor(private entityManager: EntityManager) {
this.entity = entityManager.create(SomeEntity);
}
This methodology can be adapted for various objects and works seamlessly with TypeScript. Implementing a base class for objects enables code reusability, particularly when following a domain-oriented approach.
PROS: This technique ensures safety by maintaining full DI hierarchy, minimizing unintended side effects.
CONS: One drawback is the inability to use new
or directly access services in arbitrary code. Dependency on the DI and factories is necessary at all times.
Alternative method - h4ckz0rs
Establish a dedicated service to retrieve necessary services via DI for objects.
This approach closely resembles the primary method, except that the service serves as a conduit rather than a factory. It transfers the injected services to an external object defined in a separate file (explained later). For example:
...
import { externalServices } from './external-services';
@Injectable()
export class ExternalServicesService {
constructor(http: Http, router: Router, someService: SomeService, ...) {
externalServices.http = http;
externalServices.router = router;
externalServices.someService = someService;
}
}
The object holding the services is defined in its own file:
export const externalServices: {
http,
router,
someService
} = { } as any;
Note that services do not specify types (a downside but a necessity).
Ensure ExternalServicesService
is injected once. The main app component is an ideal location, like so:
export class AppComponent {
constructor(..., externalServicesService: ExternalServicesService) {
At this point, services can be utilized in any object after the main app component is instantiated.
import { externalServices } from '../common/externalServices'
export class SomeObject() {
doSomething() {
externalServices.http().request(...)
}
}
However, services cannot be accessed within the class code or objects instantiated prior to app instantiation. Typically, such scenarios are rare in apps.
An explanation regarding this unusual setup:
Why use an object externalServices
in a separate file instead of the same file or saving services on the class (as static attributes), and why are services untyped?
Cyclic dependency issues arise during production bundling with tools like angular-cli/webpack, leading to cryptic errors difficult to diagnose. To combat this, keep ExternalServicesService
reliant solely on externalServices
, streamlining service instances. Arbitrary code only interfaces with externalServices
, devoid of additional dependencies or typings.Conversely, importing ExternalServicesService
would introduce unwanted baggage, worsening circular dependency challenges common in ng2/webpack prod builds.
Type declarations, similar to imports, trigger said problems.
PROS: Once configured, this method offers seamless services access and preserves usage of new
. Any code file simply imports the externalServices
for immediate service availability.
CONS: Drawbacks lie in the hackish setup and probable cyclic dependency complications. Sensitivity peaks since assurance of present services in externalServices
hinges on app initialization post-ExternalServicesService
injection. Moreover, loss of concrete typing weakens interface reliability.
PS: Why such solutions aren't more prevalent puzzles me.
Domain-centric design advocates robust entities (e.g., equipped with REST-triggering functions or cross-service interactions). Overcoming this challenge within both AngularJS and contemporary Angular2+ remains elusive, underscoring unaddressed library lacunae.