mgechev / angular2-style-guide

[Deprecated] Community-driven set of best practices and style guidelines for Angular 2 application development
https://mgechev.github.io/angular2-style-guide/
1.2k stars 98 forks source link

API design #10

Closed evanplaice closed 8 years ago

evanplaice commented 8 years ago

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:

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:

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.

mgechev commented 8 years ago

@evanplaice thanks for the effort! I really like your suggestion on using facades and would love a PR which introduces your ideas in the style guide!

roelleor commented 8 years ago

@evanplaice thanks for this! Question1: Use Case 3: Reuse: Doesn't the installed feature end-up in the node_modules folder? Question2: I personally like the pattern that a component's style is located within the component folder and included from within the @component styles property. What if a reuble component is installed but requires different styling in that project, would this pattern be still implementable somehow? Or is this only possible by using styling on the level of the feature and not the component?

roelleor commented 8 years ago

ps: what about adding an additional layer to the file structure by adding the components in a components directory?

evanplaice commented 8 years ago

@roelleor

Question 1: Yes, installed modules end up in node_modules or jspm_packages if you use JSPM. Generally the tool you use to handle loading external modules will map the package path to a module name.

For example, JSPM uses System.js to load ES6 modules and a config.js file to map package locations to short import paths. So, if you install Rxjs it'll be importable via import rxjs.

I'm not as familiar with how Webpack and/or Typescript are used to load external modules so maybe somebody else can chime in.

Question 2:

I also prefer to include the style file alongside the component. There are other issues that discuss directory structure patterns in much more depth so I won't cover that here.

As for overriding the style of a component, I'm not aware of a way to dynamically load styles in Angular2. What I'd do is expose the other parts (ex services, etc) in the facade and implement a new component that reuses the rest of the library code.

With the facade you can provide individual imports and/or create collections of useful components/directives/pipes/etc. You may want to implement a custom top-level component but that component may include a tree of many subcomponents that can be reused as-is.

Grouping imports is kind of a new concept in Angular2. For instance, before Angular2 was updated to provide them by default you would have to import and the CORE_DIRECTIVES constant and add it under viewDirectives if you wanted access to all of the default templating directives (ex ngFor, ngIf, etc). I need to add an additional use case #5 that covers import grouping.

roelleor commented 8 years ago

@evanplaice thanks for your reply! Not being able to override the style file of a component makes me consider not having component based style files at all, for that lowers the reusability quite a lot. One style file per feature seems more flexible (but one does need a good css naming convention such as BEM methodology for this to work well).

evanplaice commented 8 years ago

@roelleor You might try using a loader instead.

For instance, with System.js you can import CSS files using plugin-css. Likewise, Webpack has a CSS loader plugin. I'm not exactly sure how they'll affect the CSS cascade but it would be worth a try.