Web开发模式-如何流水线化

Web开发之殇

做Web做久了,才发现,Web真的没有规范化好,不同的语言不同的社区都有自己的一套方案。并且,这些方案真的不算是简洁. 

Java 通常都是使用SSH架构,M层是hibernate,各种纹身了的Pojo. Spring这个粘合剂呀为了省却参数传递神马的,各种依赖注入让人云里雾里.C层要传一坨坨参数给Service层的各个函数有木有…. 虽说是分层,可是当真正写代码的时候,那些应该放在controller层,那些放在Service层,那些放在DAO层,Service层应该怎么怎么组织,写几个类等等,并没有明确的一个标准很规范,让人纠结。

那么,Web能不能像造一辆汽车一样,有一个标准的流程呢。保证高效,简洁?如何用真正面向对象的方式去处理?

有。当然有。不然我也不会在这叽歪了!

所谓道法自然,所谓面向对象,其实是人们希望去模拟自然界的方式去开发和理解程序。同样,各种神奇的算法,比如蚁群算法,神经算法等,都是人们学习现实的结果。所以,很多程序流程,我们都是可以去模拟现实中的一些案例的。
那么 Web开发应该模拟什么呢?蜜蜂?蚂蚁?还是神经元?

答案是:流水线

好,我们现在开始讲讲理论,你只要有个印象就好。后面我会举个实际项目中的例子,如何有效的利用这种理念去规范和提高你的开发效率,关键是,优化你的代码组织.

通常对于一个Web请求,用户输入的是相关定制参数,我们的代码则是生产线,目标很简单,就是生产出用户定制的产品。

这段话我们抽取出几个要素:

  1. 我们的目标是一个产品,对吗?产品是我们整个流水线的核心,所有的工作都是围绕着它.我们最终只是返回给用户一个产品,或者说一个套装(json格式,html页面等,记住,这些只是包转,或是一种运输格式)。用户提交的定制化参数,应该是这个产品的一个属性。

  2. 我们会有很多流水线,每条流水线每次只能生产出一个产品,并且返回给用户。如果你想同一时刻服务更多用户,那么你需要添加流水线,如果你想削减成本,你可以减少一条流水线生产一个产品的时间。

  3. 流水线上会有很多工人。

  4. 每个工人只会完成某一个方面的职责。

  5. 流水线系统会按顺序将产品送到工人面前,说:“嘿 你现在可以开始加工了”。这是流水线和工人的唯一交流。

  6. 工人知道产品的一切细节,他知道自己需要给这个产品安装那些螺丝。他不需要流水线告诉自己:“嘿,我给你个螺丝,你把螺丝装在那个地方”。

怎么和Web开发相关联呢?下面的六条对应上面的六条。

  1. 一个Web请求就是返回给用户一个对象(产品)(嘿 你也许会说: 我返回的是一个集合,但是,这也是一个对象,对吗?),所有的工作都是围绕着这个对象.我们最终只是返回给用户一个对象,或者说一个套装(json格式,html页面等,记住,这些只是包转,或是一种运输格式)。用户提交的定制化参数,应该是这个对象的一个属性。

  2. 我们会有很多请求,每个Web线程(流水线)只能返回一个结果。如果你想同一时刻服务更多用户,那么你需要添加更多的线程,如果你想削减成本,你需要提高每个线程的处理能力。

  3. 处理用户的请求过程中,会有很多对象(工人),他们会根据用户提交的参数,根据需要定制对象返回给用户。

  4. 每个Service对象只会完成某一个方面的职责。

  5. 系统会按顺序调用Service对象的某个固定方法,系统不需要传递任何参数给Service,Service知道当前只有一个对象需要处理,所以他可以直接拿到那个对象。

  6. Service 知道需要处理的对象的一切细节,他知道自己需要填充该对象的哪个属性,那些属性已经被处理了。那些属性是自己可以用的。

说了一通,打了一通的比喻,其实就是说,用户传来数据,我们根据这些数据创建一个基本的对象A,接着我们创建一个流水线对象,在这个对象上我们注册很多工人。之后该对象for循环,一个一个通知工人,因为整个过程中,工人只需要对对象A 进行处理,所以完全免去了返回值和参数的传递。最终将对象A打包传递给用户。
用代码来看应该是这样的:

```

Product a = new Product(parmsFromUser);//根据用户的参数,创建一个基础的产品
Thread.currentThread.set(a);//其实这里应该是用ThreadLocal来实现。将产品放进流水线
List workerlines = new ArrayList();//新建产品线
workerlines.register(new WorkerA());//注册工人
workerlines.register(new WorkerB());//注册工人
for(Worker worker: workerlines){
worker.work();//产品线通知工人可以工作了。
}


```

