The (hopefully) Definitive Angular2 API Design Style Guide
Say what you want about OOP and the dangers of deep coupling and over-use of inheritance. One thing that OOP does really well is encapsulation.
Hiding the internal implementations had 2 significant benefits:
- it makes code easier for users to reason about
- it allows developers to change the internal structure without negatively impacting users
API breaking changes can be devastating to the long-term stability of a project. The ES6 module provides a solid foundation. The facade
pattern provides the means to define a convention that will benefit both users and developers alike. So, what is the facade pattern?
The facade
pattern (or façade
pattern) is a software design pattern
commonly used with object-oriented programming. The name is by analogy to an architectural facade
. A facade
is an object that provides a simplified interface to a larger body of code, such as a class library.
Source: Facade Pattern - Wikipedia
To see the full benefit, we need a reasonably complex application structure.
.
├── app
│ ├── about
│ │ └── components
│ │ ├── about.e2e.ts
│ │ ├── about.component.ts
│ │ └── about.spec.ts
│ ├── master
│ │ └── components
│ │ ├── master.css
│ │ ├── master.e2e.ts
│ │ ├── master.view.html
│ │ ├── master.component.ts
│ │ └── master.spec.ts
│ ├── assets
│ │ ├── img
│ │ │ └── smile.png
│ │ └── main.css
│ ├── home
│ │ └── components
│ │ ├── home.css
│ │ ├── home.component.ts
│ │ └── home.spec.ts
│ ├── shared
│ │ └── services
│ │ ├── name_list.service.ts
│ │ └── name_list.spec.ts
│ ├── todo
│ │ ├── components
│ │ │ ├── todo.view.html
│ │ │ ├── todo.component.ts
│ │ │ ├── todoitem.component.ts
│ │ │ ├── todoitem.view.html
│ │ │ ├── todoitem.e2e.ts
│ │ │ ├── todolist.component.ts
│ │ │ ├── todolist.view.htm
│ │ │ └── todolist.e2e.ts
│ │ ├── models
│ │ │ └── todos.model.ts
│ │ ├── services
│ │ │ └── todo.service.ts
│ │ └── todo.ts <- module facade
│ ├── main.component.ts
│ └── index.html
└── package.json
What we have here is a basic website with a reasonably complex Todo
feature. Lets say we want to import the TodoComponent
for use in the HomeComponent
.
Use Case 1: The Basics
The nested folder structure makes the import statements look pretty hairy.
-
Setup the TodoService
so it's available for injection
app/main.ts
import { TodoService } from './todo/services/todo.service' // <- deep link ಠ_ಠ
...
bootstrap(MainComponent, [ TodoService ]);
-
Import the TodoComponent
app/home/components/home.ts
import { TodoComponent } from '../../todo/components/todo.component'; // <- deep link ಠ_ಠ
...
Not bad but there's room for improvement if we implement the facade.
/app/todo/todo.ts
export { TodoComponent } from './todo/todo.component';
export { TodoService } from './todo/todo.service';
Then the setup becomes
-
Setup the TodoService
so it's available for injection
app/main.ts
import { TodoService } from './todo/todo' // <- shallow link ʘ‿ʘ
bootstrap(MainComponent, [ TodoService ]);
-
Import the TodoComponent
app/home/components/home.ts
import { TodoComponent } from '../../todo/todo'; // <- shallow link ʘ‿ʘ
OK, I admit. This example is pretty contrived but it sets up a good foundation that we'll build from.
Usefulness Factor: 3/10
Use Case 2: Maintainability
So we've got a facade, and both the service and component linked to it. Throwing all of the components into the module seems a bit messy. Looks like a good time to add another level of directories and split the files up by context.
│ ├── todo
│ │ ├── todo
│ │ │ ├── todo.view.html
│ │ │ └── todo.component.ts
│ │ ├── todoitem
│ │ │ ├── todoitem.component.ts
│ │ │ ├── todoitem.view.html
│ │ │ └── todoitem.e2e.ts
│ │ ├── todolist
│ │ │ ├── todolist.component.ts
│ │ │ ├── todolist.view.htm
│ │ │ └── todolist.e2e.ts
│ │ ├── models
│ │ │ └── todos.model.ts
│ │ ├── services
│ │ │ └── todo.service.ts
│ │ └── todo.ts <- module facade
Since all of the parts that are referenced externally already point to the facade, all we have to do now is update the facade to reflect the change.
/app/todo/todo.ts
export { TodoComponent } from './todo/todo.component'; // <- was './components/todo.component';
export { TodoService } from './services/todo.service';
The links between the source files within the Todo
feature still have to be updated but all changes are localized to the feature. As long as the rest of the app imports from the facade, the rest of the app shouldn't be affected. No more project-wide 'find in files'. No more stress about possibly missing a reference that needs to be updated and breaking the app.
The stability and maintainability characteristics are warming up.
Usefulness Factor: 6/10
Use Case 3: Reuse
Maintainability is cool and all but what if we'd like to reuse this feature on another site, put it under its own source control, and post it on GitHub for some OSS cred?
The hard part is already done. A public API has been defined via the facade. All of the relevant components, services, models, etc are already organized as a single unit. The rest depend on personal preference.
I suggest:
- initialize a new repo
- copy the contents of the Todo feature directory
- push to Github
- install via NPM/JSPM
Assuming the module is mapped to todo
using your ES6 module loader...
-
Update the TodoService
reference so it's available for injection
app/main.ts
import { TodoService } from 'todo' // <-- was './todo/todo
bootstrap(MainComponent, [ TodoService ]);
-
Import the TodoComponent
app/home/components/home.ts
import { TodoComponent } from '../../todo/todo'; // <- was '../../todo/todo'
Bonus: Before you extract the code from the project, try moving it to the shared
folder and update the references. This is a good idea to check for references that weren't updated to point to the facade.
Now that the code is on GitHub. It can be developed independently, contributed to by others, and used on as many applications as desired.
Usefulness Factor: 10/10
Use Case 4: Composibilty
A facade is provided to localize the impact of changes to the feature. The feature has been restructured, allowing for growth. The feature has been extracted for reuse.
All of which is great if you're looking for a cookie-cutter implementation of the Todo feature. What about customizability? What happens when the feature needs to be integrated into an existing site?
For example your company wants to embed the Todo feature directly into an internal project tracking application. This will require application-specific styling and a new service to tie into the existing backend API.
One option is to create a new repo and copy the contents over. What about DRY? What about the benefits of the additional development that takes place on the original repo?
To enable a finer degree of granularity, the facade will need to be extended to update the public API.
/app/todo/todo.ts
export { TodoComponent } from './todo/todo.component';
export { TodoItemComponent } from './todoitem/todoitem.component';
export { TodoListComponent } from './todolist/todolist.component';
export { TodosModel } from './models/todos.model';
export { TodoService } from './services/todo.service';
Since all of the parts are available via the public API, the Todo feature can be installed as a dependency and its parts imported individually.
Just create a new application-specific Todo feature and import the parts from the original that can be reused. In this case the TodoComponent and TodoService will need to be created, the TodoModel TodoItem, and TodoListService can be reused.
Usefulness Factor: 11/10
None of these techniques are 'novel'. The ES6 module loader allows a degree of control over imports that didn't exist previously. The facade pattern is used extensively throughout the Angular2 source. All this guide provides is the means to effectively leverage both.