原文地址: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.
隨著應(yīng)用程序的增長(zhǎng)晶府,我們經(jīng)常會(huì)發(fā)現(xiàn)文件結(jié)構(gòu)和組織對(duì)于代碼的維護(hù)至關(guān)重要兜粘。
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.
在這篇文章里柬帕,我會(huì)講述親自在項(xiàng)目中實(shí)踐出來的三條組織規(guī)則赵誓。遵循這些規(guī)則汤功,你的應(yīng)用代碼會(huì)更加易讀告私,同時(shí)你會(huì)發(fā)現(xiàn)自己不用再把時(shí)間浪費(fèi)在文件導(dǎo)航馁龟,頻繁重構(gòu)以及bug修復(fù)上了蹋盆。
I hope that these tips will prove useful for developers who want to improve their application structure, but don’t know where to start.
我希望這些建議對(duì)于那些想要提升他們的代碼結(jié)構(gòu)俭令,但是不知道從何入手的的開發(fā)者來說是有幫助的后德。
Three rules for project structure | 三條關(guān)于項(xiàng)目結(jié)構(gòu)的建議
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.
下面是一些關(guān)于項(xiàng)目結(jié)構(gòu)基本的規(guī)則。要知道的是抄腔,這些規(guī)則與框架和語(yǔ)言本身無關(guān)瓢湃,所以你可以在任何情況下應(yīng)用它們。但是赫蛇,我們是以React和Redux為例绵患,熟悉這些框架會(huì)很有用。
Rule #1: Organize by feature | 規(guī)則#1:基于特征組織
Let’s first start by going over what not to do. A common way that projects can be organized is by object roles.
首先讓我們來看一下什么是不能做的悟耘,常見的一種方式是根據(jù)對(duì)象角色來組織代碼結(jié)構(gòu)落蝙。
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.
看起來把相似的對(duì)象組織在一起是合理的,就像這樣(controllers with controllers,components with components),但是隨著應(yīng)用的增長(zhǎng)筏勒,這樣的結(jié)構(gòu)將不利于擴(kuò)展赚瘦。
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.
當(dāng)你增加或者修改特征時(shí),你會(huì)注意到某些部分的對(duì)象也會(huì)傾向于被修改奏寨。這些對(duì)象在一起組成了一個(gè)特征模塊起意。比如,在一個(gè)todo應(yīng)用里病瞳,每當(dāng)你修改reducers/todos.js
文件時(shí)揽咕,很有可能也要修改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.
比起浪費(fèi)時(shí)間在整個(gè)文件夾中找到todos相關(guān)的文件,更好的做法是將這些文件放到一個(gè)地方套菜。
A better way to structure Redux + React project: | 一個(gè)更好地組織Redux + React 項(xiàng)目的方法:
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.
提示:我將在下一篇文章里深入講解這些文件的細(xì)節(jié)
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.
在一個(gè)大型項(xiàng)目中亲善,按照特征組織代碼讓你能夠?qū)W⒂诮谑诌叺奶卣鳎挥脫?dān)心瀏覽整個(gè)項(xiàng)目逗柴。也就是說如果我需要修改todos相關(guān)的東西蛹头,我可以單獨(dú)工作在當(dāng)前模塊下,不用考慮應(yīng)用程序的其他部分戏溺。感覺上像是創(chuàng)建了一個(gè)主應(yīng)用程序中的應(yīng)用程序
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.
表面上看渣蜗,按照特征進(jìn)行組織像是與審美有關(guān),不過就如我們?cè)俳酉聛淼膬蓚€(gè)規(guī)則中所看到的那樣旷祸,這種構(gòu)建項(xiàng)目的方式將會(huì)簡(jiǎn)化你的應(yīng)用程序代碼耕拷。
Rule #2: Create strict module boundaries | 設(shè)計(jì)嚴(yán)格的模塊邊界
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中,將負(fù)責(zé)度定義為編織(或者交織)的東西托享。當(dāng)你將兩個(gè)模塊耦合在一起骚烧,你會(huì)在你的代碼中看到某種和現(xiàn)實(shí)中的纏結(jié)或辮子一樣的形態(tài)。

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.
項(xiàng)目結(jié)構(gòu)的復(fù)雜度相關(guān)指的是闰围,當(dāng)你把一個(gè)對(duì)象靠近于另一個(gè)對(duì)象時(shí)赃绊,把其耦合在一起的障礙就會(huì)顯著減少
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.
舉個(gè)例子來說,我們想要在TODO應(yīng)用中增加一個(gè)新的特征:在項(xiàng)目中維護(hù)待辦列表羡榴。這就意味著我們將將會(huì)創(chuàng)建一個(gè)新的名為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.
現(xiàn)在,projects模塊顯然會(huì)依賴todos炕矮。在這種情況下么夫,嚴(yán)格約束,僅耦合于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.
另外一點(diǎn)就是避免和其他模塊的狀態(tài)產(chǎn)生耦合。比如涉枫,在projects模塊中邢滑,我們需要從todos的狀態(tài)中獲取信息用來渲染組件。更好的做法是todos模塊為projects暴露一個(gè)接口用來查詢信息,而不是和todos狀態(tài)交織在一起困后。
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模塊并不關(guān)心todos模塊內(nèi)部的狀態(tài)。這樣非常地好摇予,因?yàn)槲覀兛梢宰杂筛淖僼odos狀態(tài)的結(jié)構(gòu)而不用擔(dān)心破壞其他依賴的模塊汽绢。當(dāng)然我們還是需要維護(hù)selector契約,但是另一種選擇則必須找遍所有不相干的組件侧戴,然后再依次重構(gòu)它們宁昭。
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.
通過人為地建立嚴(yán)格的模塊邊界,我們就能夠簡(jiǎn)化應(yīng)用程序代碼酗宋,同時(shí)也可以提高應(yīng)用程序的可維護(hù)性积仗。我們應(yīng)該思考如何組織和維護(hù)模塊之間的契約,而不是隨意侵入到模塊里面蜕猫。
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.
既然項(xiàng)目已經(jīng)是根據(jù)特性來組織寂曹,每個(gè)特性之間也有明顯的界限,那么接下來就要涉及到最后一件事:循環(huán)依賴
Rule #3: Avoid circular dependencies | 避免循環(huán)依賴
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.
不用我說你也知道回右,循環(huán)依賴非常不好隆圆。然而,如果沒有合適的項(xiàng)目結(jié)構(gòu)翔烁,就很容易陷入循環(huán)依賴的陷進(jìn)匾灶。
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.
大多數(shù)情況下,依賴一開始是無害的租漂。我們可能認(rèn)為projects模塊需要在todos的actions來reduce一些狀態(tài)阶女。如果我們不是按照特性來組織的,就會(huì)看見一個(gè)全局的actionTypes.js文件中包含了所有的actions類型清單哩治,對(duì)我們來說秃踩,根本無需考慮,就很容易獲取到我們所需要的信息(在當(dāng)時(shí))业筏。
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.
假設(shè)憔杨,在todos內(nèi)部,我們想根據(jù)projects的action類型來reduce狀態(tài)蒜胖。如果我們已經(jīng)有一個(gè)全局的actionTypes.js文件的話消别,就足夠簡(jiǎn)單了。但是台谢,我們很快就會(huì)知道寻狂,如果我們有明顯的模塊邊界的話,這些都不足掛齒朋沮。為了說明原因蛇券,來看看下面的例子。
Circular dependency example | 循環(huán)依賴事例
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?
下面的代碼會(huì)產(chǎn)生什么樣的結(jié)果?
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).
我們可能以為會(huì)打印“Hello Alice!”纠亚,但實(shí)際上塘慕,a()會(huì)打印“Hello undefined!”。這是因?yàn)樵赽導(dǎo)入a的時(shí)候蒂胞,從a中導(dǎo)出的name是無可用的(因?yàn)檠h(huán)依賴)图呢。
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!
這里有一個(gè)暗示,我們不能讓projects依賴todos內(nèi)部的action類型同時(shí)todos也依賴projects內(nèi)部的action類型骗随。你可以用聰明的方式繞過這種限制蛤织,但是如果你這樣一直下去,不久之后就被它坑(循環(huán)依賴)的蚊锹。
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.
換句話說瞳筏,通過創(chuàng)建循環(huán)依賴,你是在用最糟糕的方式打著繩結(jié)牡昆。把一個(gè)模塊想象成一縷頭發(fā)姚炕,然后所有模塊相互依賴在一起形成一個(gè)又大又亂的毛球。

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.
無論什么時(shí)候你想使用一個(gè)毛球中的小模塊丢烘,你都別無選擇地陷入巨大的混亂中柱宦。更糟糕的是,當(dāng)你改變了毛球中的某些東西播瞳,你將很難不去破壞其他東西掸刊。
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.
只要遵循規(guī)則#2,你便不會(huì)輕易地產(chǎn)生循環(huán)依賴赢乓。不要去對(duì)抗它忧侧,要用這份精力去適當(dāng)分解你的模塊。
Now that we have our three rules, there is one last topic I want to discuss: How to detect project smells.
現(xiàn)在我們已經(jīng)知道了三條規(guī)則牌芋,我還想討論最后一個(gè)話題:如何發(fā)現(xiàn)項(xiàng)目壞味道蚓炬。
Litmus test for project structure | 項(xiàng)目結(jié)構(gòu)的石蕊測(cè)試
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.
對(duì)我們來說,使用工具來告訴我們代碼中的壞味道是非常重要的躺屁。從經(jīng)驗(yàn)來看肯夏,項(xiàng)目?jī)H僅一開始整潔不代表會(huì)一直整潔下去。因此犀暑,我想提出一種簡(jiǎn)單的方式用來偵測(cè)到項(xiàng)目結(jié)構(gòu)壞味道驯击。
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).
每隔一段時(shí)間,在你的應(yīng)用程序中選一個(gè)模塊耐亏,試圖把它提取成一個(gè)外部模塊(e.g. 一個(gè)NodeJS模塊徊都,Ruby gem 等)。你沒必要真的這樣做苹熏,但至少要想那樣去思考一遍碟贾。如果你不怎么費(fèi)勁就可以做到提取币喧,你就知道這個(gè)模塊已經(jīng)很好地分解了轨域。在這里“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.
在你的應(yīng)用程序的其他模塊中去試驗(yàn)一下干发。簡(jiǎn)單記下任何你發(fā)現(xiàn)的問題:循環(huán)依賴朱巨,違反模塊邊界等。
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.
基于你的發(fā)現(xiàn)枉长,無論你是否采取動(dòng)作冀续,這都取決于你。畢竟必峰,軟件工程是一個(gè)與折中息息相關(guān)的行業(yè)洪唐。但是你至少應(yīng)該對(duì)你的項(xiàng)目結(jié)構(gòu)有一個(gè)更深入的了解。
Summary | 總結(jié)
Project structure isn’t a particularly exciting topic to discuss. It is, however, an important one.
項(xiàng)目結(jié)構(gòu)不是特別能令人興奮的話題討論吼蚁,但是凭需,它卻非常重要。
The three rules presented in this post are:
1.Organize by features
2.Create strict module boundaries
3.Avoid circular dependencies
這篇文章講的三條規(guī)則是:
- 基于特性組織
- 設(shè)計(jì)嚴(yán)格的模塊邊界
- 避免循環(huán)依賴