其中 workder 中work方法的细节可能是这个样子的:

```

public class WorkerA implements Worker
{
public void work(){
Product a = Thread.currentThread.get();//获取待加工的产品
int feature1 = a.feature1;
int feature2 = a.feature2;
a.result = doSomething(feature1,feature2);
}
}


```

总体来说,流水线的结构就是像上面这个样子。代码依然有点不好看,对吗?看看,又是什么设计规范,似乎又多了许多代码,你会觉得真的没必要,

当然,这些其实是框架做掉的,实际环境中,它会更好看点。你真正需要写的是Product 和Worker 两个类(Worker 你也当然可以写多个,看你的业务复杂度) ,

系统会自动让Worker 加工Product.

以这两天的 标签系统 一个 查询功能 为例子,来谈谈我如何利用流水线原理组织,规划代码。

其实以前我在一个搜索系统也是使用流水线原理组织每一个Action.非常自然,代码组织非常干净。

首先会涉及到几个概念:

  1. 标签
  2. 标签关联的文章
  3. 同义词:每个标签会有同义的标签。比如winxp 和windows xp是同义的。
  4. 频道: 每个频道会包含多个标签,频道由这些标签聚合出来的文章组成。
  5. 屏蔽标签: 每个频道会包含多个不允许出现的标签
  6. bla bla…当然还有其他的

我们再看下对系统的输入和输出。

频道ids => [标签系统] => 关联文章列表

标签系统的处理流程:

  1. 通过频道ids,拿到频道下关联的tags
  2. 每个传入的tag会有对应的同义词,找到他们,重新得到一个标签列表。
  3. 找到这些频道不允许出现的标签
  4. 根据这些数据,获取相关的文本列表,并进行排序

根据前面的流水线原理,当系统刚拿到频道ids的时候,就应该组装成一个产品。之后整条生产线都应该是对这个产品进行加工,和其他东西没有任何联系。

假设我们的产品对象设计是这个样子的:

```

/**
* User: WilliamZhu
* Date: 12-5-19
* Time: 下午4:34
*/
public class ContextTags {
public Set tags_ids;//标签列表
public Set blocked_tags_ids;//被屏蔽的标签列表
public String type;//聚合出来文章的类型
public Set channel_ids;//只有特定分类下的文章才需要聚合
public String[] order_fields;//排序字段
public String[] order_fields_desc_asc;//升序或者降序排


public Set<Integer> aggregation_ids;//频道ids,用户传入的

public int start = 0;//分页参数
public int size = 15;//同上

public long total = 0;//查询到的结果总数
public List<Map> results = new ArrayList<Map>();//返回给用户的结果集

//接下来是各种神奇的方法



}


```
包括频道ids(aggregation_ids),分页(start,size),最终成型的结果(results)等都已经包含在这个对象。每个工人(工序)都可以从该对象拿到原料或者之前工人加工好的东西,并且将得到结果放进这个原始的对象中。

当用户发给我们原材料或者定制参数的时候,我们会这样构建初始的产品(下面几行代码就是Controller(Actioin)层所有的代码了)

```

    ContextTags contextTags = registerProduct(ContextTags.class,request);//将产品放进线程,同时将request的参数注入ContextTags对象中。

     try{
            ServiceSystem.loadAndRunService(TagsRelationshipSearchService.class);//加载服务类,并运行
        }catch (Exception e){
           channel.content(format(FAIL, ServiceExceptionHandler.handle(e))) ;
           return;
        }



```

上面会通过registerProduct方法创建一个包含用户传来参数的产品对象。我们看到,后面的service的运行并不需要显式传递刚刚注册的ContextTags对象,因为向Service注册自己的工人可以直接通过一个静态方法拿到(Threadlocal实现)需要的产品。基本上整个 Controller层都是这种模式。注册一个产品,注册一个Service,就Ok了。

我们看看Service 是什么样子的:

```

public class SearchService extends BaseService {

    @Override
    public BaseService register() {
        register(new TagParser());
        register(new TagRelationSearch());
        return this;
    }
}


```

Service也非常简单,将你需要的工人注册到产品线上就行。上面就是整个Service层所有的代码了。这就是上文提到的,所有工作都被系统做掉了,

你只要写个产品,注册下,写个工人,注册下,然后就Ok了。

