Services are a ubiquitous part of Angular applications, but is this always justified? What exactly do we mean when we label an object a "service," and what are the implications of this choice? This article delves into the practice of naming objects, the potential pitfalls of ambiguous responsibilities, and strategies for more precise definitions.
Interestingly, around the time this topic came to mind, an RFC proposing updates to Angular style guides was released. One of its suggestions was to drop the "service" suffix from class and file names. The rationale, as stated by the author, was that the term "service" fails to convey any meaningful detail about a class's function.
Following a lively debate, the decision was made to leave this topic open for developer interpretation, excluding it from the official style guide rules. Nevertheless, future official documentation and examples will refrain from using this suffix. Link
Before diving deeper, let's first examine the current naming conventions for objects within Angular.
What Kinds of Classes and Objects Are Common in Angular Applications?
When architecting an Angular application, we rely on several fundamental object types—directives (including components), pipes, guards, resolvers, and interceptors. These objects generally have well-understood roles. To simplify: pipes transform data for display, guards restrict route access based on conditions, resolvers fetch data before a view renders, and interceptors handle request or response modifications.
Components, meanwhile, serve as the core building blocks, encompassing both the view and its associated logic. Given their broad definition, we often categorize components as "smart" or "dumb" to better define their specific duties.
But What About Services?
Defining a service's role is far less clear-cut. Services are generally linked with tasks like API communication, business logic implementation, or state management—essentially, taking on work for components and facilitating data sharing across the app.
In practice, any class adorned with the @Injectable decorator and a "Service" suffix is often labeled a service. The @Injectable decorator simply marks a class for dependency injection. Thus, calling an object a service primarily indicates that it is a dependency, not what it does.
However, you've probably seen injectable classes that are not called services—like interceptors, guards, or resolvers. This suggests that we reserve the "service" label for dependencies that lack a more specific name.
This observation leads to the conclusion that "service" is often a catch-all term, a convenience that avoids the effort of clearly defining an object's purpose and naming it accordingly.
A Note on the Functional Approach
It's noteworthy that recent Angular versions support functional implementations for guards and resolvers. However, this discussion uses class and object terminology. The functional style doesn't alter the core considerations regarding object responsibility, so an object-oriented lens is maintained here.
The Art of Delegating Responsibilities
As noted, we often delegate tasks from components to other objects. Delegation is a key technique for code maintainability. Yet, it's critical to delegate to well-defined objects. Without a clear purpose for the recipient, the codebase can become difficult to manage. Therefore, we should delegate to classes with accurately defined roles. A well-thought-out name clarifies the object's purpose and its interactions.
Now, let's see how object-oriented principles can aid in this naming process.
Finding the Right Name: Consulting OOP
Object-oriented programming (OOP) is more than just class definitions or concepts like polymorphism and encapsulation. It also includes best practices and patterns for designing robust systems, such as SOLID principles and a catalog of design patterns.
Design patterns, in particular, offer a rich vocabulary and structure for defining an object's name and role. Let's explore them further.
What Are Design Patterns?
Essentially, they are proven solutions to recurring problems in software architecture. Here, "problems" refer to common technical challenges, not bugs. For instance, in frontend development, a recurring challenge is handling cross-cutting concerns like adding authentication headers to requests. A dedicated object serves as a solution to this challenge—the interceptor. Other Angular object types can also be viewed as specific patterns.
To better understand and apply design patterns, let's briefly review their origins and a formal description.
The History of Design Patterns
The formalization of design patterns in software is credited to the "Gang of Four" in their influential book, Design Patterns: Elements of Reusable Object-Oriented Software. These patterns provide general, reusable solutions to common architectural problems, guiding developers toward more flexible and maintainable code.
Formal Definition of a Pattern
A pattern is typically described by four key elements:
- A distinct name
- The specific problem it addresses
- The proposed solution, detailing the classes, objects, and interactions involved
- The trade-offs and consequences of adopting the pattern
How can we translate this thinking back to Angular?
Framework-Specific Patterns
Beyond general software patterns, Angular provides its own patterns like components, pipes, and directives. Following these framework conventions gets us part of the way to a clean architecture.
The next step is to identify recurring challenges within our specific application and define our own patterns to systematically solve them. Angular is opinionated, but it doesn't solve all domain-specific problems for you.
Let's look at some common challenges that could benefit from a more methodological approach.
Examples of Problems Needing a Solution
Interfacing with APIs
This is a classic task delegated to manage network implementation details and share functionality. By confining an object’s role strictly to API communication—without storing data or handling errors—we achieve a clear, single-responsibility object, which could be called an API Facade. A class managing product API calls could be aptly named ProductsApiFacade.
Managing State
I particularly favor state management libraries, with the NgRx Signal Store being a recent favorite. In its documentation, the defining file is termed a "store," not a "service." Similarly, with Redux-based stores, each component has a specific name—reducer, selector, action, or effect. Why then, when implementing custom state logic (like a service relying on a Subject), do we often fall back on "service" instead of "store"?
No matter the underlying implementation, the class (and its file) should be named with "store," as in ProductsStore. This ensures the object's responsibility is strictly perceived as that of a store.
Dealing with Component Logic: The Pitfalls of "Presenter"
When components get too large, developers often extract logic into helpers, sometimes adopting a pattern known as "presenter" from MVP architecture. A prominent resource is Lars Nielsen’s series on MVP in Angular.
I see two primary issues with this:
- Angular is inherently component-based, not MVP-based. While Nielsen's "presenter" is an object handling complex presentation logic, this deviates from the original MVP definition. Repurposing a well-established term in a different context can lead to confusion.
- The name "presenter" is still quite vague regarding the object's exact role, making it not much different from "service," even if it's tied to a specific component.
Instead of delegating all "complex logic" to a generic "presenter," it's better to assign specific tasks (e.g., handling a form) to dedicated objects designed for that exact purpose.
Implementing Team Standards
Once problems and their solutions are identified, the challenge shifts to ensuring team-wide adoption.
- Documentation: Keeping a record of project-specific patterns is vital. It's a task that requires diligence but pays off in organized concepts, easier onboarding, and more effective code reviews.
- Communication: Holding regular discussions about recurring challenges, current practices, and potential improvements is essential for collaborative evolution.
- Consistency: The agreed-upon standards must be reinforced during code reviews. If the team struggles to follow them, it might be time to revisit and clarify the definitions.
Conclusion
In closing, I've advocated for moving away from generic "services" and towards objects with clearly defined purposes and names. Vague naming leads to vague responsibilities. I've suggested using established design patterns and creating custom ones for domain-specific problems to achieve this clarity. The successful implementation of such standards depends on solid documentation, open communication, and a consistent commitment to code review.
