Angular Delegator

The Story of the Missing Abstraction

@markdalgleish

The Problem

Splitting large services into smaller ones

The Missing Abstraction

Prologue

We Were Writing a Search App

Filters Model

The core model in our app


app.value('filters', {

  keywords: "",
  sortBy: "relevance",
  workTypes: [],
  ...
  
});

URL Parameters

Filters model needs to be serialized and deserialized:

/results?keywords=foo&workTypes=0,1,2

Whenever the route changes

We need to sync the filters model with the URL

$rootScope.$on('$stateChangeSuccess',

  function(event, toState, toParams) {
    _.extend(filters, ParamsAdapter.toFilters(toParams));
  }
  
);

(We're using ui-router)

ParamsAdapter

Two methods:

It started small

Then it grew and grew

And the tests grew. And grew.

It started to suck.

Separate those concerns!

Let's break it up into smaller services

We're On The Right Track...

Lots of small services with the same interface

So what does ParamsAdapter look like?

app.service('ParamsAdapter', function(Foo, Bar, Baz) {

  this.toFilters = function(params) {
    return _.extend({},
      Foo.toFilters(params),
      Bar.toFilters(params),
      Baz.toFilters(params)
    );
  };
  
  this.fromFilters = function(filters) {
    return _.extend({},
      Foo.fromFilters(filters),
      Bar.fromFilters(filters),
      Baz.fromFilters(filters)
    );
  };
  
});

This seemed verbose, but OK

Until we wrote the tests

The Missing Abstraction

Act One

Configuring Service Arrays

Arrays of services

Configured with Angular Delegator


app.config(function(DelegatorProvider) {

  DelegatorProvider.set('ParamsAdapters', [
    'KeywordsParamsAdapter',
    'SalaryParamsAdapter',
    'WorkTypeParamsAdapter',
    ...
  ]);
  
});

We can now reference this service array

Inside our 'ParamsAdapter' service

app.service('ParamsAdapter', function(Delegator) {

  this.toFilters = function(params) {
  
    // Merge the results of each service into a single object:
    return _.extend.apply(_,
      _.map(Delegator.get('ParamsAdapters'), function(delegate) {
        return delegate.toFilters(params);
      })
    );
    
  };
  
  // ...and again for 'fromFilters'
  
});

This is better, but it's starting to look like

Framework code

The Missing Abstraction

Act Two

Angular Delegator Gets Smarter

Let's work at a higher level

We're manually creating a service that talks to 'Delegator'

What if...

Angular Delegator could create the service for us?

app.config(function(DelegatorProvider) {

  DelegatorProvider.service('ParamsAdaptersDelegator', {
  
    interface: {
      'toFilters': 'merge',
      'fromFilters': 'merge'
    },
    
    delegates: [
      'KeywordsParamsAdapter',
      'SalaryParamsAdapter',
      'WorkTypeParamsAdapter',
      ...
    ]
    
  );
  
});

Now we're free to delete code

We no longer need 'ParamsAdapter' or its test

The Missing Abstraction

Act Three

Angular Delegator Proves Itself

We now have a clean way to break up our services

We started to notice other uses of this pattern

Active filter pills

| "Foobar" (X) | Part Time, Casual (X) |

app.config(function(DelegatorProvider) {

  DelegatorProvider.service('PillPresenterDelegator', {
  
    interface: {
      'present': 'truthy'
    },
    
    delegates: [
      'KeywordsPillPresenter',
      'SortByPillPresenter',
      'WorkTypePillPresenter',
      ...
    ]
    
  );
  
});

The delegator runs all pill presenters

And iterates over the results:

<div ng-repeat="pill in pillPresenters">
  <div>{{ pill.text }}</div>
  <div ng-click="pill.reset()">X</div>
</div>

Any other obvious uses?

Validation

app.config(function(DelegatorProvider) {

  DelegatorProvider.service('FilterValidatorDelegator', {
  
    interface: {
      'isValid': 'all'
    },
    
    delegates: [
      'KeywordsFilterValidator',
      'SortByFilterValidator',
      'WorkTypeFilterValidator',
      ...
    ]
    
  );
  
});

Delegator Strategies


Creating new strategies

They're just services

app.service('TextDelegatorStrategy', function(MapDelegatorStrategy) {

    return function(services, args) {
      return MapDelegatorStrategy(services, args).join(', ');
    };
    
});
interface: {
  'toText': 'text'
}

Moral of the story

Want to split up a large service into smaller ones?

Reach for angular-delegator


$ bower install --save angular-delegator

Thank you!

@markdalgleish