Написал на Ангуляре первое приложение. Пока разобрался с RxJS и не наловил ошибок, приложение писал так, чтобы оно работало прямо и исправно. И мне помог этот паттерн. Прошу под кат.

Паттерн предполагает замену работы с null/undefined и изибавиться от последующей обработки таковых. https://www.carloscaballero.io/design-patterns-null-object/

Пример: аутентификация пользователя

Сразу к примеру. Получаем при логине пользователя, сохраняем в локал-сторадже, подписываемся на него в нужных компонентах.

// наша моделька
export class Passport {
  constructor(
    public accessToken: string,
    public refreshToken: string,
    public roles: string[]
  ) {
  }
}

// Аутентифкатор
@Injectable({providedIn: 'root'})
export class Authenticator {
  private passport$: Subject;

  constructor(private http: HttpClient, private router: Router,) {
    // загружаем юзера из localstorage
    // делаем this.passport$.next(НАШ_ЮЗЕР)
  }

  public get passport(): Subject<Passport | undefined> {
    return this.passport$;
  }

Что тут не так: у нас возможно варианты, когда загрузка пользователя не прошла корректно: его еще нет, какая-то ошибка в приложении и прочее.

Это заставляет писать наш код так, что в подписчиках нужно делать фильтры и везде проставлять union-типа типы T | undefined. А еще можно забыть и много всего интересного.

Это утомительно, чревато ошибками и куче бойлерпелейта.

Исправляем:

// модель 
export class Passport {
  constructor(
    public accessToken: string,
    public refreshToken: string,
    public roles: string[]
  ) {
  }
}


// Null Object, который будет у нас всегда, 
// когда нет корректного пользователя и при разлогине
export class Anonymous extends Passport {
  constructor() {
    super('anon', 'anon', 'anon', []);
  }
}


// Аутентификатор
@Injectable({providedIn: 'root'})
export class Authenticator {
  private passport$ = new BehaviorSubject<Passport>(new Anonymous())

  constructor(private http: HttpClient, private router: Router,) {
    // загружаем юзера из localstorage
    // если ошибка, в системе есть анонимный null object, 
    // который корректный тип сам по себе
  }

  public get passport(): BehaviorSubject<Passport> {
    return this.passport$;
  }

Теперь у нас всегда в аутентификаторе лежит корректный с точки зрения системы объект. Просто он без ролей, с битым некорректным токеном, но система работает прямо и корректно, можно писать более надежный и точно более постой код.

Теперь код всегда корректный, с ним мы можем писать спокойно наши интерцепторы, наши компоненты и делать HTTP-запросы на бекенд. Но тк значения не валиды, то бекенд отклонит токен, а отсутствие ролей не даст доступа к компонентам, закрытымич через Guards.

Код всегда корректен:

this.authenticator.passport
    .pipe(
        tap((passport: Passport) => {
           // тут всегда корректный тип
       })
)

Пользуйтесь :) Подход концептуальный и не является императивом.


UPD: тк статья приняла удар недовольства, то заменой этого ООП-решения может быть использование монады Maybe из ФП, а данный паттерн использовать в простых случаях с неветвистой логикой: https://functionalprogramming.medium.com/maybe-monad-in-typescript-7966d2be8623

Комментарии (27)