百年阿里

百年阿里


紧张&期待

很明白自己不太善于沟通,所以对这次百阿之旅带着紧张,对于较快融入团队并不是很有信心。但是也很期待,期待够能突破自己,发现自己新的可能,简单来说变得更 Open ,认识一些小伙伴。令人欣喜的是,这份期待没有被辜负~

缘分

缘分真的是一个很神奇的东西,一群来自不同城市、不同部门、不同年龄的人组成了这么温暖、团结、活力的 1227 百阿班。很幸运来到 1227 ,很幸运来到 三生万物 ,很幸运遇到你们,遇见新的自己 ~

感谢百阿让我遇到这么一群,有趣的、热情的、可爱的、灵动的、温情脉脉的人。

破冰

一圈自我介绍后,其实我一个人名都没记住,虽然当时每个人自我介绍完后我都会重复好几遍,重复着重复着就忘了~原谅我的烂记性不是不想记住你们!!!不过最后认人名 PK 环节,我可是能叫出全班人的名字的人,得瑟一下。

印象比较深刻的是五个小游戏任务,大家在互相还不熟悉的情况下能够做到相互信任、团结一致、互相帮助,用智慧和合作共同创造价值,取得成果。很多环节很辛苦也充满了困难,但我们在短时间内形成的超强凝聚力和执行力,让团队势如破竹,朝着目标冲刺。在这个过程中出现了很多小彩蛋,在 “达芬奇密码” 环节大家看完规则后,你一句我一句着的说着游戏规则,我还没听懂规则是啥,天相突然跳出来说了一个听起来很牛逼的调度算法,目瞪狗呆。又是一个算法怪兽,规则看完调度算法就出来了,感觉智商受到了降维打击…..

由于算法对于调度者依赖性太高,对调度内存是一个很大的考验,为了保险起见我们采用了两趟遍历,虽然中间出了小插曲,导致牌序混乱。大家都一脸懵逼的时候,天相挺身而出力挽狂澜,终于体会到什么叫做 “此时此刻,非我莫属!”

游戏任务
十只脚落地
安全部门合影

凝聚

经过第一天的破冰,第二天大家慢慢开始变得熟悉起来,尤其是在这次的土话的探寻中,团队更加凝聚。第一次见到阿里土话是在公司的大屏上:“此时此刻,非我莫属” 这是我记得最清楚的一句,也是最感动我的一句。

因为信任,所以简单。整个探寻任务从分工到执行只在很短的时间,没有过多的担心某个环节出问题了怎么办?某些子任务完不成怎么办?我们相信大家都能够把自己的那一部分做的很精彩,都很自觉的专注于自己的部分,相信伙伴!

一点多才开始剪视频,又感受了一把速度与激情,有一种五点前赶发布的感觉。既要冷静又要快速响应,在大家的共同努力下,开讲前视频渲染结束。

晚上一起去庆功呀~ 5号食堂!

结束后,弦柱在群里说了一句,“今天聊的很开心,好像又回到了学生时代!”对的!就是这种感觉,好放松~

变化

昨天还在一起团队任务,马上就要因为另外的任务分开。“拥抱变化” ,变化来的如此之快。在弦柱的带领下我们完成了对飞猪业务的探寻,让我重新认识了飞猪,她的历史,现状,困难与机遇。对于从来没接触过的 OTA 也有了初步的了解。

在这个团队最大的感受就是有活力,因为我们的队长 — 弦柱(未来的杭州市旅游局局长) 就是一个很有活力的人!对团队也非常认真负责,带着我们去飞猪部门探索,采访!准备演讲到凌晨两点多,辛苦啦~

夜谈

对这个环节印象也非常深刻,每一个人的分享都好像是一本十分值的回味的书,透过他们的讲述读到不同的精彩故事。通过他们的精彩看到自身的不足,通过他们的失落看到逆境中的坚韧,每个人的经历都很有意思,可惜时间很短不然肯定整整一晚都在谈心交流。

不得不说,十分佩服泰伯,一个敢闯、敢拼、敢探索。知道自己想要什么,追求的是什么,并且付诸于行动,很少人能有这种想法和魄力。放一张大佬的照片,像不像香港扛把子 , haha~

公益

公益真的是一件帮助他人幸福自己的事情,在这个过程中能认识到自己的价值,也能为他人创造价值。公益能够感染身边的人,让更多的人以一颗柔软的心来对待这个世界。有很多同学反映在做公益的过程受到许多人的帮助,就像马老师所说的要时刻保持爱,对社会、对自然、对身边的人,爱的力量小而强大。做一个内心温暖而强大的人。

公益后的 “总裁局” 也是让人十分放松和开心,好久没和这么多人一起开开心心吃吃喝喝玩玩了。

阿里

这就是阿里,以前我们会说:“这是阿里”,但是现在我们会告诉别人:“这就是阿里!”。因为现在的我们是一个不追求大,不追求强,我们追求成为一家活102年,截止到2036年,服务20亿消费者,创造1亿就业机会,帮助1000万家中小企业盈利的好公司。

对于新六脉神剑价值观,并不是为了改变我们的想法,或是束缚我们,而是在这个共识下选择同路人。价值观并非口号。可以看到不论是少年阿里还是成年后的他一直都在用实际行动诠释着价值观里的每一条。

另外对于年轻人,保持自己的好恶和棱角很重要,每个人都拥有对机会的选择权!

做一个守则的阿里人,在自身利益和公司里冲突的时候以公司利益优先,保证公司数据安全,不要触碰公司红线。

活着

来到阿里的第一年最重要的是 “活下来” ,因为作为一个新人很难融入到节奏走么快,业务难度这么高,新人培训少的团体。我们最重要的不是说想着如何实现集团的大目标,保证自己能够活下来,landing — 技术落地,业务落地。要明白自己生存在一个什么样的环境,去了解这个环境,适应这个环境。

而三到五年的沉淀,才能真正成为一个真正的阿里人,这个时候就需要面对更过的变化和更快的节奏,多从自身去思考问题。

伙伴

感谢在这次遇到的优秀的小伙伴们,因为我相信 无论我遇见谁,他/她都是我生命该出现的人,绝非偶然,他/她一定会教会你一些什么。

属七:感谢我的小天使,看得出来你是一个温暖的、热爱生活的人。从礼物的包装上,看得出你花了不少心思,真的非常感动。尤其是你的赠言充满了鼓励与期待,谢谢!你的字写的真的很棒~

弦柱(局长):阳光、帅气、自信、具有少年感。跟他待在一块感觉自己又回到了大学校园,轻松愉悦!

余征:认真、严肃、细致。短短几个月就减掉 1/3 的体重的毅力,令人钦佩。

泰伯:敢闯、敢拼、有想法。看起来是一个很严肃的大佬,其实是很好相处的大佬!感谢鼓励陪伴!

拾月:博学、美丽、有气质。第一眼就被气场震撼到了,除了气质还有博学多识!

星吟:自律、美丽、脑洞大。每次不鸣则已,一鸣惊人,一句吐槽能让我笑一两分钟,苹果肌要坏掉了~

珺兮:聪明、文静、可爱。都是应届生相似点还蛮多的,也能说到一块去。可爱!有趣!

天相:自信、认真、有责任、有主见。令我印象深刻的就是 不使用一次性杯子盛可乐,因为不环保!!!

迪奇:聪明、好奇心、冷幽默。我们的数据中台,反应快,技术强,感觉内心也很萌哈哈,我们技术人的楷模。

久谙:幽默、有趣、情商高。思维太活跃了,想象力也超级棒,一句话带动全场气氛!

山魏:认真、戏精、沉稳、可靠。因为角色投入一炮而红,红遍大江南北!但是也非常的可靠,做事认真。

顺序不分先后 ,按照 “总裁饭局” 席位,希望下次再相见,我们还能按照这个顺序坐在一起,聊天喝酒!

优秀的人大多会玩儿,幽默,会来事儿,正是因为这些特质,能够吸引并凝聚一波人,作为一个不太说话的程序员,应该培养锻炼自己让自己变的活跃,open!这次百阿是一个良好的开端。感谢认识你们,真的很开心~

oh-my-zsh主题体验

以前一直使用的 oh-my-zsh 的默认主题,这是因为一直找不到一款钟爱的主题,powerline 一开始看起来比较有科技感其实也不是很耐看,而且配置很麻烦啊。

后来想着干脆直接用 随机主题 好了,你永远不知道下次他会给你什么惊喜。

下面是在使用随机主题的时候看到的一些比较漂亮的一些主题,整理出来最后直接保留这些主题,那么再怎么 random 都是我喜欢的主题啦~

