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

Simplify directory structure #36

Closed corydeppen closed 8 years ago

corydeppen commented 8 years ago

I know the discussion about the directory structure was a lengthy one, but I was wondering what you thought about simplifying it. The recommended structure seems a bit verbose, including a prefix that could be removed from each file and instead be used to create a containing directory, as well as a repetitive "component" segment that seems unnecessary for non-TS files. An alternative to the current directory structure might look something like the following:

|- admin
|   |- home-dashboard
|   |   |- component.ts
|   |   |- styles.css
|   |   |- template.html
|   |- login
|   |   |- component.ts
|   |- models
|   |   |- admin.ts
|   |- services
|   |   |- order-management.ts
|   |   |- user-management.ts
|- shared
|   |- avatar
|   |   |- component.ts
|   |   |- template.html
|   |- directives
|   |   |- form-validator.ts
|   |   |- tooltip.ts
|   |- login-form
|   |   |- component.ts
|   |   |- styles.css
|   |   |- template.html
|   |- pipes
|   |   |- format-order-name.ts
|   |- services
|   |   |- authorization.ts
|- shop
|   |- edit-profile
|   |   |- component.ts
|   |   |- styles.css
|   |   |- template.html
|   |- home
|   |   |- component.ts
|   |   |- template.html
|   |- models
|   |   |- shopping-cart.ts
|   |   |- shopping-item.ts
|   |   |- user.ts
|   |- register
|   |   |- component.ts
|   |- services
|   |   |- checkout.ts
joshwiens commented 8 years ago

@corydeppen I think verbose was the point. If you think about opening a large application as consumer as opposed to the author, verbosity makes an application easier to learn at the price of a little extra typing.

We spend significantly more time reading code than we do writing it, anything to make that task easier is something i'm all for.

I get where you are trying to go with this and at a smaller scale, I'd agree this may be easier. Here is where I have an issue with "easier" and why I prefer the verbosity. I lead a mid-sized team, somewhere in the 20's depending on where you draw the "dev" line and most of use wear more than one hat.

In the world I work in, readability & verbosity speed the time to production. Be it from turn over or the fact that as full stack devs, you aren't afforded the time to gain intimate knowledge of a single feature, much less the entire application.

I need to be able to open a project, scan the directory, find exactly what I want rapidly, execute a change and move on.

I personally feel that it's a little redundancy / verbosity / whatever you want to call it is a small price to pay to improve the development experience and decrease the learning / familiarity curve for the people who will have to consume, learn and extend our work.

Just my $0.02

mgechev commented 8 years ago

@corydeppen we had a discussion about the verbosity, especially in using code unit type suffix for already grouped in a directory files (for instance todo-item.component.ts in a directory components). Our motivation behind this convention is that the usually a project scales iteratively. In the first iterations we may have only a few files and distinguish their content based on their suffix. Once we group them in a separate directory we have to do a lot of manual error-prone work for removing this suffix. We also decided to keep the suffix for templates and styles for consistency.

I like your suggestion and it seems simple - create a directory with the component name and put all the assets belonging to given component there. All other code units go to their specific directory (services go to services, models to models, etc.). What bothers me is that this consistency could be easily broken once you add service.ts service in home-dashboard, or model.ts or whatever. Also, most components are composition of other components. Should the children components belong to the same folder where their parent resides in? This may break reusability as @e-oz mentioned in the directory structure discussion.

On the other hand, I really like that in each component directory you know that the file called component.ts will hold the component's logic, style.css its styles and template.html the template. It also seems to scale well since you create a directory based on the components which are potential roots of "bounded contexts" with different levels of granularity. Its scalability is very natural.

As recap:

Pros

Cons

I'd love to keep this issue open for other opinions.

// cc @evanplaice @ludovichenin @nareshbhatia @ludohenin

e-oz commented 8 years ago

Idea that each component can have own set of services is absolutely show-stopper for me. Not sure if votes are counted here, but I would vote against it. Services is pure logic, they should know nothing about the app, about templates, folders structures, etc. And that's why each component can easily reuse them and that's why services can be moved to npm_modules as the first candidates, to be reused in other projects of your company, or to mobile app, working with the same server API.

joshwiens commented 8 years ago

@corydeppen - I also think that in a complex packing / lazy loading / multi-level acl application ( where I spend my of my time ) the simplified naming and different directory structure is going to make life more difficult on the Ops side.

