原文地址:https://jaysoo.ca/2016/02/28/organizing-redux-application
As our applications grow, we often find that file structure and organization to be crucial for the maintainability of application code.
随着应用程序的增长,我们经常会发现文件结构和组织对于代码的维护至关重要。
What I want to do in this post is to present three organizational rules that I personally follow on my own projects. By following the rules, your application code should be easier to reason about, and you will find yourself wasting less time on file navigation, tedious refactoring, and bug fixes.
在这篇文章里,我会讲述亲自在项目中实践出来的三条组织规则。遵循这些规则,你的应用代码会更加易读,同时你会发现自己不用再把时间浪费在文件导航,频繁重构以及bug修复上了。
I hope that these tips will prove useful for developers who want to improve their application structure, but don’t know where to start.
我希望这些建议对于那些想要提升他们的代码结构,但是不知道从何入手的的开发者来说是有帮助的。
Three rules for project structure | 三条关于项目结构的建议
The following are some basic rules for structuring a project. It should be noted that the rules themselves are framework and language agnostic, so you should should be able to follow them in all situations. However, the examples are in React and Redux. Familiarity with these frameworks is useful.
下面是一些关于项目结构基本的规则。要知道的是,这些规则与框架和语言本身无关,所以你可以在任何情况下应用它们。但是,我们是以React和Redux为例,熟悉这些框架会很有用。
Rule #1: Organize by feature | 规则#1:基于特征组织
Let’s first start by going over what not to do. A common way that projects can be organized is by object roles.
首先让我们来看一下什么是不能做的,常见的一种方式是根据对象角色来组织代码结构。
Redux + React:
actions/
todos.js
components/
todos/
TodoItem.js
...
constants/
actionTypes.js
reducers/
todos.js
index.js
rootReducer.js
AngularJS:
controllers/
directives/
services/
templates/
index.js
Ruby on Rails:
app/
controllers/
models/
views/
It may seem reasonable to group similar objects together like this (controllers with controllers, components with components), however as the application grows this structure does not scale.
看起来把相似的对象组织在一起是合理的,就像这样(controllers with controllers,components with components),但是随着应用的增长,这样的结构将不利于扩展。
When you add and change features, you’ll start to notice that some groups of objects tend to change together. These objects group together to form a feature module. For example, in a todo app, when you change the reducers/todos.js file, it is likely that you will also change actions/todos.js and components/todos/*.js.
当你增加或者修改特征时,你会注意到某些部分的对象也会倾向于被修改。这些对象在一起组成了一个特征模块。比如,在一个todo应用里,每当你修改reducers/todos.js
文件时,很有可能也要修改actions/todos.js
和components/todos/*.js
Instead of wasting time scrolling through your directories looking for todos related files, it is much better to have them sitting in the same location.
比起浪费时间在整个文件夹中找到todos相关的文件,更好的做法是将这些文件放到一个地方。
A better way to structure Redux + React project: | 一个更好地组织Redux + React 项目的方法:
todos/
components/
actions.js
actionTypes.js
constants.js
index.js
reducer.js
index.js
rootReducer.js
Note: I will go into the details of what's inside the files in the next post.
提示:我将在下一篇文章里深入讲解这些文件的细节
In a large project, organizing by feature affords you the ability to focus on the feature at hand, instead of having to worry about navigating the entire project. This means that if I need to change something related to todos, I can work soley within that module and not even think about the rest of the application. In a sense, it creates an application within the main application.
在一个大型项目中,按照特征组织代码让你能够专注于近在手边的特征,而不用担心浏览整个项目。也就是说如果我需要修改todos相关的东西,我可以单独工作在当前模块下,不用考虑应用程序的其他部分。感觉上像是创建了一个主应用程序中的应用程序
On the surface, organizing by feature may seem like an aesthetics concern, but as we will see in the next two rules, this way of structuring projects will help simplify your application code.
表面上看,按照特征进行组织像是与审美有关,不过就如我们再接下来的两个规则中所看到的那样,这种构建项目的方式将会简化你的应用程序代码。
Rule #2: Create strict module boundaries | 设计严格的模块边界
In his Ruby Conf 2012 talk Simplicity Matters, Rich Hickey defines complexity as the complecting(or interleaving) of things. When you couple modules together, you can picture an actual knot or braid forming in your code.
Rich Hickey在他的Ruby Conf 2012 演讲Simplicity Matters中,将负责度定义为编织(或者交织)的东西。当你将两个模块耦合在一起,你会在你的代码中看到某种和现实中的缠结或辫子一样的形态。
The relevence of complexity to project structure is that when you place objects in close proximity to one another, the barrier to couple them lowers dramatically.
项目结构的复杂度相关指的是,当你把一个对象靠近于另一个对象时,把其耦合在一起的障碍就会显著减少
As an example, let’s say that we want to add a new feature to our TODO app: We want the ability to manage TODO lists by project. That means we will create a new module called projects.
举个例子来说,我们想要在TODO应用中增加一个新的特征:在项目中维护待办列表。这就意味着我们将将会创建一个新的名为projects的模块。
projects/
components/
actions.js
actionTypes.js
reducers.js
index.js
todos/
index.js
Now, it is obvious that the projects module will have a dependency on todos. In this situation, it is important that we exercise discipline and only couple to the “public” API exposed in todos/index.js.
现在,projects模块显然会依赖todos。在这种情况下,严格约束,仅耦合于todos/index.js中暴露出来的"公共"接口就变得非常重要。
BAD
import actions from '../todos/actions';
import TodoItem from '../todos/components/TodoItem';
GOOD
import todos from '../todos';
const { actions, TodoItem } = todos;
Another thing to avoid is coupling to the state of another module. For example, say that within the projects module, we need to grab information out of todos state in order to render a component. It is better that the todos module exposes an interface for projects to query this information, rather than complecting the component with todos state.
另外一点就是避免和其他模块的状态产生耦合。比如,在projects模块中,我们需要从todos的状态中获取信息用来渲染组件。更好的做法是todos模块为projects暴露一个接口用来查询信息,而不是和todos状态交织在一起。
BAD
const ProjectTodos = ({ todos }) => (
<div>
{todos.map(t => <TodoItem todo={t}/>)}
</div>
);
// Connect to todos state
const ProjectTodosContainer = connect(
// state is Redux state, props is React component props.
(state, props) => {
const project = state.projects[props.projectID];
// This couples to the todos state. BAD!
const todos = state.todos.filter(
t => project.todoIDs.includes(t.id)
);
return { todos };
}
)(ProjectTodos);
GOOD
import { createSelector } from 'reselect';
import todos from '../todos';
// Same as before
const ProjectTodos = ({ todos }) => (
<div>
{todos.map(t => <TodoItem todo={t}/>)}
</div>
);
const ProjectTodosContainer = connect(
createSelector(
(state, props) => state.projects[props.projectID],
// Let the todos module provide the implementation of the selector.
// GOOD!
todos.selectors.getAll,
// Combine previous selectors, and provides final props.
(project, todos) => {
return {
todos: todos.filter(t => project.todoIDs.includes(t.id))
};
}
)
)(ProjectTodos);
In the “GOOD” example, the projects module is not concerned with the internal state of todos module. This is powerful because we can freely change the structure of the todos state, without worrying about breaking other dependent modules. Of course we still need to maintain our selector contracts, but the alternative is having to search through a whole bunch of disparate components and refactor them one by one.
在“GOOD”的例子中,projects模块并不关心todos模块内部的状态。这样非常地好,因为我们可以自由改变todos状态的结构而不用担心破坏其他依赖的模块。当然我们还是需要维护selector契约,但是另一种选择则必须找遍所有不相干的组件,然后再依次重构它们。
By artificially creating strict module boundaries, we can simplify our application code, and in turn increase the maintainability of our application. Instead of haphazardly reaching inside other modules, we should think about forming and maintaining contracts between them.
通过人为地建立严格的模块边界,我们就能够简化应用程序代码,同时也可以提高应用程序的可维护性。我们应该思考如何组织和维护模块之间的契约,而不是随意侵入到模块里面。
Now that the projects are organized by features, and we have explicit boundaries between each feature, there is one last thing I want to cover: circular dependencies.
既然项目已经是根据特性来组织,每个特性之间也有明显的界限,那么接下来就要涉及到最后一件事:循环依赖
Rule #3: Avoid circular dependencies | 避免循环依赖
It shouldn’t take too much convincing for you to believe me when I say that circular dependencies are bad. Yet, without proper project structure, it is all too easy to fall into this trap.
不用我说你也知道,循环依赖非常不好。然而,如果没有合适的项目结构,就很容易陷入循环依赖的陷进。
Most of the time, dependencies start out innoculously. We may think that the projects module need to reduce some state based on todos actions. If we are not grouping by features, and we see a large manifest of all action types within a global actionTypes.js file, it is all too easy for us to just reach in and grab what we need (at the time) without a second thought.
大多数情况下,依赖一开始是无害的。我们可能认为projects模块需要在todos的actions来reduce一些状态。如果我们不是按照特性来组织的,就会看见一个全局的actionTypes.js文件中包含了所有的actions类型清单,对我们来说,根本无需考虑,就很容易获取到我们所需要的信息(在当时)。
Say, that within todos we want to reduce state based on an action type of projects. Easy enough if we have a global actionTypes.js file. However, we will soon learn that this is no easy feat if we have explicit module boundaries. To illustrate why, consider the following example.
假设,在todos内部,我们想根据projects的action类型来reduce状态。如果我们已经有一个全局的actionTypes.js文件的话,就足够简单了。但是,我们很快就会知道,如果我们有明显的模块边界的话,这些都不足挂齿。为了说明原因,来看看下面的例子。
Circular dependency example | 循环依赖事例
Given:
a.js
import b from './b';
export const name = 'Alice';
export default () => console.log(b);
b.js
import { name } from './a';
export default `Hello ${name}!`;
What happens with the following code?
下面的代码会产生什么样的结果?
import a from './a';
a(); // ???
We might expect “Hello Alice!” to be printed, but in actuality, a() would print “Hello undefined!”. This is because the name export of a is not available when a is imported by b (due to circular dependencies).
我们可能以为会打印“Hello Alice!”,但实际上,a()会打印“Hello undefined!”。这是因为在b导入a的时候,从a中导出的name是无可用的(因为循环依赖)。
The implication here is that we cannot both have projects depend on action types within todosand todos depend on action types within projects.** You can get around this restriction in clever ways, but if you go down this road I can guarantee you that it will come to bite you later on!
这里有一个暗示,我们不能让projects依赖todos内部的action类型同时todos也依赖projects内部的action类型。你可以用聪明的方式绕过这种限制,但是如果你这样一直下去,不久之后就被它坑(循环依赖)的。
Don’t make hairballs! | 不要制造毛球
Put another way, by creating circular dependencies, you are complecting in the worst possible way. Imagine a module to be a strand of hair, then modules that are inter-dependent on each other form a big, messy hairball.
换句话说,通过创建循环依赖,你是在用最糟糕的方式打着绳结。把一个模块想象成一缕头发,然后所有模块相互依赖在一起形成一个又大又乱的毛球。
Whenever you want to use a small module within the hairball, you will have no choice but to pull in the giant mess. And even worse, when you change something inside the hairball, it would be hard notto break something else.
无论什么时候你想使用一个毛球中的小模块,你都别无选择地陷入巨大的混乱中。更糟糕的是,当你改变了毛球中的某些东西,你将很难不去破坏其他东西。
By following Rule #2, it should make it hard for you to create these circular dependencies. Don’t fight against it. Instead, use that energy to properly factor your modules.
只要遵循规则#2,你便不会轻易地产生循环依赖。不要去对抗它,要用这份精力去适当分解你的模块。
Now that we have our three rules, there is one last topic I want to discuss: How to detect project smells.
现在我们已经知道了三条规则,我还想讨论最后一个话题:如何发现项目坏味道。
Litmus test for project structure | 项目结构的石蕊测试
It is important for us to have the tools to tell us when something smells in our code. From experience, just because a project starts out clean doesn’t mean it’ll stay that way. Thus, I want to present an easy method to detect project structure smells.
对我们来说,使用工具来告诉我们代码中的坏味道是非常重要的。从经验来看,项目仅仅一开始整洁不代表会一直整洁下去。因此,我想提出一种简单的方式用来侦测到项目结构坏味道。
Every once in a while, pick a module in your application and try to extract it as an external module(e.g. a NodeJS module, Ruby gem, etc). You don’t have to actually do it, but at least think it through. If you can perform this extraction without much effort then you know it is well factored. The term “effort” here remains undefined, so you need to come up with your own measure (whether subjective or objective).
每隔一段时间,在你的应用程序中选一个模块,试图把它提取成一个外部模块(e.g. 一个NodeJS模块,Ruby gem 等)。你没必要真的这样做,但至少要想那样去思考一遍。如果你不怎么费劲就可以做到提取,你就知道这个模块已经很好地分解了。在这里“effort”并没有被下定义,需要你自己去衡量(无论主管还是客观)。
Run this experiment with other modules in your application. Jot down any problems you find in your experiments: circular dependencies, modules breaching boundaries, etc.
在你的应用程序的其他模块中去试验一下。简单记下任何你发现的问题:循环依赖,违反模块边界等。
Whether you choose to take action based on your findings is up to you. Afterall, the software industry is all about tradeoffs. But at the very least it should give you a much better insight into your project structure.
基于你的发现,无论你是否采取动作,这都取决于你。毕竟,软件工程是一个与折中息息相关的行业。但是你至少应该对你的项目结构有一个更深入的了解。
Summary | 总结
Project structure isn’t a particularly exciting topic to discuss. It is, however, an important one.
项目结构不是特别能令人兴奋的话题讨论,但是,它却非常重要。
The three rules presented in this post are:
1.Organize by features
2.Create strict module boundaries
3.Avoid circular dependencies
这篇文章讲的三条规则是:
- 基于特性组织
- 设计严格的模块边界
- 避免循环依赖