pygmalion-virtualenv.zsh-theme

RemoveNthNodeEndOfList-19

Given a linked list, remove the n-th node from the end of list and return its head.

Example:

1
2
3
Given linked list: 1->2->3->4->5, and n = 2.

After removing the second node from the end, the linked list becomes 1->2->3->5.

Note:

Given n will always be valid.

Follow up:

Could you do this in one pass?

题目要求就是通过一次遍历删除倒数第 n 个元素。

思路

  1. 一开始想着就是 链表长度 - n 这样遍历到这个位置把这个节点跳过就行了。可是这样的话需要遍历链表两次。
  2. 那么我们要想办法一边遍历一边跳过节点,这个时候想到了数组中删除某个节点使用了 p,q 指针,那么这里是否可以呢?
  3. 确实,我们 pq 指针开始指示的位置不同,q指针指向 n 节点后的位置,这样只要 q 指针到了链表的结尾我们就能找到 需要删除的节点就是 p 的位置。

优化

经过上面的一些想法基本形成了解题思路但是在实现的时候会有几个特例这个算法过不了比如 [1] 1 [1,2] 2 ,一开始我还尝试使用了一些方案来修补代码,但是这样总觉得不是很好,算法没有比较好的适用性。然后我想到这种状态主要是head指针一开始指向一个空节点那么删除head元素的时候就不会出现各种问题了。所以有下面的代码。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// add null head
ListNode nullNode = new ListNode(0);
nullNode.next = head;
head = nullNode;

ListNode first = head, second = head;
for (int i = 0; i < n; i++) {
second = second.next;
}
while (second != null && second.next != null) {
first = first.next;
second = second.next;
}
first.next = first.next.next;
return head.next;
}
}

结果

Runtime: 0 ms, faster than 100.00% of Java online submissions for Remove Nth Node From End of List.

Memory Usage: 35 MB, less than 100.00% of Java online submissions for Remove Nth Node From End of List.

设计模式 - 装饰者模式

装饰者模式

本文转载自 屈定’s Blog

装饰者模式实际上是一直提倡的组合代替继承的实践方式,个人认为要理解装饰者模式首先需要理解为什么需要组合代替继承,继承又是为什么让人深恶痛绝.

为什么建议使用组合代替继承?

面向对象的特性有继承与封装,但两者却又有一点矛盾,继承意味子类依赖了父类中的实现,一旦父类中改变实现则会对子类造成影响,这是打破了封装性的一种表现.
而组合就是巧用封装性来实现继承功能的代码复用.
举一个Effective Java中的案例,当前需求是为HashSet提供一个计数,要求统计它创建以来曾经添加了多少个元素,那么可以写出下面的代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class InstrumentedHashSet <E> extends HashSet<E> {

private int addCount = 0;

@Override
public boolean add(E e) {
this.addCount++;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> c) {
this.addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return this.addCount;
}
}

下面测试代码会抛出异常,正确结果是6,是不是匪夷所思,这种匪夷所思需要你去看HashSet的具体实现,其addAll实际上是调用了add方法.

1
2
3
InstrumentedHashSet<String> hashSet = new InstrumentedHashSet<>();
hashSet.addAll(Arrays.asList("张三", "李四", "王二"));
Assert.assertEquals(hashSet.getAddCount(), 3);

这个案例说明了继承导致子类变得很脆弱,其不知道父类的细节,但是却实实在在的依赖了父类的实现.出现了问题也很难找出bug.本质原因是HashSet并不是专门为继承所设计的类,因此强行继承那会出现意想不到的问题.有关什么时候该用继承在设计模式–模板方法模式的思考一文章有相关讨论,感兴趣的可以去看看.

回到正题那么换成组合模式,让InstrumentedHashSet持有HashSet的私有实例,add以及addAll方法由HashSet的私有实例代理执行.这就是组合所带来的优势,充分利用其它类的特点,降低耦合度,我只需要你已完成的功能,相比继承而并不受到你内部实现的制约.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class InstrumentedHashSet <E>{

private int addCount = 0;

private HashSet<E> hashSet = new HashSet<>();

public boolean add(E e) {
this.addCount++;
return hashSet.add(e);
}

public boolean addAll(Collection<? extends E> c) {
this.addCount += c.size();
return hashSet.addAll(c);
}

public int getAddCount() {
return this.addCount;
}
}

装饰者模式

装饰者模式定义为:动态的给一对象添加一些额外的职责,对该对象进行功能性的增强.(只是增强,并没有改变使用原对象的意图)
装饰器模式类图:
img
以上是标准的装饰器模式,其中AbstractDecorator为一个装饰器模板,目的是为了提高代码复用,简化具体装饰器子类的实现成本,当然不需要的话也是可以省略的,其最主要的功能是持有了ComponentInterface这个被装饰者对象,然后子类可以利用类似AOP环绕通知形式来在被装饰类执行sayHello()前后执行自己的逻辑.这是装饰者模式的本质.

比如ContreteDecoratorA增强了sayHello()

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ContreteDecoratorA extends AbstractDecorator {

public ContreteDecoratorA(ComponentInterface componentInterface) {
super(componentInterface);
}

@Override
public void sayHello() {
System.out.println("A start");
super.sayHello();
System.out.println("A end");
}
}

具体使用方式

1
2
3
4
public static void main(String[] args) {
final ContreteDecoratorA decoratorA = new ContreteDecoratorA(new ComponentInterfaceImpl());
decoratorA.sayHello();
}

输出

1
2
3
A start
hello world
A end

其中默认实现ComponentInterfaceImpl的sayHello()功能被装饰后增强.

Java I/O与装饰者

字节流

Java I/O框架就是一个很好的装饰者模式的实例.如下InputStream关系图
img
其中FileInputStream,ObjectInputStream等直接实现类提供了最基本字节流读取功能.
FilterInputStream作为装饰者,其内部引用了另一个InputStream(实际被装饰的对象),然后以AOP环绕通知的形式来进行功能增强,笔者认为这里应该把该类定义为abstract更为合适.其承担的角色只是代码复用,帮助具体的装饰者类更加容易的实现功能增强.
img
具体的装饰者BufferedInputStream为其他字节流提供了缓冲输入的支持.DataInputStream则提供了直接解析Java原始数据流的功能.

由于装饰者模式的存在,原本一个字节一个字节读的FileInputStream只需要嵌套一层BufferedInputStream即可支持缓冲输入,

1
BufferedInputStream br = new BufferedInputStream(new FileInputStream(new File("path")));

字符流

相比较字节流,字符流这边的关系则有点混乱,主要集中在BufferedReaderFilterReader,其两个角色都是装饰者,而FilterReader是更加基本的装饰者其相对于字节流中的FilterInputStream已经升级为abstract了,目的就是便于具体装饰者实现类更加容易的编写.那么为什么BufferedReader不继承FilterReader呢?这个问题暂时不知道答案,有兴趣的可以关注下知乎,等大牛回答.
为什么BufferedReader 不是 FilterReader的子类,而直接是Reader的子类?

不过从另一个角度来说,设计模式并不是套用模板,其最主要的是思想,对于装饰者模式最重要的是利用组合代替了继承,原有逻辑交给内部引用的类来实现,而自己只做增强功能,只要符合这一思想都可以称之为装饰者模式.
img

Mybatis与装饰者

Mybatis中有不少利用到装饰者模式,比如二级缓存Cache,另外其Executor也正在朝着装饰者模式改变.这里以Cache接口为主,类图如下:
img
从类图来看和装饰者模式似乎无半毛钱关系,实际上其省略了AbstractDecorator这一公共的装饰者基类.那么要实现装饰者其实现类中必须有一个Cache的被装饰对象,以LruCache为例.

1
2
3
4
5
6
7
8
9
10
11
12
public class LruCache implements Cache {

private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;

@Override
public String getId() {
return delegate.getId();
}
....
}

其内部拥有Cache delegate这一被装饰者,也就是无论什么Cache,只要套上了LruCache那么就有了LRU这一特性.
org.apache.ibatis.mapping.CacheBuilder#setStandardDecorators构造时则根据配置参数来决定增强哪些功能,下面代码则很好的体现了装饰者模式的优势,还望好好体会.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
cache = new SerializedCache(cache);
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}

线程安全与装饰者

装饰者模式的功能是增强原有类,因此其经常被用来包装一个非线程安全的类,使其提供线程安全的访问,在JDK中的体现则是Collections.synchronizedXXX方法以及与其类似的一些方法。以synchronizedList为例,其本意是将线程不安全的List实例包装成线程安全的实例,包装方式是使用SynchronizedList提供同步包装,如下所示:对相关方法都使用独占锁来修饰,保证了并发访问的线程安全性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
final List<E> list;

