Блог Михаила Крамера. PHP и JS

Правильное уведомление о глобальных событиях в Angular

Иногда в системе может происходить какое-то событие, которое нас должно заставить перерисовать огромное количество несвязанных компонентов. Яркий пример - пользователь вошёл на сайт, и надо сменить и хедер, и сайтбар, и ещё черт знает что. Давайте посмотрим, как это делается неправильно и правильно.

Обычно для логина заводится какой-нибудь AuthSerivce, задача которого - дёрнуть апи, передав туда логин и пароль. Выглядеть это может, ну например, так:

import {Injectable} from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class AuthService{
  constructor(private http: HttpService) {}
  
  login(login: string, password: string) {
    return this.http.post("http://api-domain/login", {login, password})
  }
}

И так, пока эта служба вообще никого ни о чём не уведомляет, и уведомление должен сделать компонент, её использующий, например так:


/* ... */
@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.scss']
})
export class AuthComponent implements OnInit, OnDestroy {
  form = new FormGroup({
    login: new FormControl,
	password: new FormControl
  })
  /* Как мы знаем, компоненты, не состоящие в родстве дочерний/родительский, должны общаться через службы, в которых заводятся Subject для нужных событий */
  constructor(private auth: AuthService, private headerService: HeaderService) {}
  
  doLogin() {
     this.auth.login(this.form.value.login, this.form.value.password).subscribe(
	   () => {
	      this.headerService.loggedIn();
	   }
	 )
  }
}

И тут мы явно нарушаем SOLID, а именно принципы Single Responsibility (единой ответственности) и принцип Open/Close (открытости к расширениям и закрытости к изменениям). Почему у нас компонент, ответственный за аутентификацию, знает, что в системе есть компонент, ответственный за хедер? А если нужно будет обновить ещё сайтбар, тоже сюда? А если ещё какой-то компонент? Ответственностью компонента AuthComponent должно быть только получить логин и пароль и передать его в службу AuthService.

ОК, давайте попробуем перенести это в службу Auth:


 login(login: string, password: string) {
    return this.http.post("http://api-domain/login", {login, password}).pipe(
	   tap(() => {this.headerService.loggedIn(); })
	)
  }

Теперь мы сняли обязанность с компонента, но служба аутентификации теперь знает, какие компоненты есть в системе. Это её совсем не касается!!! Служба должна знать лишь одно: если клиенты, которых надо уведомить про то, что пользователь вошёл на сайт. Теперь приходим к тому решению, которое будет правильным. Выносим все уведомления в отдельную службу:

import {Injectable} from '@angular/core';
import {Subject} from "rxjs";

@Injectable({
    providedIn: 'root'
})
export class LoginNotificationService {

    readonly private loggedIn$ = new Subject();
    readonly private loggedOut$ = new Subject();
    constructor() {
    }

    notifyLogin() {
        this.loggedIn$.next();
    }

    notifyLogout() {
        this.loggedOut$.next();
    }
	
	get onLoggedIn$() { return this.loggedIn$.asObservable(); }	
	get onLoggedOut$() { return this.loggedOut$.asObservable(); }	
}

Теперь код метода логина в службе меняется так:


 login(login: string, password: string) {
    return this.http.post("http://api-domain/login", {login, password}).pipe(
	   tap(() => {this.loginNotificationService.notifyLogin(); })
	)
  }

Какие мы получили преимущества? Теперь ни компонент ни служба аутентификации понятия не имеют, кого нужно уведомить о том, что логин произошёл. Они знают только, что кого-то надо уведомить, и поэтому остаются изолированы на своих задачах. А клиенты, которым надо как-то поменять своё состояние в ответ на логин пользователя, могут использовать LoginNotificationService для того, чтобы подписаться на событие логина.

Ваш комментарий
Комментарии