In my experience, verbosity & consistency help to simplify some of the complexities above.

That said, it's a guess and the only real way to prove that would be to build it.

corydeppen commented 8 years ago

While I won't claim to have worked on any complex Angular applications, I don't feel that the Angular community should be guiding directory structure decisions based on application complexity. One of the many parts of Ember's philosophy I admire is the idea that regardless of what kind of application is built using Ember CLI, it will always be familiar to any developer who browses the source. So whether it's a new member to your team, or a new application the team needs to enhance or support, the structure will remain the same and should be easily digestible. Having a common structure and setting aside the complexity factor should only help with the adoption of Angular CLI.

I think verbose was the point. If you think about opening a large application as consumer as opposed to the author, verbosity makes an application easier to learn at the price of a little extra typing.

I personally feel that it's a little redundancy / verbosity / whatever you want to call it is a small price to pay to improve the development experience and decrease the learning / familiarity curve for the people who will have to consume, learn and extend our work.

@d3viant0ne I don't know about relying on verbosity to make something easier to learn. Conventions and consistency should enable developers to more easily understand and more effectively work with a code base.

joshwiens commented 8 years ago

I don't feel that the Angular community should be guiding directory structure decisions based on application complexity

I don't feel we should be guiding decisions based on any single point of view. We do on the other hand need to come up with a solution that allows an application to evolve in complexity as seamlessly as possible.

With that there are going to be tradeoffs on both ends of the spectrum to find some sort of middle ground.

I don't know about relying on verbosity to make something easier to learn. Conventions and consistency should enable developers to more easily understand and more effectively work with a code base.

I didn't make my point very well now that I read it again. I was speaking to the velocity in which a developer can scan, find and execute a task be it a code review, modification or bug fix. I personally agree with the existing proposal because looking at the structure / naming I can find what I am looking for without having to think about what I am looking at.

What's proposed in #5 evolves naturally through the complexity scale while maintaining the ability to scan and find things quickly. Someone could open a larger scale project and get a feel for it by just scanning the file tree.

That said, I think we all agree on the point about conventions & consistency. The topic I have noticed that people fall on one side of the fence or the other is verbosity.

I will choose more effort in the beginning to prevent a lot of pain later every day of the week.

aderito7 commented 8 years ago
- app.html - app.ts - components - home - - home.html - - home.scss - - home.ts
- list-item
- - list-item.html
- - list-item.scss
- - list-item.ts
-
- list
- list.html
- list.ts
-

+1 for the idea of the OP, I've found working with ionic's template suits me very well as I see every item I create as an independent component (no pun intended) of my entire app. It's more modular, if you'd like to say. Further I can continue to add

- shared - services - interfaces (for typings) - factories (still can't get over them) - config (Headers, Routes, App variables, etc.) - components (perhaps for more generic stuff like a popup, I don't know)
evanplaice commented 8 years ago

@corydeppen

|   |- home-dashboard
|   |   |- component.ts
|   |   |- styles.css
|   |   |- template.html
|   |   |- mobile.template.ts
|   |   |- some-sub.component.ts

Components are given names based on what they do. In practice many features/bounded-contexts will be made up of a component and multiple sub-components.

Having a common structure and setting aside the complexity factor should only help with the adoption of Angular CLI.

The type suffixes were copied directly from the angular-cli project.

Ref: https://github.com/angular/angular-cli

@e-oz I think we already covered services. If you prefer to add services at the application-level rather than directly to a bounded context, they can be placed in /shared. If your whole application represents one single bounded context then it's perfectly reasonable to separate the source into /components, /services, etc.

e-oz commented 8 years ago

I will keep my services in src/app/services. If somebody's services are so non-reusable that they should be placed in some page-specific folder... my condolences.

On Fri, 11 Mar 2016 at 00:41, Evan Plaice notifications@github.com wrote:

@corydeppen https://github.com/corydeppen

| |- home-dashboard | | |- component.ts | | |- styles.css | | |- template.html | | |- mobile.template.ts | | |- some-sub.component.ts

Components are given names based on what they do. In practice many features/bounded-contexts will be made up of a component and multiple sub-components.

Having a common structure and setting aside the complexity factor should only help with the adoption of Angular CLI.

The type suffixes were copied directly from the angular-cli project.

Ref: https://github.com/angular/angular-cli