SynchronizedList(List<E> list) {
super(list);
this.list = list;
}

public E get(int index) {
synchronized (mutex) {
return list.get(index);
}
}

public void add(int index, E element) {
synchronized (mutex) {
list.add(index, element);
}
}
....
}

函数式编程与装饰者

在函数式编程中因为函数是一等公民,因此互相嵌套是常有的事情,比如以下对于加锁解锁的一个函数封装调用

1
2
3
4
5
6
7
8
public static <T> T  lockTemplate(Lock lock, Supplier<T> supplier) {
lock.lock();
try {
return supplier.get();
} finally {
lock.unlock();
}
}

该函数接收一个锁以及一个supplier提供者,其使用方式也很简单,比如下面方式使得i++变得线程安全。

1
2
3
4
5
6
7
8
ReentrantLock lock = new ReentrantLock();

int[] boxInt = new int[1];

Integer value = LockTemplate.lockTemplate(lock, () -> {
// 线程不安全的操作
return boxInt[0]++;
});

由于Java是面向对象范式语言,对函数式编程支持的并不是很好,所以这个例子并不能很好的描述函数式编程,不过思想上来看这是一种装饰者模式的实践,只不过装饰者与被装饰都变成了函数,装饰者函数的功能也是对被装饰者功能的增强。

装饰者模式与桥接模式

这两个模式起初笔者很疑惑,两者的本质都是组合,并且从类图上来看两者几乎是一致的,那么他们的区别是什么呢?
我认为从继承树上来看装饰者模式的目的是纵向的扩展类(增加树的深度),从而为现有的实现类提供更强大的支援。
桥接模式则是水平扩展(增加树的宽度),以现有的类为代码复用的基础,然后在这个基础上水平扩展出另外的业务实现,这里更加注重的是解耦,把变化的与不变的分离开。

另外就是组合模式,基本看起来他的结构和装饰者模式一模一样,只是他们的用法是不一样的,两个都是继承一个,并且里面包含实例,但是装饰者时使用了实例的功能,而组合模式则是为了形成一个结构,比如Mybatis里面的SqlNode。

另外设计模式本身之间相互影响,没必要纠结于是某一种特定的模式,只要理解其背后的思想就可以了。

总结

装饰者模式本质上来说是AOP思想的一种实现方式,其持有被装饰者,因此可以控制被装饰者的行为从而达到了AOP的效果。

扩展

偶然看到一篇博文: 项目中用到的一个小工具类(字符过滤器),里面运用了装饰者设计模式,工厂模式,模板方法模式设计了这样一个符合开闭原则的工具类.感兴趣的也可以看看.

设计模式 - 责任链模式

责任链模式

本文转载自屈定’s Blog

标准责任链模式

责任链模式: 客户端发出的请求,客户端本身并不知道被哪一个对象处理,而直接扔给对象链,该请求在对象链中共享,由对象本身决定是否处理. 当请求被处理后该链终止.本质目的是把客户端请求与接收者解耦,但是解耦的太彻底了,只能让接收者一个个来看看是不是自己该处理的请求.
标准的责任链模式一个请求只被一个对象处理,一旦处理成功后则链终止,请求不再被继续传递.标准的责任链模式并不是很通用,这种一对一模式大多场景可以用策略模式来代替,只有在客户端并不清楚具体的执行者是哪个对象的时候,责任链才比较适合.
举个例子:你想在天朝办理一个证,但是你不知道去哪比较好,因此你的选择就是一条链路,先去A局,A局让你去B局,B局让你去C局等等,直到解决你的问题,当然也存在白跑一趟的结果.这也是标准责任链的缺点,产生了太多没必要的调用.标准的责任链实际上应用场景并不是很多,而常使用的是升级版的功能链.

功能链

功能链是责任链的演变,结构上并没有实质的变化,只是每一个节点都可以处理请求,处理完转向下一个,也就是每一个请求都经历全部的链.这种应用场景就比较多了,比如我要办一件事,先去A再去B最后去C,这个例子还有点说明ABC三者的关系,取决于构造链时的顺序,另外每一步没处理好可以自由的选择退出链.
文字说的不是很理解,下面举几个实际中的代码实例.

Java中Filter链

对于Filter,其是由FilterChain来进行链的组合调用,请求的request与返回response实际上是共享的上下文信息,每一个处理的Filter都可以查看与修改.

1
2
3
4
public interface FilterChain {
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException;
}

在Tomcat中实现类为org.apache.catalina.core.ApplicationFilterChain,其结构如下图:
img
其中数组filters就是所谓的filter链,利用pos(当前执行到的位置)与n(filter链长度)来进行链的调用.
那么怎么让链节点选择继续执行还是停止执行呢?答案的FilterdoFilter方法,该方法把责任链作为参数FilterChain一直传递下去,继续就调用chain的doFilter方法,不继续则不调用.

1
2
void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;

Spring Security中拦截链

Spring Security中的链实际上是由SecurityFilterChain接口所定义,其很简单的就是暴露出一个list的链.其中matches判断请求是否是这条链来处理.

1
2
3
4
5
6
public interface SecurityFilterChain {

boolean matches(HttpServletRequest request);

List<Filter> getFilters();
}

在其入口处类org.springframework.security.web.FilterChainProxy中包含着多条链.

1
private List<SecurityFilterChain> filterChains;

每一个链处理的是一类请求.Spring Security使用简单的for循环判断定位到具体执行的链.

1
2
3
4
5
6
7
8
private List<Filter> getFilters(HttpServletRequest request)  {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}

那么这个结构因为这个设计模式就很清晰了(这也是熟悉了设计模式之后的优势,看源码可以有一种全局把控感)
img

还有一个问题,链是如何自由执行的?
这一点与Java Filter一模一样,Spring Security实现了一个org.springframework.security.web.FilterChainProxy.VirtualFilterChain类,该类同样实现了FilterChain接口,里面的调用逻辑也与tomcat方式一致.具体就不讨论了.

另外Spring Security也提供了一种数据共享的方式,利用ThreadLocal保证线程安全,达到共享数据的目的.另外这里的SecurityContextHolderStrategy是策略模式的一种应用,值得一看.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
final class ThreadLocalSecurityContextHolderStrategy implements
SecurityContextHolderStrategy {

private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<SecurityContext>();

public void clearContext() {
contextHolder.remove();
}

public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();

if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}

return ctx;
}
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}

public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}

Mybatis中插件链

Mybatis中插件使用的是类似责任链的一种模式,当然也可以称之为责任链模式,毕竟思想都是类似的.其中插件是通过Interceptor接口实现的,其中plugin方法就是为目标对象套上该链的一个节点.

1
2
3
4
5
6
7
8
public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

Object plugin(Object target);

void setProperties(Properties properties);
}

那么如何构造这个链?在InterceptorChain中有如下方法,InterceptorChain是在构造配置时组装好的,燃后对目标使用pluginAll方法,构造完整链.

1
2
3
4
5
6
7
8
9
10
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
...
}

其中plugin官方推荐Plugin.wrap(target, this)方法,该方法本质上是用代理模式嵌套住目标类

1
2
3
4
5
6
7
8
9
10
11
12
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}

那么这种构造出的链大概如下面这种嵌套结构,这种链可以说是彻底的功能链,其一旦组装好就无法变化了.当然这种也适合Mybatis这种从配置中就定死了执行链.
img

业务开发中可以常用到的链

在业务开发中常常能遇到这类需求,比如退款操作,退款后可以恢复商品库存,恢复活动库存,退掉用户的优惠券,退掉用户的活动资格等等,该一系列的操作就是一条线性链,那么就可以利用责任链的思想来完成在这样的需求.
先提取出一个公共接口,链节点实现该接口,完成具体的退款操作

1
2
3
4
5
6
7
8
9
public interface RegainAfterRefundOrder {
/**
* 退回操作
* @param bo 该订单,可能是子订单,也可能是主订单,自行判断
* @param operator 操作人
* @return true成功
*/
boolean regain(BizOrderDO bo, Long operator);
}

