Skip to content

API design #10

@evanplaice

Description

@evanplaice

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.

  1. Setup the TodoService so it's available for injection

    app/main.ts

    import { TodoService } from './todo/services/todo.service' // <- deep link ಠ_ಠ
    ...
    bootstrap(MainComponent, [ TodoService ]);
  2. 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

  1. Setup the TodoService so it's available for injection

    app/main.ts

    import { TodoService } from './todo/todo' // <- shallow link ʘ‿ʘ
    bootstrap(MainComponent, [ TodoService ]);
  2. 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...

  1. Update the TodoService reference so it's available for injection

    app/main.ts

    import { TodoService } from 'todo' // <-- was './todo/todo
    bootstrap(MainComponent, [ TodoService ]);
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions