Or, how should I divide the logic in my applications?
First of all, this blog entry isn't specific to Angular, it does apply to most applications. It is about separation of concerns, where to put what and why to bother. However as Angular is my go-to framework, the code-samples in this article will use that.As a side-effect, you will also pick something up on refactoring if you follow along and examine the code,
But first some background. I see quite some code while helping people out. One of the things I see over and over again is mixing business logic with view logic, as well as components/parts that have grown too large, but that is another thing and out of the scope for this article. (Probably even a bigger issue, but the advice in this article will help there too!)
First of all, lets set a definition of business logic in a down to earth manner. What does count as business logic? The most basic definition that I would come up with is the following. Anything that you do to get, combine, manipulate and validate data, coming from any source.
That would conclude that whatever is left is view logic. That is a bit cutting corners, so let's extrapolate a bit on that.
View logic is all the logic you use to generate the UX. This includes temporal data, that only exists for view state. (Re)implementing validations, massaging the data so it looks O.K. in the view. There is an overlap with business logic, no doubt about that, and sometimes it's hard to know where something would go. I would say, if there is a (remote) change you need the same manipulation again, it's business logic.
Validation is actually the odd one out. It is business logic for sure, and your server should reinforce those rules. However, there might be cases where you can cope with just the default validators that are available in your framework or even provided by the browser.
However, that usually means those end up in the view component, as that is the place where your user interacts with the actual data. This might be fine, but at least think about this. Custom validations should be in the business logic layer. Allways! and then enforced in the view by utilizing whatever your framework has for that purpose. In Angular, this means custom validators. Inject the service that holds the actual validation in there, to hook it up to your view logic.
Let's look at a small example. I'm utilizing the Star Wars API to show a list of personalities, and when selected will show the movies, they were part of and the related starships.
So, let's have a look at the initial implementation:
template:
<section>
<h1>people</h1>
<article *ngFor='let person of persons$|async' (click)="select(person)">
{{person.name}}
</article>
</section>
<section>
<h1>{{selectedPerson.name}} occurs in</h1>
<article *ngFor="let movie$ of movies">
<ng-container *ngIf='movie$ | async; let movie'>
{{movie.title}}
</ng-container>
</article>
<h1>Related starships</h1>
<div *ngIf="selectedPerson.starships.length===0">No ships for this person</div>
<article *ngFor="let ship$ of starships">
<ng-container *ngIf='ship$ | async; let ship'>
{{ship.name}}
</ng-container>
</article>
</section>
Component:
@Component({
selector: 'all-in-one',
templateUrl: './all-in-one.component.html',
styleUrls: ['./all-in-one.component.css']
})
export class AllInOneComponent {
constructor(private http: HttpClient) {}
starships: Observable<Ship>[] = [];
movies: Observable<Movie>[] = [];
selectedPerson: Person = {
name: 'none selected',
starships: []
} as Person;
private _load = url =>
this.http
.get<RootObject<Person>>(url)
.mergeMap(
root => (root.next ? of(root).merge(this._load(root.next)) : of(root))
);
private load = (url): Person[] =>
this._load(url)
.map((r: RootObject<Person>) => r.results)
.scan((combinedList, latestAdditions) =>
combinedList.concat(latestAdditions)
)
.map(set => set.sort((x, y) => (x.name < y.name ? -1 : 1)));
persons$ = this.load('https://swapi.co/api/people/');
select(person: Person) {
this.selectedPerson = person;
// replace the ships array
this.starships = person.starships.map(url => this.http.get<Ship>(url));
// replace the movies array
this.movies = person.films.map(url => this.http.get<Movie>(url));
}
}
Well, that doesn't look all too bad, doesn't it? Well, for once, it mixes all kind of concerns. To make this a bit more clear, let me separate out the 'lists' into their own components, so there is less clutter in the template.
<section>
<app-persons [persons]='persons$|async' (selectedNewPerson)='select($event)'></app-persons>
</section>
<section>
<h1>{{selectedPerson.name}} occurs in</h1>
<app-movies [movies]='movies'></app-movies>
<star-ships [starships]='starships'></star-ships>
</section>
The component code stay's the same as the earlier version. It looks already much clear. But looking at it confuses me. What is 'select($event)`, and where do `movies` and `starships` suddenly appear from? I have to look at the component code to know that. Can we improve on that?
Let us refactor the template part a bit more:
<section>
<app-persons [persons]='persons$|async' (selectedNewPerson)='selectedPerson = $event'></app-persons>
</section>
<section>
<h1 *ngIf='selectedPerson && selectedPerson.name'>{{selectedPerson.name}} occurs in</h1>
<app-movies1 [movies]='selectedPerson>.films'></app-movies>
<star-ships1 [starships]='selectedPerson?.starships'></star-ships>
</section>
Yes, that is much better. I can quickly take a peek at the template, and I immediately know what is going on, perfect!
However, this won't run without updating the code, so let's refactor that as well, and see what the components code now will look like!
@Component({
selector: 'seprarated-logic',
templateUrl: './seprarated-logic.component.html',
styleUrls: ['./seprarated-logic.component.css']
})
export class SepraratedLogicComponent {
persons$ = this.logic.persons$;
selectedPerson: Person;
constructor(private logic: PeopleService) {}
}
Oh, somehow that got much easier to reason about too! What happened? When I separated the business logic from the view logic, both became a lot simpler. To be honest, there is now a bit more view logic in the `app-movies1` and in the `star-ships1`, but it's not the mess I started with.
Well, does that mean the services are now humongous?.
Let's dive into that and have a look:
here is the PeopleService:
@Injectable()
export class PeopleService {
constructor(private swapi: SwapiService) {}
persons$ = this.swapi.loadAll<Person>('https://swapi.co/api/people/');
}
Wait, wait, you must be cheating!. Nope ;-) But I did refactor the services a bit more, and added a new SwapiService class:
@Injectable()
export class SwapiService {
constructor(private http: HttpClient) {}
private _load = <T>(url) =>
this.http
.get<RootObject<T>>(url)
.mergeMap(
root => (root.next ? of(root).merge(this._load(root.next)) : of(root))
);
public loadAll = <T>(url): Observable<T[]> =>
this._load<T>(url)
.map((r: RootObject<T>) => r.results)
.scan((combinedList, latestAdditions) =>
combinedList.concat(latestAdditions)
)
.map(set => set.sort((x, y) => (x.name < y.name ? -1 : 1)));
}
By doing that refactoring, I can reuse the SwapiService inside MovieService and ShipsService that I also added.
So, what did I just how you? Well, I started off with a sample that was mixing business and view logic. As a first step I separated the view into different components and then refactored the resulting view, so it was easier to reason about. By doing that, I forced myself to think in terms of separation, and it resulted in more clean code.
The last thing I showed was that the services now hold all the business code, and the components are left with only a bit of view logic.
If you want to have a look for yourself, the code is available on my github account:github, and you can see it in action here on the github pages
If you have any questions, don't hesitate to contact me. Either by filing an issue or ping me on twitter. (or both!)