Artful DI in Angular – Part 3: How to Encapsulate a Pattern in an Abstract Service

In Part 2 of this series, we saw a class that looked up information about produce given a Produce Lookup (PLU) code. In a full application, there would be logic to learn whether the lookup succeeded, failed, or is still pending. Ideally this would be done in a uniform way across all types of lookups, and this way would make it easy to do things such as set CSS classes based on the lookup status, or access fields from the retrieved records in a safe manner.

This is a situation that cries out for an abstract class. Enter the LookupService<TRecord> class!

Before looking at the whole class, let’s look at the two most important methods: lookup and wrappedLookup. The lookup method is abstract; each class that derives from the LookupService<TRecord> class will supply its own implementation. The wrappedLookup method wraps lookup to enforce certain design decisions in a uniform way across all derived classes.

In our case, as you may recall from the last post, the derived class ProduceLookupService implemented lookup and returned the result as an Observable<ProduceRecord>. Now let’s see what the wrappedLookup method does for us.

  wrappedLookup(key: string): void {
    if (key.trim().length === 0) {
      this.lookupNoKey();
    } else {
      this.lookupPending();
      this.lookup(key).subscribe({
        next: rec => this.lookupSuccess(rec),
        error: err => this.lookupFailure(err)
      });
    }
  }

  protected abstract lookup(key: string): Observable<TRecord>;

  private lookupNoKey(): void {
    this.setProperties(LookupStatus.noKey);
  }

  private lookupPending(): void {
    this.setProperties(LookupStatus.pending);
  }

  private lookupFailure(msg: string): void {
    this.setProperties(LookupStatus.failed, msg);
  }

  private lookupSuccess(rec: TRecord): void {
    this.setProperties(LookupStatus.succeeded, '', rec);
  }

  private setProperties(stat: LookupStatus, msg = '', rec?: TRecord) {
    this.record = rec;
    this.errorMessage = msg;
    this.status.next(stat);
  }

You can see that wrappedLookup embodies a pattern that can now be consistent throughout the application:

  1. The class is designed to look up based on keys that come from <input> elements and are therefore strings. If the derived class wants to treat key as a number, it may validate and convert it. (In fact, ProduceLookupService does so.)
  2. If key is empty, we don’t bother with the lookup and we call a method that sets the status to LookupStatus.noKey. But we don’t do that until we clear the message and record fields. The order is important because, as you’ll see in Parts 6 and 7 of this series, consumers of this class are going to subscribe to an Observable on the status (more on that later). When a subscriber gets a status update, everything else should match. It would be easy to make a mistake in this regard; by getting it right in this one place and reusing the code for all types of lookups, we can be sure to get it right throughout the application.
  3. Before doing the lookup, we set the status to LookupStatus.pending, again being sure to set message and record before setting the status. Note how lookupNoKey and lookupPending both call the setProperties method, where this pattern of correct order is represented in one, DRY place.
  4. We subscribe to the result of the wrapped lookup method. This demonstrates how to respond to the Observable that subscribe returns. There are several overloads but here we use the one that seems most self-documenting, passing an object whose next property is a function to execute when the next result arrives (and here there will be only one), and whose error property is a function that processes the error. That’s all there is to it!
  5. The wrappedLookup function does not return the record. Instead, the class is designed to notify its callers of the result through the Observable status. This is to emphasize that the looked-up record is not available for modification. The purpose of this class is to provide looked-up values, nothing more.

Here is the module stripped of everything except code pertaining to status, which is worth a separate discussion.

export abstract class LookupService<TRecord> {

  private status: BehaviorSubject<LookupStatus>;

  constructor() {
    this.status = 
        new BehaviorSubject<LookupStatus>(LookupStatus.notStarted);
  }

  subscribe(next: (value: LookupStatus) => void) {
    return this.status.subscribe(next);
  }

  private setProperties(stat: LookupStatus, msg = '', rec?: TRecord) {
     this.status.next(stat);
  }
}

I’ve mentioned that status is an Observable, but it happens to be a special type of Observable called a BehaviorSubject. A Subject is an Observable that has the special property of being able to multicast to more than one subscriber. We will want more than one control in the UI to know about changes in status, so we use a Subject. A BehaviorSubject has the further property of giving each of those subscribers the same initial value. In our case, we want the initial value of LookupStatus.notStarted.

When we change the status with the private setProperties method at the end of the snippet, all subscribers will know about it.

We could expose the status directly to consumers of this class as a public member and let them subscribe to it. However, that would also let consumers issue changes to status by calling status.next with a new LookupStatus. We don’t want that. To make this class as tightly encapsulated as possible, we make status private and offer the subscribe method in the snippet.

To summarize, we have used an abstract class to encapsulate several design patterns that we want to make unerringly consistent across the application:

  • Responding properly to an empty lookup key.
  • Setting a status at each stage of the lookup.
  • Setting related fields in the correct order.
  • Ensuring that only our abstract class is able to change the lookup status.

We did all of that with no knowledge of what type of record we were looking up! In the complete code listing at the end of this post, notice that the class is generic on TRecord. The derived class, ProduceLookupService, supplies the generic parameter as it inherits from the base class:

class ProduceLookupService extends LookupService<ProduceRecord>

As for the commented-out @Injectable, you will hear more about it in Part 5 of this series.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.