接下来是链的统一管理,也就是需要Chain这个类来管理,可以按照下面的实现,其调用链只是简单的在applyAllPlugin循环调用,该过程可以按照Spring Security等方式实现更加灵活的调用.
可以根据需求设计为一旦创建就不可改变的类,包括类中的interceptors,这样使得代码更加健壮.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class RefundOrderAndRegainChain {

private final List<RegainAfterRefundOrder> interceptors = new ArrayList<>();

public void applyAllPlugin(BizOrderDO bo, Long operator) {
for (RegainAfterRefundOrder interceptor : interceptors) {
interceptor.regain(bo, operator);
}
}

public void addInterceptor(RegainAfterRefundOrder interceptor) {
interceptors.add(interceptor);
}

public List<RegainAfterRefundOrder> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}

最后是借助IOC实现链的组装,假设有RegainCoupon,RegainInventoryCount,RegainInvitationCodeWithDraw等RegainAfterRefundOrder的实现类,依次在Spring的Configuration类中实现注入,并构造出需要的RefundOrderAndRegainChain.最后再业务需要的地方直接注入该Chain即可.对于这种逻辑实现了解耦与灵活的组合.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class RefundOrderAndRegainConfig {

@Bean
public RefundOrderAndRegainChain paidToRefund(
RegainInventoryCount regainInventoryCount,
RegainCoupon regainCoupon,
RegainPromotionRegistered regainPromotionRegistered,
RegainInvitationCodeWithDraw regainInvitationCode) {
RefundOrderAndRegainChain chain = new RefundOrderAndRegainChain();
chain.addInterceptor(regainInventoryCount);
chain.addInterceptor(regainPromotionRegistered);
chain.addInterceptor(regainCoupon);
chain.addInterceptor(regainInvitationCode);
return chain;
}

}

责任链模式的本质

  1. 让请求者不关心具体接收者是谁,只需要得到自己的具体结果
  2. 在一个请求对应多个接收者情况下(Spring Security这种),接收者之间可以自由组合,灵活性很高
  3. 新增接收者处理也只需要增加链中的一个节点,不需要改动太多.

设计模式 - 策略设计模式

策略设计模式

本文转载自屈定’s Blog

策略模式是一种简单的设计模式,但是其在业务开发中是一种非常有用的设计模式.举个例子,当你的业务需要针对不同的场景(可以简单理解为枚举类),执行不同的策略时那么使用策略模式可以帮助你更好的写出低耦合与高可扩展的代码.


标准策略模式

策略模式: 把具体的算法从业务逻辑中分离出来,使得业务类不必膨胀,并且业务与具体算法分离,便于扩展新算法.类图如下:
img
使用策略模式往往策略上有着相似的输入参数以及输出结果,或者有一个公共的上下文,便于抽象出策略接口Strategy,然后对应的业务Service只需要引用StrategyContext填充具体的策略完成自己的需求.

1
new StrategyContext(new CouponStrategy()).sendPrize(uid, prize)

这是标准的策略模式,这种模式在如今的IOC下应用场景并不是很多,该模式有不少缺点

  1. 客户端必须知道所有的策略,然后手动选择具体的策略放入到Context中执行.
  2. 仍旧无法避免if/else逻辑,针对大多数场景下都是根据条件分支判断来选择具体的策略,那么在客户端耦合具体策略的情况下这个是没法避免的

策略枚举模式

Java的枚举类可以实现接口,而且枚举常量天然的可以与具体行为绑定,那么两者结合起来就是一种很棒的策略枚举模式(笔者自己起的名字).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum StrategyEnum implements Strategy{
COUPON(1) {
@Override
public boolean sendPrize(Long uid, String prize) {
//发放优惠券
return true;
}
},
RMB(2) {
@Override
public boolean sendPrize(Long uid, String prize) {
//发放RMB
return true;
}
}
;
private int code;
StrategyEnum(int code) {
this.code = code;
}
}

相比标准的模式,该模式省掉了Context类,而且符合大多数场景,比如用户获得礼品,该礼品对应的是Mysql的一条记录,该记录有type标识是优惠券(Coupon)还是RMB,当DAO从DB查询出来后根据typeCode,定位到具体的枚举类,然后执行其sendPrize(Long uid, String prize)完成逻辑.这个流程很清晰.
基于枚举的策略模式也有一些问题:

  1. 枚举类无法外部实例化,因此无法被IOC管理,往往策略实现都是复杂的依赖众多其他服务,那么这种时候枚举类就无从下手

IOC配合下的策略模式

实践中,客户端往往不关心具体的实现类是如何实现的,他只需要知道有这个实现类的存在,其能帮我完成任务,得到我要的结果,所以在标准的策略模式基础上,扩展Context类,让其担任选择策略的能力,而不是客户端手动选择具体的策略,也就是具体策略实现与客户端解耦,转用枚举常量来代表其所希望的策略.改进后的Context(依赖Spring IOC)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class StrategyContext implements InitializingBean {

@Resource
private Strategy couponStrategy;

@Resource
private Strategy RMBStrategy;
/**
* 保存策略与具体实现的Map
*/
private static Map<StrategyEnum, Strategy> strategyMap = new HashMap<>(2);

public Strategy getStrategy(StrategyEnum strategyEnum) {
return strategyMap.get(strategyEnum);
}

@Override
public void afterPropertiesSet() throws Exception {
strategyMap.put(StrategyEnum.COUPON, couponStrategy);
strategyMap.put(StrategyEnum.RMB, RMBStrategy);
}
}

客户端调用时使用

1
strategyContext.getStrategy(StrategyEnum.COUPON).sendPrize(uid,prize)

这里的Context相当于中间层,提供的是外观模式的功能,当新增策略时只需要新增对应的枚举类,以及具体的实现策略,在Context中添加一个新的枚举与实现类关系.客户端代码基本不要任何改变.
补充: 更加优雅的做法是利用Spring的事件机制,在Spring初始化完毕后再构建整个策略Map,可以参考我在观察者模式中所使用到的方法.
设计模式–观察者模式的思考

策略模式的本质

策略模式的本质是把复杂的算法从一个类中提取出来,用一种合理的方式管理起来,避免业务类的膨胀.
对于扩展只需要新增策略,而不需要怎么动业务代码.对于修改也只需要修改具体的策略类.业务类与策略成功的实现了低耦合.
与IOC的配合下可以更加彻底的与业务类解耦,其间只需要枚举类与策略接口进行联系,对于代码的扩展性更加有力.

与状态模式的关系

状态设计模式的类图结构与策略模式几乎是一致的.从逻辑上状态是平行的无法互相替换,但是策略与策略之间是可以完全替换的,只是实现方式的不同.在选择设计模式的时候是根据这一点来区分,代码上的体现是对于状态设计模式以State结尾,对于策略设计模式以Strategy结尾,让开发人员第一眼看过去就能明白整个设计的思路最佳.

设计模式 - 模板方法模式

设计模式–模板方法模式的思考

本文转载自屈定’s Blog

模板方法同样也是一种很实用的方法,目的是提高代码复用,并且统一大体的算法流程,比如一个一台电脑主机,定义好放置CPU,硬盘,内存等空位后,就形成了一个骨架,那么这个就是模板,具体的CPU,内存,硬盘是什么牌子型号则不需要考虑,这些是具体到业务中的实现类所负责的事情.

模板方法模式

模板方法模式可以说是抽象类的一种特性,可以定义抽象(abstract)方法与常规方法,抽象方法延迟到子类中实现.因此标准的模板方法一般是一个抽象类+具体的实现子类,抽象类(AbstractClass)负责整个执行流程的定义,而子类(ConcreteClass)负责某一具体流程的实现策略,类图如下:
img

Mybatis中的模板方法模式

实际中由于模板方法很好的兼容性,因此经常与其他设计模式混用,并且在模板类之上增加一个接口来提高系统的灵活性.因此模板类经常作为中间层来使用,比如Mybatis的Executor的设计,其中在Executor与具体实现类之间增加中间层BaseExecutor作为模板类.
img

作为模板类的BaseExecutor到底做了什么呢?举一个代码比较短的例子,下面的代码是Mybatis缓存中获取不到时执行去DB查询所需要的结果,顺便再放入缓存中的流程.其中doQuery()方法便是一个抽象方法,其被延迟到子类中来实现.而缓存是所有查询都需要的功能,因此每一个查询都会去执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// doQuery具体查询策略延迟到子类中来实现
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

BaseExecutor作为模板类的同时其还是抽象父类,因此还可以实现一些子类锁需要的公共方法,比如事务的提交与回滚,模板类的本质还是抽象类,同时也是父类,当然可以有这些公共方法的定义.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}

@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}

总结来说模板方法类作为上级,那么其要做的事情就是针对接口提出的需求进行规划,自己实现一部分,然后把需求拆分成更加细小的任务延迟到子类中实现,这是模板的责任与目的.