@e-oz https://github.com/e-oz I think we already covered services. If you prefer to add services at the application-level rather than directly to a bounded context, they can be placed in /shared. If your whole application represents one single bounded context then it's perfectly reasonable to separate the source into /components, /services, etc.

— Reply to this email directly or view it on GitHub https://github.com/mgechev/angular2-style-guide/issues/36#issuecomment-195101683 .

evanplaice commented 8 years ago

@corydeppen Ironically, our example is actually more complex. It presents a directory structure where the contexts are nested two-layers deep. Aside from breaking the file naming convention, your example doesn't break the conventions already mentioned in the guide, it just extends them further.

Levels of complexity describe the unit of measure, ie a bounded context.

A project may contain:

The subdirectory structure may be:

Flat, single context:

|- admin
|   |- admin.ts  <- library/application facade
|   |- home-dashboard.component.ts
|   |- home-dashboard.styles.css
|   |- home-dashboard.template.html
|   |- login.component.ts
|   |- admin-model.ts
|   |- order-management.service.ts
|   |- user-management.service.ts
|   |- avatar.component.ts
|   |- avatar.template.ts

Note: Great for very simple applications, libraries, and toy samples.

Nested, single context:

|- admin
|   |- admin.ts  <- library/application facade
|   |- components
|      |- home-dashboard.component.ts
|      |- home-dashboard.styles.css
|      |- home-dashboard.template.html
|      |- login.component.ts
|      |- avatar.component.ts
|      |- avatar.template.ts
|   |- models
|      |- admin-model.ts
|   |- services
|      |- order-management.service.ts
|      |- user-management.service.ts

Note: Great for simple applications, and more complex libraries.

Flat, multiple context:

|- admin
|-  admin.ts  <- library/application facade
|   |- home
|      |- home.ts  <- context facade
|      |- home-dashboard.component.ts
|      |- home-dashboard.styles.css
|      |- home-dashboard.template.html
|   |- login
|      |- login.ts  <- context facade
|      |- login.component.ts
|      |- admin-model.ts
|- shared
|   |- shared.ts  <- application-wide modules facade
|   |- avatar.component.ts
|   |- avatar.template.ts
|   |- order-management.service.ts
|   |- user-management.service.ts

Note: Great for complex applications, and larger multi-purpose libraries.

Nested, multiple context:

|- admin
|-  admin.ts  <- library/application facade
|   |- home
|      |- home.ts  <- context facade
|      |- components
|         |- home-dashboard.component.ts
|         |- home-dashboard.styles.css
|         |- home-dashboard.template.html
|      |- models
|         |- ...
|      |- pipes
|         |- ...
|      |- services
|         |- ...
|   |- login
|      |- login.ts  <- context facade
|      |- components
|         |- login.component.ts
|      |- models
|         |- admin-model.ts
|      |- pipes
|         |- ...
|      |- services
|         |- ...
|- shared
|   |- shared.ts  <- application-wide modules facade
|   |- components
|      |- avatar.component.ts
|      |- avatar.template.ts
|   |- services
|      |- order-management.service.ts
|      |- user-management.service.ts

Note: Great for large complex applications, and very larger multi-purpose libraries.

Naming

The file names are always consistent, the structure may change if/when the project grows in size/complexity. This isn't a great example because each structure uses the same number of files.

Facades

The facades are completely optional and only act as a public API for the library/application/context.

At the application-level, a facade is provided as a public API for developers who consume your library/application as a dependency. External import statements should point to the facade to prevent from creating deep links into the project's folder structure. This prevents changes to the structure/implementation within the application from unnecessarily breaking code that depends on it.

At the context-level, a facade is provided to provide a public API for each context. It avoids deep linking across contexts so the internal implementation for each context is free to change over time without breaking existing dependent code.

In traditional OOP languages, access to internal implementation details would likely be blocked altogether by specifying classes/methods/members as private/internal. JS provides no such protections but the facade pattern can be used as a clean convention for defining public APIs.

It's a new feature that is only made available with the introduction of ES6 module imports. To see it in action just take a look at the Angular2 source, they use it extensively to organize the application structure.

Reuse

If it makes sense to reuse a context in another project just copy the folder contents into a separate project, making it a single context project. If it already includes a facade, the context facade becomes the library/application facade with no changes to the code.

If you remove the context from a project and add it back in as an external dependency (assuming you used a context-level facade) all you have to do to update the code is search/replace imports that point to the previous context-level facade.

