Angular Design Patterns: Feature Services
This short post we will cover a design pattern called Feature Services. In a previous post we discussed the Feature/Presentation design pattern. This post will continue to expand on this idea.
When using Feature and Presentation components its common for the Feature component to inject several different services the pull together the data needed for the feature and expose any methods need to save or process data.
The Feature Service design pattern is a way to pull out all of this feature logic from our Feature Component into a single Feature Service. The Feature Service is a Singleton Service that is Injected at the Feature Component level in the component provider. This ensures that the Feature Service is only instantiated once for the Feature Component.
With Angular applications its common that our data will be exposed via RxJS Observables from either API endpoints or NgRx state management. We pull this logic into our Feature Service, so our Feature Component is unaware of where to get the data or how to save it. We have thoroughly encapsulated all of the logic of our feature into a single class.
Benefits of the Feature Service
What does this get us? Well, a few things. First, our Feature Component typically will only need to inject a single Service into its constructor. This, in turn, makes it much easier to test our component as we don't have to stub or mock out many dependencies. Our Feature Component is very clear and easy to understand. For example, let's take a look at this Employee Report Feature Component.
@Component({
selector: 'app-employee-report',
templateUrl: 'employee-report.component.html',
stylesUrl: 'employee-report.component.css',
providers: [EmployeeReportService]
})
class EmployeeReportComponent {
employees: Observable<Employee[]>;
reports: Observable<Reports[]>;
constructor(private employeeReportService: EmployeeReportService) {}
ngOnInit() {
this.employees = this.employeeReportService.employees;
this.reports = this.employeeReportService.reports;
}
saveEmployee(employee: Employee) {
this.employeeReportService.updateEmployee(employee);
}
removeEmployee(employee: Employee) {
this.employeeReportService.removeEmployee(employee);
}
sortByName() {
this.employeeReportService.sortByName();
}
sortReportsByDateCreated() {
this.employeeReportService.sortReportsByDateCreated();
}
}
Our component is small and lightweight. It's very apparent what our feature is trying to accomplish. Our data comes from a single source of truth which is our Feature Service. Interactions with that data are all passed through the Feature Service as well. This pattern is suited well for large features or features with many different data resources.
With a combination of Feature Components and Feature Services, we get an architecture like below.
Abstracting everything into a Feature Service is not a strict rule. While we try to keep as much of the high-level feature logic at the feature service, there are exceptions. Services like ElementRef
and FormBuilder
must be injected into the component. This is ok. We are separating the component view logic from the feature logic.
While the Feature Service has some excellent benefits, there are downsides. If your feature is relatively simple, this can be an over abstraction. This also creates additional boilerplate code with component methods mapping to a service method.
With all design patterns they have pros and cons. We at Vintage Software have found this pattern to work really well on large scale Angular applications in combination with Feature Components.