Vue-like services in Aurelia Pros/Cons

We did something like Vue services with Aurelia. Please consider the following code:

import { FrameworkConfiguration, Aurelia } from 'aurelia-framework';

declare module 'aurelia-framework' {
  export interface Aurelia {
    $strings: {
      upperCase: (value: string) => string;
      lowerCase: (value: string) => string;
    };
  }
}

export function configure(config: FrameworkConfiguration) {
  Aurelia.prototype.$strings = AureliaStrings;
}

export class AureliaStrings {
  static upperCase(value: string): string {
    return value.toUpperCase();
  }
  static lowerCase(value: string): string {
    return value.toLowerCase();
  }
}

export const aurelia = Aurelia.prototype;

You can use the service everywhere like the following:

aurelia.$strings.upperCase("Aurelia");

and 
aurelia.$scroll....
aurelia.$keyboardShortcut....
...

I think this way is good for singleton classes (services) but I want to know your opinions.

What are the Pros/Cons and Advantages/Disadvantages of this way?
Do you recommend using this method?
Is it a good idea to add all services to the Aurelia class as a point of sharing?

1 Like

I’m going to ask the obvious here :slight_smile:

What problem does this solve exactly? What’s wrong with loose exported functions?

5 Likes

There are a few issues I see with this approach.

First of all it merely limits you to handling singletons and leaves the consumer no easy way to decide whether it should be lazily resolved or done as a factory, which is one of the reasons you’re using a DI. Coming from Vue, I guess the lack of a standard IoC Container is what made this pattern pop up.

Second, when it comes to unit testing, your approach makes it harder to stub-out change the behavior. With a DI, a fundamental aspect is that you inject dependencies to the consumer and let that one decide how to work with it. So you’d have to constantly shuffle the implementation of the global prototype link, whereas the DI approach leaves you full control to how the component should act under test.

Third, what if you’re about to use a multi-app approach. With your approach you’ve patched one instance of Aurelia but a second app would need to do the exact same, whereas with the DI you’d get the resolve done automatically.

A potentially last reason would be that extending a prototype might be a bad idea if practised by multiple users. Imagine you consume a plugin, which also does $strings manipulation via a service. Now who would win in this case? Depending on the load order either your or the plugins definition would win, which could create a debugging nightmare if that behavior is not upfront visible by the plugin.

In general I’d say all of this boils down to using a proper IoC pattern plus DI vs finding an alternative. As @fkleuver said I’d also like to understand the reason before coming to conclusions. Are you having a hard time with how the DI works in Aurelia or something you feel tends to clutter your codebase and you wish to search for a lightweight opportunity? Either way, at least I’d change the configure function as follows:

export function configure(config: FrameworkConfiguration) {
  const instance = config.aurelia.container.get(AureliaStrings);
  Aurelia.prototype.$strings = instance;
}

this way at least the DI is used to resolve the AureliaStrings service, which in turn might itself use further DI based injections.

6 Likes

@fkleuver It was just a question. I love to work with different frameworks, libraries, and ecosystems and learned and experienced about them. I just like the simplicity of this method, not more!

@zewa666 Really great explanation thank you so much.

About IoC, I discussed with Ivan You here. I really did not get convinced!

1 Like

I can’t second guess what Evan meant with the internal way of creating components. I do agree with you that ctor injection is definitely the preferred way by lots of DI containers. Anyways I was wondering what you felt about the depicted approach to be nicer/simpler etc compared to the DI approach?

2 Likes

For utility functions with no dependencies, I would just create a module that exports loose functions for that purpose.

For most other scenario, I’d handle it with constructor injection. (There are always exceptions, of course, but not many in this case that I can think of.)

Attaching things to the aurelia singleton is just creating implicit singletons via reference, which is little different from creating global variables that are attached to window. You lose all the control, power, testability, etc. that using DI will give you. But there’s a bigger issue here. This technique forces you to violate Law of Demeter, creating an implicit coupling between services, which is not visible without reading every line of code in your application. I’ve seen applications that were riddled with this problem and it can be a disaster. (e.g. React prop passing)

In general, try to always make dependencies as explicit as possible. The dependencies between components is one of the most critical aspects of your software architecture.

7 Likes

I am so happy to asked the question. I learned a lot from you guys. Thank you so much.

@zewa666

I just like a point of sharing (aurelia class) to find services in one place when using intellisense . it is personal interest, not technical.

About DI injection via ctor my concern is Long parameter list (a code smell).

@EisenbergEffect
I really enjoy reading your answer again and again :slight_smile:

1 Like

@HamedFathi Always glad to share my thoughts with you and the community :slight_smile:

You bring up a good point about long parameter lists.

There is no hard and soft rule about this. Personally, when I see something with 5 or more constructor args, that’s when I start to worry. Usually, it’s some indicator that there’s a missing concept in the model. For example, three of the five things might actually make more sense to be coordinated by a separate object, which would reduce the original five dependencies down to three.

It’s hard to give general advice on this because it’s so very contextual. However, when the parameter lists start to grow, that’s usually a sign that something isn’t quite right in the software design and might warrant some revisited thinking. In this way, constructor-based injection can actually shine a light on some problems in the general design. When you encounter a smell, such as long parameter lists, always ask yourself “where is that smell coming from?”

5 Likes

One case I’ve seen ctor parameters flooded is when writing MVVM apps and having too many, more or less strictly coupled Services. I don’t know for you but I’ve seen and sadly also written apps, where in order to create a new component you at least had to add 3-4 services. You know like a DataService, StringService, AuthService and so on. One thing I found is that my recent apps with Aurelia Store have drastically less default injections (except the store which now is a pretty much default one :wink: ). Large chunks of business logic is now encapsulated in actions and those few services left are not polluting the list too much. So instead now actions are imported and just kicked off.
The beauty, when it comes to testing, is that you don’t need to mock the actual actions but instead only the injected store’s dispatch method, which can be conveniently also only mocked for a specific signature, namely the one action I’m about to avoid.

5 Likes

@zewa666
Unfortunately, so far, I have not used your fantastic library. I promise this will happen soon. :smile: :wink:

1 Like

Hehe alright so great opportunity to try it out :wink:
Anyways this is not specific to Aurelia Store but in general to a centralized State Management. So even going with Redux or Vuex, in the case of Vue, could help to reduce the number of injected dependencies.

2 Likes

@EisenbergEffect, For a library like Zenscroll or Faker.js

Zenscroll is a vanilla JavaScript library that enables animated vertical scrolling to an element or position within your document or within a scrollable element (DIV, etc.). It can also automatically smooth all scrolling within the same page.

Faker.js generate massive amounts of realistic fake data in Node.js and the browser

What is your suggestion?

  1. Just import and use it anywhere.
    Or
  2. Write an Aurelia service (singleton) and inject by DI
    Or

For fake data, I usually have that abstracted away from my app, maybe through a repository interface. So, in the early days of development you can use the fake repo implementation and then later you can swap it for a real implementation, without having to change any other code.

For something like a scrolling library, I would typically abstract that behind a custom attribute, so that again, the rest of the app would not be aware of that library.

Within each of these two abstractions, whether or not I would try to inject the actual 3rd party libraries, depends a lot on how those libraries are written. Since many libraries are legacy, pre-ES2015, I tend not to inject them. Also, since most of the strategies above are designed to provide an abstraction which itself is injected, then injecting the 3rd party libraries also seems devoid of value.

1 Like