标题是 Repository
,但是内容是我如何从错用的 Repository
变成了感觉还算对的 Repository
的过程。
DDD 里面的 Repository
是一个处理数据存储或者说是数据持久化的单元。通常一个 Aggregate
对应一个 Repository
。对于通常的 web 服务,很多时候我们都是在与数据存储打交道,以至于很多时候存储就成为了真个应用最关键的逻辑了。那么刚刚接触 DDD 的时候,我就觉得 Repository 就是以前经常使用的类似 DAO 的东西。下面这样的代码经常出现在我们的应用里面。
final Application app = applicationRepository.create(name, user, stack);
return Response.created(routes.application(app)).build();
其中 applicationRepository
管理了对于应用的创建。为了创建我们的对象 app
我们将一堆需要的参数扔进一个 applicationRepository
这样子的不知道背后是什么鬼实现的黑盒子,出来就是我们想要的东西了。再看另外一个例子。
public class ApplicationRecord implements Application, Record {
...
@Override
public void removeEnv(String envName) {
mapper.removeEnv(envName, this);
}
...
}
在这里例子里面,application
可以有环境变量,在 application
中提供了一个 removeEnv
的方法,mapper
是一个具体的持久层工具 Mybatis
需要的东西,可以忽略。当我需要删除环境变量的时候,我只需要做如下的事情。
Application app = applicationRepository.ofId(appId);
app.removeEnv(envName);
在这里事实上我根本没有显性的调用任何持久化方法,在 app
里面持久化就偷偷的帮我把事情做了。然后需要注意的是我的 Application
仅仅是一个接口,实现它的是一个 ApplicationRecord
它内部通过注入的方式塞进去了 Mybatis DataMapper
的东西从而实现了持久化的工作。然后在 Mybatis
可以放一个叫做 ObjectFactory
的东西使得 Mybatis
和 java injector 关联子一起,当从 Mybatis
获取对象时 Mybatis
会自动的讲所有的依赖注入到这个对象里。
说白了就是将数据层和模型绑定在一起,持久层做了业务层的事情。
然而我希望的是可以将业务层做成这个样子:
Repository
作为和存储打交道的组件应该仅仅是做持久化,它就是拿来一个对象,然后存到数据库里,没有任何业务逻辑,没有任何花哨的方法。interface ApplicationRepository {
void save(Application application);
Application ofId(String appId);
}
没有什么 addEnv
removeEnv
等等,这些都是 Application
自己要做的。Mybatis
版本的 Repository
具体的 mapper
仅仅出现在 MybatisApplicationRepository
里面,其他地方都不会出现。按照这个思路把上边的代码修改之后是下面这个样子。
final Application app = new Application(name, user, stack);
applicationRepository.save(app);
return Response.created(routes.application(app)).build();
新创建的 app
本身就是一个 POJO
里面全部都是纯粹的业务代码。
public class Application {
private Envs envs;
...
public void removeEnv(String envName) {
envs.remove(envName);
}
...
}
Application
有一个 envs
的属性,在调用 removeEnv
之后,application
的环境变量就更新了。如果需要持久化,就单独调用 applicationRepository
。
Application app = applicationRepository.ofId(appId);
app.removeEnv(envName);
applicationRepository.save(app);
这样的话持久化就和业务逻辑完全的分离开了,所有的 POJO
保证即使没有持久化也都可以正常的运转。领域对象是 class
而不是 interface
保证了内部的逻辑都是包含在业务层的。
沿着上一部分 Get Started Redux,在利用 redux
构建了 store
reducer
之后,现在需要给应用提供个 view
了。我们用当下最流行的 react
。
安装 react
$ npm install --save react react-dom react-redux
其中 react
react-dom
是 react
的原生包,react-redux
提供了一些方便的方法用来将 redux
和 react
一起使用。后面的代码示例会着重讲解。
添加 babel
对 jsx
的支持。
jsx
是 react
支持的一种特殊的语法,这种语法用于将 html
写到 js
中,例如
return (
<p>
Click {value} times.
{ ' ' }
<button onClick={onIncrement}>+</button>
{ ' ' }
<button onClick={onDecrement}>-</button>
</p>
)
目前可以认为 jsx
是写 react
的标配,那么就需要在 babel
中添加一个新的 react
翻译器。
$ npm install --save-dev babel-preset-react
然后修改 .babelrc
{
"presets": ["es2015", "react"]
}
这样,我们就可以开始写 react
了。
在 redux
的官方文档 Use With React 解释了一种 redux
和 react
结合的模式:将 react
的组件分为两种,一种是不与 redux
交互的 presentational
组件,它是一种通用的组件,任何提供给它所需要的 func
或者是 prop
的框架都可以使用它。另一种是 container
组件,是和 redux
的 action
以及 store
绑定的组件。通常都是会先写一个 presentational
组件,然后再创建一个 container
组件对 presentational
组件进行包装后使用。后面的 full example 展示了 Counter
与 Visible Counter
两个不同类型的组件。当然在官方也提供了非常好的例子。
首先展示一下目录结构
.
├── actions
│ └── index.js
├── components
│ ├── App.js
│ └── Counter.js
├── containers
│ └── VisibleCounter.js
├── dist
│ ├── bundle.js
│ ├── index.html
│ └── styles.css
├── entry.js
├── package.json
├── reducers
│ └── counter.js
├── styles
│ ├── index.scss
│ └── theme.scss
├── test
│ └── reducers
│ └── counter.spec.js
└── webpack.config.js
相对于之前的目录结构多了两个目录 containers
components
分别对应了 container
和 presentational
的组件。
首先我们定义一个 presentational
的组件 Counter
components/Counter.js
:
import React, { Component, PropTypes } from 'react';
class Counter extends Component {
render() {
const { value, onIncrement, onDecrement } = this.props;
return (
<p>
Click {value} times.
{ ' ' }
<button onClick={onIncrement}>+</button>
{ ' ' }
<button onClick={onDecrement}>-</button>
</p>
)
}
}
Counter.PropTypes = {
value: PropTypes.number.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired
};
export default Counter;
注意 PropTypes
有点像是一个函数的参数说明,它明确的定义了在使用这个组件时需要提供什么样子的东西,react
官方建议一定要对每一个组件都明确的定义这样的参数说明,它也起到了文档的作用,方便与其他人协作。这里我们需要三个东西:
value
当前的计数onIncrement
当点击 +
时的动作onDecrement
当点击 -
时的动作然后对 Counter
进行包装
containers/VisibleCounter.js
:
import { connect } from 'react-redux';
import Counter from "../components/Counter";
import { increment, decrement } from '../actions/index';
function mapStatToProps(state) {
return {
value: state
}
}
function mapDispatchToProps(dispatch) {
return {
onIncrement: () => {
dispatch(increment())
},
onDecrement: () => {
dispatch(decrement())
}
}
}
const VisibleCounter = connect(
mapStatToProps,
mapDispatchToProps
)(Counter);
export default VisibleCounter;
这里用到了一个 react-redux
提供的方法 connect
它和下文提到的 Provider
配合使用,用于为 react
传递全局的 store
。connect
需要两个函数 mapStatToProps
与 mapDispatchToProps
分别将 store
里的属性和 store
的 dispatch
动作传递给 presentational
组件。上面的代码就分别将 store
的 state
对应给组件的 value
属性,将两个 action
的 dispatch
对应到 onIncrement
与 onDecrement
。
然后还有一个 App
组件,用于包装 VisibleCounter
,它没有任何依赖的属性。
components/App.js
:
import React, { Component } from 'react';
import VisibleCounter from '../containers/VisibleCounter';
class App extends Component {
render() {
return (
<div>
<VisibleCounter />
</div>
)
}
}
export default App;
最后我们修改 entry.js
将 store
与我们的视图绑定起来。
entry.js
:
require('./styles/index.scss');
import React from "react";
import ReactDOM from 'react-dom';
import counter from './reducers/counter';
import { Provider } from 'react-redux';
import App from './components/App';
import { createStore } from 'redux';
const store = createStore(counter);
const rootEl = document.querySelector("#root");
function render() {
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootEl
)
}
render();
store.subscribe(render);
这里就用到了 Provider
方法,将 store
传递给 App
,然后用 ReactDOM
在页面上生成 react
组件。
沿着上一部分 Redux With React,在利用 redux
与 react
构建了单个视图的 WebApp,这部分介绍 redux
与 react-router
结合实现多个视图的 WebApp,代码也是在上一部分的基础上做修改。
react-router
提供了 react
的路由机制,除了这个库之外还有另外一个库 react-router-redux
用官方文档的话来说,首先 redux
可以和 react-router
两个一起使用就可以解决路由的问题,但是如果考虑到一些辅助的功能(例如和 Redux DevTool 等一起使用),就很有必要使用 react-router-redux
了。而且其实两者的结合非常的简单,我这里就先把它加上了。
$ npm install --save react-router react-router-redux
在不改变之前单个 VisibleCounter
的视图的前提下引入 router
。首先看一下代码的目录结构。
.
├── actions
│ └── index.js
├── components
│ ├── App.js
│ └── Counter.js
├── containers
│ └── VisibleCounter.js ++
├── dist
│ ├── bundle.js
│ ├── index.html
│ └── styles.css
├── entry.js ++
├── package.json
├── reducers
│ ├── counter.js
│ └── index.js ++
├── routes.js ++
├── styles
│ ├── index.scss
│ └── theme.scss
└── webpack.config.js
其中新添加或者是有修改的文件都用 ++
做了标记,可以看到作为 components(presentational) 在引入其他的组件的时候并没有收到影响,而 containers 则会因为 store
的变化而变化。
为了支持 router
需要做这么几件事情:
用 react-router
提供的 Route
标签声明路由结构
这里就是一个路由 /
对应组件 App
,而 App
里面包含一个 VisibleCounter
routes.js
:
import React from 'react';
import { Route } from 'react-router';
import App from './components/App';
export default (
<Route path="/" component={App}></Route>
)
在原有的 reducer
中添加 react-router-redux
所提供的路由的 reducer
,这里通过 redux
所提供的 combineReducers
实现。
reducers/index.js
:
import counter from './counter';
import { routerReducer as routing } from 'react-router-redux';
import { combineReducers } from 'redux';
export default combineReducers({
counter,
routing
});
由于 reducer
做了调整,那么在 VisibleCounter
与 store
链接时也会有改变,在下面的代码中我用 ++
标识修改的部分
containers/VisibleCounter.js
:
import { connect } from 'react-redux';
import Counter from "../components/Counter";
import { increment, decrement } from '../actions/index';
function mapStatToProps(state) {
return {
value: state.counter //++
}
}
function mapDispatchToProps(dispatch) {
return {
onIncrement: () => {
dispatch(increment())
},
onDecrement: () => {
dispatch(decrement())
}
}
}
const VisibleCounter = connect(
mapStatToProps,
mapDispatchToProps
)(Counter);
export default VisibleCounter;
修改入口,用 react-router
所提供的 <Router>
标签包装整个应用,并以属性的方式传递路由结构(routes
)和所需要的浏览器历史(history
)支持(hash
或者是 browser
)
entry.js
:
require('./styles/index.scss');
import React from "react";
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { hashHistory, Router } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import { createStore } from 'redux';
import reducer from './reducers/index';
import routes from './routes';
const store = createStore(reducer);
const history = syncHistoryWithStore(hashHistory, store);
const rootEl = document.querySelector("#root");
function render() {
ReactDOM.render(
<Provider store={store}>
<Router routes={routes} history={history}/>
</Provider>,
rootEl
)
}
render();
store.subscribe(render);
现在添加一个新的视图 About
containers/About.js
:
import React, { Component, PropTypes } from 'react';
class About extends Component {
render() {
return (
<p>
This is about page.
</p>
)
}
}
export default About;
拆分 App
和 VisibleCounter
使得 App
作为支持所有子视图的框架
containres/App.js
:
import React, { Component } from 'react';
class App extends Component {
render() {
const { children } = this.props.children;
return (
<div>
{ children }
</div>
)
}
}
export default App;
更新路由,添加 /
下的两个子路由 counter
和 about
routes.js
:
import React from 'react';
import { Route, IndexRoute } from 'react-router';
import App from './components/App';
import VisibleCounter from './containers/VisibleCounter';
import About from './components/About';
export default (
<Route path="/" component={App}>
<IndexRoute component={VisibleCounter}/>
<Route path="/counter" component={VisibleCounter}/>
<Route path="/about" component={About}/>
</Route>
)
其中 IndexRoute
表明 VisibleCounter
为在 /
时的默认路由。
在执行 npm start
之后看看 http://localhost:8080/
是不是计数器?http://localhost:8080/#/about
是不是我们的 About
页面?
这里讲解了基本的路由的构建,当然路由还不仅仅有这些内容啦,还有 <Link>
pushState
等等内容,其中 <Link>
的内容可以到 https://github.com/reactjs/react-router-tutorial
来看,然后其他部分在以后遇到的时候再解释。