Spring JDBC中的模板方法模式

模板的另一种实现方式就是Java的接口回调机制,固定好方法模板后接收一个行为策略接口作为参数,模板中执行该接口的方法,比如Spring中的JdbcTemplate就是这样的设计.

1
2
3
4
5
6
7
8
9
10
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
...
stmt = con.createStatement();
applyStatementSettings(stmt);
// 执行传入的策略接口
T result = action.doInStatement(stmt);
handleWarnings(stmt);
return result;
...
}

因为篇幅原因,这里删减了很多代码,但是可以看出来这种方式实现有点策略模式的味道.其需要两个东西

  1. 方法模板,在这里是该execute()方法
  2. 策略接口,这里是StatementCallback,其本质上是一个函数是接口.

这种模式的好处自然是灵活,通过策略接口可以把行为分离出来并且可以灵活的在运行时替换掉对应的行为,雨点策略模式的味道.
那么这种到底是策略模式还是模板方法模式呢?个人认为没必要纠结这些,说他是哪个都有挺充分的理由,但是设计模式本身就是思想的体现,很多模式与模式之间都互相有思想的重叠,具体业务场景不同选择不同.

总结

模板方法在我看来更像是一个产品经理,而接口就是需求方,面对需求方模板做的事情是制定合理的统一执行计划,然后把需求拆分成更加细小的任务,分配到对应的程序员身上.
另外模板方法模式是一种变与不变的思想体现,固定不变的,提出变化的,这样增加系统的灵活性,就像圆规画圆一样,先固定中心点,然后另一个脚随意扩展.这种思想是很实用,比如产品往往提出需求后,程序员就需要考虑具体的对象模型,那么此时比较好的做法就是尽早固定出不会变化的对象,然后其他功能在此基础上做关联来扩展,最后希望本文对你有启发.

扩展想法

在日常的业务开发中我很少看到继承相关的代码,可能是和面向对象设计中提到多用组合少用继承这一原则有关.在Effective Java第16条: 复合优先于继承这一小节中中举了如下例子:
实现HashSet的计数功能,因此复写了add,addAll方法,然而因为对于父类实现逻辑的不了解(addAll实际上是循环调用add)导致了bug.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class InstrumentedHashSet<E> extends HashSet<E> {
private Integer count;

@Override
public boolean add(E e) {
count++;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> c) {
count = count + c.size();
return super.addAll(c);
}
}

这个问题的根本原因是什么?
我认为是 HashSet并不是专门为继承设计的类,因此去继承就出现了上述的问题.这么就代表代码中不应该使用继承吗?当然不是.

随后在第17条: 要么为继承而设计,并提供说明文档,要么就禁止继承指出为继承而设计是一种可取的行为,在我看来模板方法设计模式就是一种为继承而设计的方式.模板方法设计模式主要有两点本意:
1.尽早的使用模板类,也就是Abstract或者Base开头的类来让实现类分叉,分叉的越早,对于结构上的理解就越清晰,比如下方Spring MVC对URL的处理,可以很清晰的看到一种处理是定位到具体的执行方法AbstractHandlerMethodMapping,一种是定位到另一个URL,可能是静态资源,可能是其他页面AbstractUrlHandlerMapping.
img

2.降低子类的实现接口的复杂度,主要是模板类中实现了接口的方法,然后把不变的固定,变化的使用抽象接口延迟到子类中,让子类的任务更加清晰合理.比如Mybatis的BaseExector就通过doQuery()把变化的查询步骤延迟到了子类中实现.另外有一种模板类是单纯的提供代码复用,其可以当成是不含有业务属性的一个方法库,提供对所有子类都有用的公共方法.这个我在我公司订单系统中采用,如下图所示(这里只列出一部分,实际上最下层的Service还会承担更多角色),AbstractOrderService只是单纯的提供数据获取,比如获取用户信息,获取优惠券信息等方法,具体的创建逻辑在子类中,比如BizVipOrderService创建vip订单,BizResearchOrderService创建研究员订单.当子类有通性是则可以在上层增加专属抽象类来提前分叉,最终保证每一个订单创建走的流程都是可控的,当要修改某一个订单的规则时,比如vip订单可以使用优惠券,则只需要改其子类而不用担心对其他的订单类型创建有影响.
最后通过组合类提供对外的入口访问.降低外部操作的复杂性.另外最底层子类也可以实现其他接口,比如观察者来实现状态更改的通知处理.
img

那么这种设计就是为继承而设计,这种设计出来的类有一个特点,通常是以Abstract/Base开头,其就是为了继承,而不想让其他人实例化自身.最后继承作为面向对象的一大特性,掖着不用还能叫面向对象编程吗?

设计模式 - 适配器模式

设计模式–适配器模式的思考


本文转载自屈定’s Blog

个人认为适配器模式是一种加中间层来解决问题的思想,为的是减少开发工作量,提高代码复用率.另外在对于第三方的服务中使用适配器层则可以很好的把自己系统与第三方依赖解耦,降低依赖.

什么是适配器模式

适配器模式: 将一个类的接口转换为客户所期望的另一个接口.适配器让原本接口不兼容的类可以合作无间.类图如下:
img

Client: 调用方
Target: 需要提供的新功能
AdaptedObject: 系统中原本存在的类似本次需要提供的新功能的类
Adapter: Target的实现类,主要负责该功能的实现,其内部持有AdaptedObject的对象,利用其对象完成本次需要提供的新功能.

整个流程大概如下:
1.客户通过目标接口调用适配器的方法发出请求.
2.适配器(Adapter)使用被适配器(AdaptedObject)已有的功能完成客户所期望的新功能
3.客户收到调用结果,但是并不知道是适配器起到的转换作用.
那么Adapter利用已经完成的AdaptedObject类实现本次提供的新功能,这一过程就是适配.

Java I/O中的适配器

在Java I/O中有把字节流转换为字符流的类java.io.InputStreamReader以及java.io.OutputStreamWriter.那么这两个类实际上使用的就是适配器模式
InputStreamReader为例,其继承了Reader类,所提供的功能是把字节流转换为字符流,其内部拥有StreamDecoder这一实例,所有的转换工作是由该实例完成.

1
2
3
4
public int read(char cbuf[], int offset, int length) throws IOException {
// 使用被适配器的功能
return sd.read(cbuf, offset, length);
}

那么在这个例子中
Client是调用方,也就是我们开发人员
Target是Reader这个抽象类.
AdaptedObject是StreamDecoder,利用的是其功能.
Adapter是InputStreamReader

Java Set集合中的适配器

Java中的Set集合有者无序,唯一元素,查找复杂度O(1)等特性.这些特性Map数据结构的key是完全符合的,那么就可以利用适配器模式来完成Set的功能.
HashSet为例,其内部持有的是一个值为固定Object的Map,如下图
img

其所有的操作会通过HashSet这个适配器来操作HashMap这个被适配器.比如:

