angular - Hur tillämpar man Single Responsibility på en komponentklass?


Min fråga är ganska enkel.

Låt oss anta att vi har 2 komponenter, och varje komponent har sitt eget formulär.

Första komponent: create-post.component.ts

Andra komponent: edit-post.component.ts

När du skapar ett inlägg fyller du helt enkelt i fälten i det formuläret och om allt är giltigt skickar du in och skapar det inlägget (i någon intern array).

När du redigerar ett inlägg, laddar du det specifika inläggsobjektet i redigeringskomponenten och sedan redigerar du vad du än behöver och skickar in det.

Nu, enligt Single Responsibility, bör vi faktiskt skapa en klass för att hantera skapandet av inlägg och hantera intern UI-logik för den komponenten (edit-post.component.html).

Jag har dock sett i många exempel att det föredragna mönstret för att hantera det fallet är att använda 1 form och vid redigering sparar de helt enkelt ett boolean som sätts när en händelse triggas för att meddela en specifik lyssnare om en ändrad ruttparameter.

Om den ruttparametern verkligen ändras, finns det då ett If-statement för att hantera den specifika logiken (samma logik som implementerats i edit-form.component.ts).

Att ha händelser och lyssnare för att hantera ett annat syfte verkar som en överträdelse av singelansvarighet.

Det här exemplet är ganska enkelt. I mer avancerade fall verkar det som att vi kan få en spaghetti-kod mycket snabbt.

Vad är det korrekta sättet att implementera ett sådant fall om jag vill hålla mig till Single-principen?

Svar #1

Jag skulle vilja lägga till mina egna tankar om den frågan som enligt min åsikt är mycket intressant.

Friskrivning: Koden nedan försöker inte vara en ren representation av SOLID i Angular men kommer åtminstone nära upp till principerna för Single Responsibility och Open-Closed. Jag är helt öppen för konstruktiva kommentarer och det skulle vara fantastiskt att titta på andra tillvägagångssätt.

Trots att storleken har betydelse i webbapplikationer, kunde vi uppnå single responsibility och Open-close principerna utan att påverka våra bundles storlek alltför mycket.

Följande med exemplet som föreslås i frågan, skulle vi kunna ha en user-form.component.ts som bara hanterar huvud-/gemensam logik såsom en formgrupp plus alla valideringar.

// user-form.component.ts
@Component({
 selector: 'app-user-form',
 template: `
  <form [formGroup]="userForm" (submit)="submit.next($event)">
   <label>Username</label>
   <input type="text" formControlName="username" />
   <label>Password</label>
   <input type="password" formControlName="password" />
   <!-- ... -->
   <button type="submit" > Save </button>
  </form>
 `
})
export class UserFormComponent {
  @Input() set user(user: User) {
   this.userForm.patchValue({...user})
  }

  @Output() submit = new EventEmitter<User>();

  userForm = new FormGroup({
   username: new FormControl('', [Validators.Required, UserNameValidator]),
   password: new FormControl('', [Validators.Required, PasswordValidator]
   // ...
  })
}

Då spelar det ingen roll om det är inbäddat i en edit-user-dialog.component.ts, add-user.component.ts osv. varje wrapper kan då hantera det på ett annat sätt.

ex :

<app-user-form [user]="user" (submit)="edit($event)">
</app-user-form>
<app-user-form (submit)="add($event)">
</app-user-form>

Om vi utgår från att user-komponenterna är hostade i sin egen modul ser jag inte poängen med att bryta ner det formuläret och dess omslag bör öka paketstorleken alltför mycket.

Open-Close

Ibland tvingar kraven (design, use cases ...) oss att göra user-form.component.ts större genom att lägga till flaggor, till exempel en submit-knapp med en annan etikett. Vi skulle behöva undvika dessa flaggor när det är möjligt.

Gör inte:

// user-form.component.ts
@Component({
 selector: 'app-user-form',
 template: `
  <form [formGroup]="userForm" (submit)="submit.next($event)">
   <label>Username</label>
   <input type="text" formControlName="username" />
   <label>Password</label>
   <input type="password" formControlName="password" />
   <!-- ... -->
   <button type="submit" > {{ mode === 'edit' ? 'Edit' : 'Add' }} </button>
  </form>
 `
})
export class UserFormComponent {
  @Input() set user(user: User) {
   this.userForm.patchValue({...user})
  }
  @Input() mode : 'edit' | 'add';
  @Output() submit = new EventEmitter<User>();

