Eisen's Blog

© 2024. All rights reserved.

DDD Repository

2016 May-17

标题是 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 会自动的讲所有的依赖注入到这个对象里。

说白了就是将数据层和模型绑定在一起,持久层做了业务层的事情。

希望的样子

然而我希望的是可以将业务层做成这个样子:

  1. 没有对什么持久层的依赖,甚至完全不知晓持久层。
  2. 领域模型不应该是接口而已,接口不能描述具体的业务行为,我同意接口和实现分离的方式,但是分离的实现也应该是领域模型重要的一部分而不是和持久层放在一起
  3. 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 保证了内部的逻辑都是包含在业务层的。


Redux with react

2016 May-12

沿着上一部分 Get Started Redux,在利用 redux 构建了 store reducer 之后,现在需要给应用提供个 view 了。我们用当下最流行的 react。

prepare package for react with redux

安装 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 了。

Presentational and Container Components

在 redux 的官方文档 Use With React 解释了一种 redux 和 react 结合的模式:将 react 的组件分为两种,一种是不与 redux 交互的 presentational 组件,它是一种通用的组件,任何提供给它所需要的 func 或者是 prop 的框架都可以使用它。另一种是 container 组件,是和 redux 的 action 以及 store 绑定的组件。通常都是会先写一个 presentational 组件,然后再创建一个 container 组件对 presentational 组件进行包装后使用。后面的 full example 展示了 Counter 与 Visible Counter 两个不同类型的组件。当然在官方也提供了非常好的例子。

full example

首先展示一下目录结构

.
├── 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 官方建议一定要对每一个组件都明确的定义这样的参数说明,它也起到了文档的作用,方便与其他人协作。这里我们需要三个东西:

  1. value 当前的计数
  2. onIncrement 当点击 + 时的动作
  3. 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 组件。

参考

  1. redux
  2. redux counter example
  3. use with react
  4. babelrc

Redux with react router

2016 May-12

沿着上一部分 Redux With React,在利用 redux 与 react 构建了单个视图的 WebApp,这部分介绍 redux 与 react-router 结合实现多个视图的 WebApp,代码也是在上一部分的基础上做修改。

prepare package for react-router

react-router 提供了 react 的路由机制,除了这个库之外还有另外一个库 react-router-redux 用官方文档的话来说,首先 redux 可以和 react-router 两个一起使用就可以解决路由的问题,但是如果考虑到一些辅助的功能(例如和 Redux DevTool 等一起使用),就很有必要使用 react-router-redux 了。而且其实两者的结合非常的简单,我这里就先把它加上了。

$ npm install --save react-router react-router-redux

更新代码支持 Router

在不改变之前单个 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 需要做这么几件事情:

  1. 用 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>
     )
  2. 在原有的 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;
  3. 修改入口,用 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 来看,然后其他部分在以后遇到的时候再解释。

参考

  1. react-router-redux
  2. react-router
  3. react-router-tutorial