1
2
3
4
5
6
public Iterator<E> iterator() {
return map.keySet().iterator();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

Client是调用方,也就是我们开发人员
Target是Set这个接口.
AdaptedObject是HashMap,利用的是其功能.
Adapter是HashSet

Mybatis中的适配器模式

Mybatis作为一款通用框架,对于日志处理必然需要适配到各种日志框架,比如slf4j,log4j,logback等,每个日志的API多多少少有点不同,这种情况下适配器模式就起到了转换的作用.
以下图由于实现类太多,只列取了几个.
img
Mybatis有自己的org.apache.ibatis.logging.Log接口,框架内部使用的都是自己的Log,具体使用哪一个Log是由配置中的适配器决定的.
org.apache.ibatis.logging.log4j2.Log4j2LoggerImpl适配器为例,org.apache.logging.log4j.Logger为被适配者.Log4j2LoggerImpl是适配器,起到了转换的作用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Log4j2LoggerImpl implements Log {

private static final Marker MARKER = MarkerManager.getMarker(LogFactory.MARKER);
//被适配者
private final org.apache.logging.log4j.Logger log;

public Log4j2LoggerImpl(Logger logger) {
log = logger;
}

@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
.....
}

与装饰者模式的区别

个人认为这两种设计模式是完全不同的思想:
装饰者模式本意是增强功能,其装饰者与被装饰者对于调用方是很清晰的,比如ContreteDecoratorA decoratorA = new ContreteDecoratorA(new ComponentInterfaceImpl());就很清晰的知道使用ContreteDecoratorA装饰了ComponentInterfaceImpl.另外ContreteDecoratorA并没有改变ComponentInterfaceImpl的功能提供出去,而是为其进行了增强处理.
适配器模式本意是复用已有的代码,对已经存在的功能进行包装转换,以另一种形式提供出去.比如HashSet,对于调用方来说其内部使用的HashMap是不可见的,调用方不关心内部被适配者是谁,只是关注该功能本身也就是Set接口.
要说相同点的话那就是都是组合复用思想对一个对象进行包装,但其目的有着本质的区别.还望好好理解.

与外观模式的区别

外观模式本意是把一组复杂的关联行为进行包装,提供一个面向开发人员更为简单的使用方式.举个例子,你觉得JDBC方式不太好用,因此写了个DBUtils这种封装类,实际上就是一种外观模式,与适配器还是有着很大的区别.

你写的代码,是别人的噩梦吗

你写的代码,是别人的噩梦吗


本文转自屈定’s Blog

Frank,是来自阿里国际技术事业部的高级技术专家,从业十年,也是一位英语说到飞起的型男。今天他将与大家聊聊关于企业应用架构实践的话题。

阿里高级技术专家Frank
从业这么多年,接触过银行的应用,Apple的应用,eBay的应用和现在阿里的应用,虽然分属于不同的公司,使用了不同的架构,但有一个共同点就是都很复杂。导致复杂性的原因有很多,如果从架构的层面看,主要有两点,一个是架构设计过于复杂,层次太多能把人绕晕。另一个是根本就没架构,ServiceImpl作为上帝类包揽一切,一杆捅到DAO(就简单场景而言,这种Transaction Script也还凑合,至少实现上手都快),这种人为的复杂性导致系统越来越臃肿,越来越难维护,酱缸的老代码发出一阵阵恶臭,新来的同学,往往要捂着鼻子抠几天甚至几个月,才能理清系统和业务脉络,然后又一头扎进各种bug fix,业务修补的恶性循环中,暗无天日!
img
CRM作为阿里最老的应用系统,自然也逃不过这样的宿命。不甘如此的我们开始反思到底是什么造成了系统复杂性?我们到底能不能通过架构来治理这种复杂性?基于这个出发点,我们团队开始了一段非常有意义的架构重构之旅(Redefine theArch),期间我们参考了SalesForce,TMF2.0,汇金和盒马的架构,从他们那里汲取了很多有价值的输入,再结合我们自己的思考最终形成了我们自己现在的基于扩展点+元数据+CQRS+DDD的应用架构。该架构的特点是可扩展性好,很好的贯彻了OO思想,有一套完整的规范标准,并采用了CQRS和领域建模技术,在很大程度上可以降低应用的复杂度。本文主要阐述了我们的思考过程和架构实现,希望能对在路上的你有所帮助。

复杂性来自哪里?

经过我们分析、讨论,发现造成现在系统异常复杂的罪魁祸首主要来自以下四个方面:

可扩展性差

对于只有一个业务的简单场景,并不需要扩展,问题也不突出,这也是为什么这个点经常被忽略的原因,因为我们大部分的系统都是从单一业务开始的。但是随着支持的业务越来越多,代码里面开始出现大量的if-else逻辑,这个时候代码开始有坏味道,没闻到的同学就这么继续往上堆,闻到的同学会重构一下,但因为系统没有统一的可扩展架构,重构的技法也各不相同,这种代码的不一致性也是一种理解上的复杂度。久而久之,系统就变得复杂难维护。

像我们CRM应用,有N个业务方,每个业务方又有N个租户,如果都要用if-else判断业务差异,那简直就是惨绝人寰。其实这种扩展点(ExtensionPoint),或者叫插件(Plug-in)的设计在架构设计中是非常普遍的。比较成功的案例有eclipse的plug-in机制,集团的TMF2.0架构。还有一个扩展性需求就是字段扩展,这一点对SaaS应用尤为重要,因为有很多客户定制化需求,但是我们很多系统也没有统一的字段扩展方案。

面向过程

是的,不管你承认与否,很多时候,我们都是操着面向对象的语言干着面向过程的勾当。面向对象不仅是一个语言,更是一种思维方式。在我们追逐云计算、深度学习、区块链这些技术热点的时候,静下心来问问自己我们是不是真的掌握了OOD;在我们强调工程师要具备业务Sense,产品Sense,数据Sense,算法Sense,XXSense的时候,是不是忽略了对工程能力的要求。

据我观察大部分工程师(包括我自己)的OO能力还远没有达到精通的程度,这种OO思想的缺乏主要体现在两个方面,一个是很多同学不了解SOLID原则,不懂设计模式,不会画UML图,或者只是知道,但从来不会运用到实践中;另一个是不会进行领域建模,关于领域建模争论已经很多了,我的观点是DDD很好,但不是银弹,用和不用取决于场景。但不管怎样,请你抛开偏见,好好的研读一下EricEvans的《领域驱动设计》,如果有认知升级的感悟,恭喜你,你进阶了。

我个人认为DDD最大的好处是将业务语义显现化,把原先晦涩难懂的业务算法逻辑,通过领域对象(Domain Object),统一语言(Ubiquitous Language)将领域概念清晰的显性化表达出来。相信我,这种表达带来的代码可读性的提升,会让接手你代码的人对你心怀感恩的。借用Abelson的一句话是

Programs must be written for people to read, and only incidentally for machines to execute.

所以强烈谴责那些不顾他人感受的编码行为。
img

分层不合理

俗话说的好,All problemsin computer science can be solved by another level of indirection(计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决),怎样?是不是感受到间接层的强大了。分层最大的好处就是分离关注点,让每一层只解决该层关注的问题,从而将复杂的问题简化,起到分而治之的作用。我们平时看到的MVC,pipeline,以及各种valve的模式,都是这个道理。

好吧,那是不是层次越多越好,越灵活呢。当然不是,就像我开篇说的,过多的层次不仅不能带来好处,反而会增加系统的复杂性和降低系统性能。就拿ISO的网络七层协议来说,你这个七层分的很清楚,很好,但也很繁琐,四层就够了嘛。再比如我前面提到的过度设计的例子,如果没记错的话应该是Apple的Directory Service应用,整个系统有7层之多,把什么validator,assembler都当成一个层次来对待,能不复杂么。所以分层太多和没有分层都会导致系统复杂度的上升,因此我们的原则是不可以没有分层,但是只分有必要的层。

随心所欲

随心所欲是因为缺少规范和约束。这个规范非常非常非常的重要(重要事情说三遍),但也是最容易被无视的点,其结果就是架构的consistency被严重破坏,代码的可维护性将急剧下降,国将不国,架构将形同虚设。有同学会说不就是个naming的问题么,不就是个分包的问题么,不就是2个module还是3个module的问题么,只要功能能跑起来,这些问题都是小问题。是的,对于这些同学,我再丢给你一句名言“Just because you can, doesn’t mean you should”。就拿package来说,它不仅仅是一个放一堆类的地方,更是一种表达机制,当你将一些类放到Package中时,相当于告诉下一位看到你设计的开发人员要把这些类放在一起考虑。

理想很丰满,现实很骨感,规范的执行是个大问题,最好能在架构层面进行约束,例如在我们架构中,扩展点必须以ExtPt结尾,扩展实现必须以Ext结尾,你不这么写就会给你抛异常。但是架构的约束毕竟有限,更多的还是要靠Code Review,暂时没想到什么更好的办法。这种对架构约束的近似严苛follow,确保了系统的consistency,最终形成了一个规整的收纳箱(如下图所示),就像我和团队说的,我们在评估代码改动点时,应该可以像Hash查找一样,直接定位到对应的module,对应的package里面对应的class。而不是到“一锅粥”里去慢慢抠。
img
本章节最后,上一张我们老系统中比较典型的代码,也许你可以从中看到你自己应用的影子。
复杂性应对之道
知道了问题所在,接下来看下我们是如何一个个解决这些问题的。回头站在山顶再看这些解决方案时,每个都不足为奇,但当你还“身在此山中”的时候,这个拨开层层迷雾,看到山的全貌的过程,并不是想象的那么容易。庆幸的是我团队在艰难跋涉之后,终有所收获。

1、扩展点设计

扩展点的设计思想主要得益于TMF2.0的启发,其实这种设计思想也一直在用,但都是在局部的代码重构和优化,比如基于Strategy Pattern的扩展,但是一直没有找到一个很好的固化到框架中的方法。直到毗卢到团队分享,给了我们两个关键的提示,一个是业务身份识别,用他的话说,如果当时TMF1.0如果有身份识别的话,就没有TMF2.0什么事了;另一个是抽象的扩展点机制。

身份识别

业务身份识别在我们的应用中非常重要,因为我们的CRM系统要服务不同的业务方,而且每个业务方又有多个租户。比如中供销售,中供拍档,中供商家都是不同的业务方,而拍档下的每个公司,中供商家下的每个供应商又是不同的租户。所以传统的基于多租户(TenantId)的业务身份识别还不能满足我们的要求,于是在此基础上我们又引入了业务码(BizCode)来标识业务。

所以我们的业务身份实际上是(BizCode,TenantId)二元组。在每一个业务身份下面,又可以有多个扩展点(ExtensionPoint),所以一个扩展点实现(Extension)实际上是一个三维空间中的向量。借鉴Maven Coordinate的概念我给它起了个名字叫扩展坐标(Extension Coordinate),这个坐标可以用(ExtensionPoint,BizCode,TenantId)来唯一标识。
img

扩展点

扩展点的设计是这样的,所有的扩展点(ExtensionPoint)必须通过接口申明,扩展实现(Extension)是通过Annotation的方式标注的,Extension里面使用BizCode和TenantId两个属性用来标识身份,框架的Bootstrap类会在Spring启动的时候做类扫描,进行Extension注册,在Runtime的时候,通过TenantContext来选择要使用的Extension。TenantContext是通过Interceptor在调用业务逻辑之前进行初始化的。整个过程如下图所示:
img

2、面向对象

领域建模

准确的说DDD不是一个架构,而是思想和方法论。所以在架构层面我们并没有强制约束要使用DDD,但对于像我们这样的复杂业务场景,我们强烈建议使用DDD代替事务脚本(TS: Transaction Script)。因为TS的贫血模式,里面只有数据结构,完全没有对象(数据+行为)的概念,这也是为什么我们叫它是面向过程的原因。然而DDD是面向对象的,是一种知识丰富的设计(Knowledge Rich Design),怎么理解?,就是通过领域对象(Domain Object),领域语言(Ubiquitous Language)将核心的领域概念通过代码的形式表达出来,从而增加代码的可理解性。这里的领域核心不仅仅是业务里的“名词”,所有的业务活动和规则如同实体一样,都需要明确的表达出来。

例如前面典型代码图中所展示的,分配策略(DistributionPolicy)你把它隐藏在一堆业务逻辑中,没有人知道它是干什么的,也不会把它当成一个重要的领域概念去重视。但是你把它抽出来,凸显出来,给它一个合理的命名叫DistributionPolicy,后面的人一看就明白了,哦,这是一个分配策略,这样理解和使用起来就容易的多了,添加新的策略也更方便,不需要改原来的代码了。

所以说好的代码不仅要让程序员能读懂,还要能让领域专家也能读懂。再比如在CRM领域中,公海和私海是非常重要领域概念,是用来做领地(Territory)划分的,每个销售人员只能销售私海(自己领地)内的客户,不能越界。但是在我们的代码中却没有这两个实体(Entity),也没有相应的语言和其对应,这就导致了领域专家描述的,和我们日常沟通的,以及我们模型和代码呈现的都是相互割裂的,没有关联性。这就给后面系统维护的同学造成了极大的困扰,因为所有关于公海私海的操作,都是散落着各处的repeat itself的逻辑代码,导致看不懂也没办法维护。

所以当尚学把这两个领域概念抽象成实体之后,整个模型和代码都一下子变清晰很多。在加上上面介绍的把业务规则显现化,极大的提升了代码的可读性和可扩展性。用尚学的话说,用DDD写代码,他找到了创作的感觉,而不仅仅是码农式Coding。下图是销售域的简要领域模型,但基本上能表达出销售域的核心领域概念。
img
关于CQRS简要说一下,我们只使用了Command,Query分离的概念,并没有使用Event Sourcing,原因很简单—不需要。关于Command的实现我们使用了命令模式,因此以前的ServiceImpl的职责就只是一个Facade,所有的处理逻辑都在CommandExecutor里面。

SOLID

SOLID是单一职责原则(SRP),开闭原则(OCP),里氏替换原则(LSP),接口隔离原则(ISP)和依赖倒置原则(DIP)的缩写,原则是要比模式(Design Pattern)更基础更重要的指导准则,是面向对象设计的Bible。深入理解后,会极大的提升我们的OOD能力和代码质量。比如我在开篇提到的ServiceImpl上帝类的例子,很明显就是违背了单一职责,你一个类把所有事情都做了,把不是你的功能也往自己身上揽,所以你的内聚性就会很差,内聚性差将导致代码很难被复用,不能复用,只能复制(Repeat Yourself),其结果就是一团乱麻。
img
再比如在java应用中使用logger框架有很多选择,什么log4j,logback,common logging等,每个logger的API和用法都稍有不同,有的需要用isLoggable()来进行预判断以便提高性能,有的则不需要。对于要切换不同的logger框架的情形,就更是头疼了,有可能要改动很多地方。产生这些不便的原因是我们直接依赖了logger框架,应用和框架的耦合性很高。

怎么破? 遵循下依赖倒置原则就能很容易解决,依赖倒置就是你不要直接依赖我,你和我都同时依赖一个接口(所以有时候也叫面向接口的编程),这样我们之间就解耦了,依赖和被依赖方都可以自由改动了。
img
在我们的框架设计中,这种对SOLID的遵循也是随处可见,Service Facade设计思想来自于单一职责SRP;扩展点设计符合关闭原则OCP;日志设计,以及Repository和Tunnel的交互就用到了依赖倒置DIP原则,这样的点还有很多,就不一一枚举了。当然了,SOLID不是OO的全部。抽象能力,设计模式,架构模式,UML,以及阅读优秀框架源码(我们的Command设计就是参考了Activiti的Command)也都很重要。只是SOLID更基础,更重要,所以我在这里重点拿出来讲一下,希望能得到大家的重视。

3、分层设计

这一块的设计比较直观,整个应用层划分为三个大的层次,分别是App层,Domain层和Repostiory层。

1.App层主要负责获取输入,组装context,做输入校验,发送消息给领域层做业务处理,监听确认消息,如果需要的话使用MetaQ进行消息通知;

2.Domain层主要是通过领域服务(Domain Service),领域对象(Domain Object)的交互,对上层提供业务逻辑的处理,然后调用下层Repository做持久化处理;

3.Repository层主要负责数据的CRUD操作,这里我们借用了盒马的数据通道(Tunnel)的概念,通过Tunnel的抽象概念来屏蔽具体的数据来源,来源可以是MySQL,NoSql,Search,甚至是HSF等。
img
这里需要注意的是从其他系统获取的数据是有界上下文(Bounded Context)下的数据,为了弥合Bounded Context下的语义Gap,通常有两种方式,一个是用大领域(Big Domain)把两边的差异都合起来,另一个是增加防腐层(Anticorruption Layer)做转换。什么是Bounded Context? 简单阐述一下,就是我们的领域概念是有作用范围的(Context)的,例如摇头这个动作,在中国的Context下表示NO,但是在印度的Context下却是YES。

4、规范设计

我们规范设计主要是要满足收纳原则的两个约束:放对位置东西不要乱放,我们的每一个组件(Module),每一个包(Package)都有明确的职责定义和范围,不可以放错,例如extension包就只是用来放扩展实现的,不允许放其他东西,而Interceptor包就只是放拦截器的,validator包就只是放校验器的。我们的主要构件如下图所示:
img

贴好标签

东西放在合适位置后还要贴上合适的标签,也就是要按照规范合理命名,例如我们架构里面和数据有关的Object,主要有Client Object,Domain Object和Data Object,Client Object是放在二方库中和外部交互使用的DTO,其命名必须以CO结尾,相应的Data Object主要是持久层使用的,命名必须以DO结尾。这个类名应该是自明的(self-evident),也就是看到类名就知道里面是干了什么事,这也就反向要求我们的类也必须是单一职责的(Single Responsibility)的,如果你做的事情不单纯,自然也就很难自明了。如果我们Class Name是自明的,Package Name是自明的,Module Name也是自明的,那么我们整个应用系统就会很容易被理解,看起来就会很舒服,维护效率会提高很多。我们的命名规则如下图所示:
GTS应用架构
经过上面的长篇大论,我希望我把我们的架构理念阐述清楚了,最后再从整体上看下我们的架构吧。如果觉得不错,也可以把framework code拉下来自己玩一下。

整体架构

我们的架构原则很简单,即在高内聚,低耦合,可扩展,易理解大的指导思想下,尽可能的贯彻OO的设计思想和原则。我们最终形成的架构是集成了扩展点+元数据+CQRS+DDD的思想,关于元数据前面没怎么提到,这里稍微说一下,对于字段扩展,简单一点的解决方案就是预留扩展字段,复杂一点的就是使用元数据引擎。

使用元数据的好处是不仅能支持字段扩展,还提供了丰富的字段描述,等于是为以后的SaaS化配置提供了可能性,所以我们选择了使用元数据引擎。和DDD一样,元数据也是可选的,如果对没有字段扩展的需求,就不要用。最后的整体架构图如下:
-END-

设计模式 - 组合模式

设计模式–组合模式的思考

本文转载自屈定’s Blog


组合模式是一种抽象树形结构的模式,其在业务开发中也是一种很有用的设计模式,下面开始分析.

组合模式

业务中有很多树形结构的表示,比如下面的目录结构

1
2
3
4
5
6
7
-- 男装
-- 上衣
-- 品牌1
-- 品牌2
-- 裤子
-- 品牌1
-- 品牌3

针对男装可以认为其是树的根节点,上衣,裤子这种下面还可以有节点的称为树枝节点,品牌这种下面不再有分支的称为叶子节点
那么转换成面向对象该怎么表示呢?

一般做法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 根节点
public class RootNode {
private List<CompositeNode> compositeNodes;// 针对节点区别对待,导致处理麻烦
private List<LeafNode> leafNodes; // 针对节点区别对待,导致处理麻烦
}
// 树枝节点
public class CompositeNode {
private List<LeafNode> leafNodes;
private List<CompositeNode> compositeNodes;
}
// 叶子节点
public class LeafNode {
}

这种做法是面向对象的思想,但是其最大的问题是对这三种类型的节点区别对待了,那么客户端就必须明确的得知这个节点到底是根还是树枝或者是叶子,那么对于客户端来说无疑是比较辛苦的,另外从功能上来说节点之间区别并不是很大,可以说是完全一样的.那么组合模式的作用就是统一这三种类型的节点,让客户端当成一种节点来处理.下面是组合模式下的方式

组合设计

1
2
3
4
5
6
7
8
9
10
11
// 其为节点的约束,主要暴露给客户端,客户端不需要了解子类是什么.
public abstract class Node {
}
// 树枝节点,当然也可以是根节点
public class CompositeNode extends Node {
// 持有Node集合,可以无限往下延伸
private List<Node> nodes;
}
// 叶子节点,其下面不再有其他节点
public class LeafNode extends Node {
}

那么相比之前的设计好在了哪里?组合体现在了哪里?

  1. 相比之前设计,这里用了一个抽象类暴露出去给客户端,只需要把客户端需要的方法定义在抽象类中,那么大大减少了客户端的理解成本,对于客户端来说节点都是一个性质的,没必要区分根,树枝,叶子等.
  2. 组合体现在CompositeNode节点的设计,其内部引用的是Node抽象类实例,也就是可以一直往下延伸.
  3. 组合模式更多的是一种面向接口编程的思想,大多数日常开发中总会有意无意的使用了这种模式思想.

Mybatis中的组合模式应用

开发中我们写的动态Sql,Mybatis会按照下面方式去理解这个结构,比如

1
2
3
4
5
6
7
8
9
10
<select id="findById" resultMap="RM-CLASSROOM">
SELECT <include refid="RM-CLASSROOM-ALLCOLS"/>
FROM classroom WHERE status = 0
<if test="!ids.isEmpty()">
AND id in
<foreach collection="list" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</if>
</select>

Mybatis解析后大概会是下面的这种树形结构,最后在拼接成需要的Sql.

1
2
3
4
5
6
7
8
-- select  根节点
-- select 叶子节点
-- <include refid="RM-CLASSROOM-ALLCOLS"/> 叶子节点
-- where status = 1 叶子节点
-- <if test="!ids.isEmpty()" 树枝节点
-- AND id IN 叶子节点
-- <foreach item="list" ..... 树枝节点
-- #{item} 叶子节点

那么这种情况下是很适合用组合模式,因此Mybatis抽象出SqlNode接口暴露给客户端

1
2
3
public interface SqlNode {
boolean apply(DynamicContext context);
}

其有如下子类(子类太多,省略了一些),按照这些子类再翻译下上面的sql
img

1
2
3
4
5
6
7
8
-- select  MixedSqlNode
-- select StaticTextSqlNode
-- <include refid="RM-CLASSROOM-ALLCOLS"/> StaticTextSqlNode
-- where status = 1 StaticTextSqlNode
-- <if test="!ids.isEmpty()" IfSqlNode 内部的contents为MixedSqlNode
-- AND id IN StaticTextSqlNode
-- <foreach item="list" ..... ForEachSqlNode 内部的contents为MixedSqlNode
-- #{item} StaticTextSqlNode

img

从结构上来说,非叶子节点,例如IfSqlNode,ForEachSqlNode是可以一直嵌套的,所实现的关键就是SqlNode接口与MixedSqlNode实现类.
从客户端角度来说里面的节点这些都是不关心的,其只需要拿到SqlNode rootSqlNode实例,然后调用下rootSqlNode.apply(context)即可获取到自己想要的sql原型.
这两个也是组合模式要解决的问题.

SpringMVC中的组合模式

SpringMVC中对参数的解析使用的是HandlerMethodArgumentResolver接口,该类有一个实现类为HandlerMethodArgumentResolverComposite,该类为一个组合类,其结构如下:
img
其本身实现了HandlerMethodArgumentResolver接口,又持有其他HandlerMethodArgumentResolver对象,那么这种设计就是组合模式设计.,在它的实现方法中是对其他组合模式中的节点进行循环处理,从而选择最适合的一个.

1
2
3
4
5
6
7
8
9
10
11
12
13
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
// 对其所拥有的对象循环,找到最适合处理的一个
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
break;
}
}
}
return result;
}