  userForm = new FormGroup({
   username: new FormControl('', [Validators.Required, UserNameValidator]),
   password: new FormControl('', [Validators.Required, PasswordValidator]
   // ...
  })
}

Gör :

// user-form.component.ts
@Component({
 selector: 'app-user-form',
 template: `
  <form [formGroup]="userForm" (submit)="submit.next($event)">
   <label>Username</label>
   <input type="text" formControlName="username" />
   <label>Password</label>
   <input type="password" formControlName="password" />
   <!-- ... -->
   <button type="submit" > {{ submitLabel }} </button>
  </form>
 `
})
export class UserFormComponent {
  @Input() set user(user: User) {
   this.userForm.patchValue({...user})
  }
  @Input() submitLabel = 'Save';
  @Output() submit = new EventEmitter<User>();

  userForm = new FormGroup({
   username: new FormControl('', [Validators.Required, UserNameValidator]),
   password: new FormControl('', [Validators.Required, PasswordValidator]
   // ...
  })
}
<app-user-form [user]="user"
               [submitLabel]="'Edit'"
               (submit)="edit($event)">
</app-user-form>
<app-user-form  [submitLabel]="'Add'"
                (submit)="add($event)">
</app-user-form>

Denna metod i synnerhet tillåter oss att tillhandahålla en standardöversättning, eller använda en egen utan att röra den ursprungliga user-form.component.ts

Men det finns andra fall, där designen eller kraven är mer specifika, då tvingas vi söka efter en elegant eller komplex lösning. Ett exempel skulle vara när vi i ett visst scenario måste gömma ett inputfält eller lägga till en extra validering.

Ett tillvägagångssätt skulle vara att bryta ner user-form.component.ts i små bitar (form-field-component.ts) och projicera innehållet inom user-form.component.

// user-form.component.ts
@Component({
 selector: 'app-user-form',
 template: `
  <form [formGroup]="userForm" (submit)="submit.next($event)">
   <ng-content></ng-content>
  </form>
 `
})
export class UserFormComponent implements IUserFormComponent {
  private value: User;
  @Input() set user(user: User) {
   this.value = user;
   this.userForm.patchValue({...user})
  }
  @Output() submit = new EventEmitter<User>();

  // IUserForm 
  userForm = new FormGroup({
   username: new FormControl('', [Validators.Required, UserNameValidator]),
   password: new FormControl('', [Validators.Required, PasswordValidator]
   // ...
  })

  
  reset() {
    this.user = this.value;
  }
}
// user-form-n-field.component.ts
@Component({
 selector: 'app-user-n-field',
 template: `
  <label> {{ usernameLabel }} </label>
  <input [control]="control" />
 `
})
export class UserFormNFieldComponent {
  @Input() label = 'n'; 
  
  control: AbstractControl;
 
  constructor(userFormComponent: IUserFormComponent) {
    this.control = this.userFormComponent.userForm.controls.n;
    // maybe subscribe the userForm changes
    // or add extra validations
  }
}

De slutliga användningarna skulle kunna se ut så här:

<app-user-form #form [user]="user" 
               (submit)="edit($event)">
 <!-- ... -->
 <app-user-form-avatar-field></app-user-form-avatar-field>
 <app-user-form-username-field></app-user-form-username-field>
 <app-user-form-password-field></app-user-form-password-field>
 <!-- ... -->
 <button type="button" (click)="form.reset()" >Reset</button>
 <button type="submit" >Edit</button>
</app-user-form>
<app-user-form [user]="user"
               (submit)="add($event)">
 <app-user-form-username-field></app-user-form-username-field>
 <app-user-form-enabled-field></app-user-form-enabled-field>
 <!-- ... -->
 <button type="submit" >Add</button>
</app-user-form>

Relaterade artiklar