Without a facade to act as the public API for the context, the import links would be different for every file and highly dependent on the file structure.


I have already used this pattern in practice with success. The problem directory structures is that the import statements traditionally depend on the folder structure staying the same. Deep linking into the file structure of a project is fragile. If the structure ever changes it's much too easy to miss an import statement and break production code. Therefore, it was useful to stick to 'one-true' folder structure for everything.

ES6 imports and the introduction of facades is much more robust in practice. It greatly reduces the variation/complexity of import statements therefore it's not nearly as risky to change/adapt the project directory structure as a project grows in size/scope.

The lack of good language-level API definition support as well as the lack of solid conventions in the JS community is part of the reason why JS applications are so difficult to scale. These practices aim to solve both.


Here's an example of your folder structure following the existing convention and using the next level of complexity.

Flat, multiple contexts (nested 2 layers deep)

|- app.ts  <- library/application facade
|- admin
|   |- admin.ts  <- context facade
|   |- home-dashboard
|       |- home-dashboard.component.ts
|       |- home-dashboard.styles.css
|       |- home-dashboard.template.html
|   |- login
|       |- login.component.ts
|   |- shared
|      |- models
|         |- admin.model.ts
|      |- services
|         |- order-management.service.ts
|         |- user-management.service.ts
|- shared
|   |- shared.ts <- application-wide modules facade
|   |- avatar
|      |- avatar.component.ts
|      |- avatar.template.html
|   |- login-form
|      |- login-form.component.ts
|      |- login-form.styles.css
|      |- login-form.template.html
|   |- shared
|      |- directives
|         |- form-validator.directives.ts
|         |- tooltip.directives.ts
|      |- pipes
|         |- format-order-name.pipe.ts
|      |- services
|         |- authorization.service.ts
|- shop
|   |- shop.ts  <- context facade
|   |- edit-profile
|       |- edit-profile.component.ts
|       |- edit-profile.styles.css
|       |- edit-profile.template.html
|   |- home
|       |- home.component.ts
|       |- home.template.html
|   |- register
|       |- register.component.ts
|   |- shared
|      |- models
|         |- shopping-cart.model.ts
|         |- shopping-item.model.ts
|         |- user.model.ts
|      |- services
|         |- checkout.service.ts

Note: Nest as many layers deep as necessary if your application calls for it.

Look familiar? It's the same pattern used above but extended to an additional layer of complexity. The benefit of this approach is that there's no upper bound to growth/complexity.

Considering that your 'simple' approach turned out to be the most complex of the bunch, it's probably safe to say that the Ember.js community chose a directory structure that could grow to reasonably support very large codebases by default. It favors a one-size-fits-all convention which isn't a bad practice if you're aiming for structural consistency across all of your projects.

Whichever you choose, I think the important point to takeaway here is that directory structure doesn't matter nearly as much as it used to. If you're aiming for long-term maintainability, include a facade and define a public API at the application and/or context level.

There may be a bit of a learning curve for pure JS developers but these conventions should look familiar to those with prior experience developing a shared OOP library codebase where API access structure matters.

I could cover how closures are used to provide public getter/setters in JS but that's a whole other topic altogether.

mgechev commented 8 years ago

There's a new directory structure which enforces lazy-loading. It reuses the ideas by @corydeppen to big extent.

gravity-addiction commented 8 years ago

is the angular2 style guide directory structure enforced because of lazy loading? I'm really confused how I am going to structure my app with a subdirectory per page type of structure.. all my pages are built off shared components.. I'd basically be putting every component into the shared folder at this point. the app folder would simply hold the wireframe for pages accessing shared components. I really do not understand the logic behind putting your service file in with your model files, they should know nothing of each other and be totally standalone.

evanplaice commented 8 years ago

@gravity-addiction Angular2 uses a component-first approach. Services are coupled to highest level component in the tree where they're used. This approach improves reusability. For instance, a component that includes dependent sub-components/services/models can be extracted and versioned separately for use in other applications.

In your specific case, if all your services/models are used application-wide then you're right to assume that app/shared is the preferred place to locate them. The app folder would contain a tree of components used in your application. Specifically, the app component (ie wireframe) as well as subcomponents for various menus and page-specific sections.

Lazy-loading (sans a custom loader) is just a beneficial side-effect of using this structure.