对于HandlerMethodArgumentResolver来说,其虽然拥有众多子类,但是对于调用方来说却只关心参数所解析的结果,它并不知道该使用哪一个具体的子类,它所希望的是能以整体的形式去访问这些子类,从而选择最适合自己的一个参数解析器.那么HandlerMethodArgumentResolverComposite在这里扮演的就是一个整体的角色,对客户端来说调用的是这个整体.

Netty中的组合模式

Netty中的CompositeByteBuf使用了组合设计模式,但是其有点特殊,Netty所描述的零拷贝是应用层面上不做任意的数据复制,而是使用组合的方式拷贝,比如有两个Buf,headByteBuftailByteBuf,那么现在的需求是把两个合在一起,很自然的想到先创建一个新的buf,然后把headByteBuf复制进去,再把tailByteBuf复制进去,这个过程中涉及到两次应用层面的拷贝,自然不是高效的做法,那么CompositeByteBuf的实现是什么样子的呢?

CompositeByteBuf的意思是组合,他所采取的方式是把headByteBuftailByteBuf组合起来,对外相当于一个新的Buf,这样的方式不会产生任何应用层面的数据拷贝,原理如下示意图所示:
img

那么这也是一种组合设计模式的思想,更可以说是一种妙用。

安全性与透明性