当然,真正的业务逻辑在工人身上。这个代码会多点。仍然需要强调的是:整个系统我们真正构建的其实就两个对象:产品,工人。

在Controller层,我们一开始就定义了ContextTags,并且注册给了系统。接着我们我们会定义两个工人类,并且将他们也注册给系统,

系统会自动调用工人给产品进行加工。我们真正需要编程的地方也是产品和工人。

产品ContextTags更像一个简单的Pojo类。类似Struts2里面的model。但是我它还包括返回给用户的结果。也就是他完整的自我包含了原始数据和结果数据。

好 我们现在讨论下怎么具体构建工人对象,这也是代码比较多的地方。但是已经有了自己的模式,你只是去填充代码而已。


标签系统的核心功能是通过标签获列表取关联的文章。这里需要一个工人B。

但是用户给我们的原材料是频道ids,所以我们需要一个工人把这个频道ids转化为标签的ids.这其中还包括找出每个标签的同义词,找出需要被屏蔽的词汇。所以这也里也需要一个工人A。

我们看看工人A是长什么样子的:

```

class TagParser implements Worker{
private ContextTags contextTags=Context.current().tags();


@Overritten
public void work() {
    return this.tag_channel_expand().tag_synonyms_expand().tag_blocked_expand();
}

private TagParser tag_channel_expand() {

    //.….

    Map channel_group = MysqlManager.single_query(channel_groups_sql);
    contextTags.type = getString(channel_group, "type");
    contextTags.channel_ids = hashSet(getString(channel_group, "channel_ids").split(","));

    return this;

}

private TagParser tag_synonyms_expand() {

   //....

    contextTags.tags_ids.addAll(query(synonyms_sql));

    return this;

}

private TagParser tag_blocked_expand() {

    //.…..

    contextTags.blocked_tags_ids.addAll(query(tag_blocked_sql, _s_tags.type));

    return this;
}



//还有各种神奇的方法
}


```

工人A做了三件事情。
tag_channel_expand()
通过频道ids拿到需要的标签ids
tag_synonyms_expand()
通过标签ids拿到所有的同义词
tag_blocked_expand();
最后拿到所有需要屏蔽的标签,并且填充到产品(ContextTags)中.

这就是A的所有工作。接着B会继续对产品进行进一步加工:

```

class TagRelationSearch implements Worker{{
private ContextTags contextTags =Context.current().tags();


@Overitten
public void work() {

    //.….
    contextTags.total = getLong(MysqlManager.single_query(count_sql), "total");

    List<Map> tag_relations = MysqlManager.query(search_sql, contextTags.start, contextTags.size);
    contextTags.results.addAll(tag_relations);

}



}


```

B的工作很简单,根据A的加工结果,获取原料,最后找到所有需要的数据,填充到产品中(ContextTags).

最后产品线自动将产品包装为json数据发送给用户。

上面两个类是你真正需要关心的业务逻辑。将取得的数据填充如产品。其他就不用管了,系统会做掉其他事情。


最后当需要返回结果给用户的时候:

```

JSONObject result = result(contextTags.results);
result.put("total",contextTags.total);
channel.content(result.toString());


```

怎么样。怎个流程非常清晰吧。 

上面因为是分步讲解的,你可能无法串联起来。我们现在再理一理。对于任何一个Http请求,你只要做两件事情。

一 创建一个 产品 类,譬如例子中的ContextTags.它包含用户需要传入的数据的字段还有结果集字段。

二  根据业务需求,定义工人类,比如例子中的 TagPaser和 TagRelationSearch. 分别解析tag和查找tag关联的文章数据。

     工人之间其实是一个链状关系,首先经过A处理,接着B处理,他们之间本身没有任何耦合,也不需要接触什么Service层,Controller层等。只要填充产品就可以了。

这其中还会遇到一个问题,就是工人发现产品存在瑕疵,比如某个需要用户定义的规格用户没有传递,或者传递出错,比如需要id字段,但用户却忽略了它。这个怎么处理呢?

这个过程工人应该通过异常机制终端处理流程。这个机制以后有机会再详解。


因此 流水线模式 很容易标准化,定义好产品,其他人只要去实现工人就好,工人就是填充产品对象里面的字段。即使同一个Action也可以让多个人完成而不会有任何冲突。

因为整个规范都已经在产品对象里面了。


所以,套着这个步骤做,基本就8-9不离十了。而且能获得我之前说的一切优点。其中,工人其实是可以复用的。当然,最好是一个工人只懂得一个产品。
不知道大家对这个模式的看法?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值