透明性
所谓的透明性是客户在使用组合模式对象时不需要关心这个节点到底是根还是树枝或者是叶子,对于自己来说都是组件对象,只需要获取一个起始点就能拿到自己想要的东西,所谓的透明性表现在接口中暴露出了所有节点的公共方法,比如添加子节点,移除子节点等,那么就必然会存在叶子节点的添加子节点功能不支持的情况,此时调用应该抛出UnsupportedOperationException.
举个反例Mybatis中客户只需要拿到SqlNode rootSqlNode就可以获取到想要的sql,对于客户端唯一的入口就是这个rootSqlNode.apply(context)获取到对应的sql,客户端本身无法修改这个节点.那么这种行为是非透明的.

安全性
非透明性实现一般就是安全性的实现,所谓的安全性保证就是一旦节点构建完毕,客户端就无法更改,只需要获取到自己想要的东西就好.SqlNode就是一种安全性的实现,所谓的安全性表现在SqlNode接口中没有暴露修改的方法,节点是在构造阶段就组装完毕的.

具体选择哪种,需要根据业务来定夺,如果是类似Mybatis这种先准备好所有数据再执行的模式,那么安全性实现则是最好的选择.如果是业务处理模式下边处理边构造,则透明性最佳.

总结

  1. 组合模式在于结构上的统一,对外接口的一致,给客户端提供更加统一或者只提供必要的操作.
  2. 组合模式是面向接口编程的思想体现,通过接口实现客户端的操作便捷与约束,同时实现更加灵活的自由组合.