不变性immutablity设计

11-10-21 banq
                   

不变性是统领业务分析和高性能架构重要法门,通过业务上不变性分析设计,可以实现代码运行的并发高性能和高扩展性。

不可变性是一种抽象,它并不在自然界中存在,世界是可变的,持续不断变化。所以数据结构是可变的,他们是描述真实世界某段时间内的状态。而状态经常会被修改,如下面情况:

1.状态被并发线程同时修改

2.多个用户对一个共享对象(状态)进行冲突性修改。

3.用户提供了无效数据,应该被回滚。

在自然可变的模型下,会在上面情况下发生不一致性或数据破碎crushing,显然可变性很容易导致错误。我们需要的是一种新视野,新角度,一种事务transaction视野,当你从事务性程序中看世界时,一切都是不可变的。这和蒯因与引用透明 中功能重复执行不可变原理非常类似,体现逻辑的线性特征。

不可变性说得这么玄,是不是和平常编程没有太大关系呢?

完全错误。举个例子,当你要对一个变量进行修改时,这个变量处于一个对象A内,你是直接对这个对象(A类的实例)内这个变量进行修改?还是重新创建一个A类新的对象实例,新的变量在新的对象实例中,然后替换掉原来的对象呢?

很多程序员对这么一个权衡问题没有过多考虑,基本是随意选择其中方案,甚至根据工作量多少为抉择,这些不经意行为都为整个系统的性能留下祸根。

这个问题其实涉及到了不可变性的问题,如果A类被设计为不可变了,那么就要采取第二个的方案,创建后替换,这个答案带来两个问题:

1.程序员如何知道A类是一个不可变的类?

2.不可变类为什么能够获得性能提高?

关键问题是第一个,我认为我们可以通过OOA/OOD等建模方式明确哪个类为不可变,很显然DDD领域驱动设计方法中,有值对象为不可变的概念,如果领域专家指定来自需求中哪个类是值对象,那么我们程序员就容易在编程实践中采取构建替换的办法,从而又能照顾到了计算机底层的性能。

我2006年在jivejdon按照DDD设计开发时,探索性地提出状态可以表达设计为值对象概念,见实战DDD(Domain-Driven Design领域驱动设计:Evans DDD),虽然此后一直有反复和怀疑,但是最近看了如何打败CAP定理?一文以后,比较肯定了自己当初想法,心中一块石头落地,这是一种探索的乐趣。

下面谈谈我如何在JiveJdon 4.5版本中如何把值对象状态不可变性贯彻落实完整的情况,在之前版本因为心中不确定,所以对状态修改时,没有遵循上面提到的第二步法则,而是直接对对象字段修改,就好象直接去修改数据表字段那样鲁莽。

这种破坏性修改的恶果也让我吃尽苦头,就是数据出现严重的不一致性,如帖子更新状态,经常是增加新帖后,在论坛列表的“最近更新”栏目出现的不是最后增加的那个新帖,有时正确有时错误,头疼不已,当时是非常怀念基于数据库的编程模式,因为这个功能只要通过一条SQL查询就能完成,轻松而简单,而JiveJdon是基于内存中的对象,这个功能是对对象的状态进行修改完成的,方式不同,思路就不一样了,后来虽然费了九牛二虎之力勉强正确,但是不够优雅和轻量,一直是自己不敢面对的痛。

现在理顺了值对象状态不可变性这样一个思路,我几乎三下五除二把JiveJdon这个功能就重构了,而且一次性调试通过。下面谈谈这个重构过程。先展示一下JiveJdon的设计类图:

将ForumThreadState这个经常变换的状态和实体聚合边界内其他变化是不一致的,惨遭DDD一书中的订单价格变化案例设计为单独的ForumThreadState,下面问题是,ForumThreadState变化是直接修改其中字段,还是重新构建一个ForumThreadState替换原来的,不幸的是我和大多数一样,起初没有太多留意走的是前一种办法,绕了不少弯路。

[该贴被banq于2011-10-21 08:45修改过]

                   

37
banq
2011-10-21 09:07

下面谈谈如何重构,过程比较简单,以Forum的ForumState为案例,类似上面的ForumThreadStat。

将ForumState以Java中最大可能的不可变语法表达,在国外论坛上看到不少Scala程序员欢快地使用Scala的不可变+Actor两个方式编写出简单容易并发程序,我也可以借助JdonFramework的ES模型,通过稍微复杂点的不可变+Domain Events实现类似的并发编程,ForumState的稍微复杂点不可变性的代码如下:

public class ForumState {
        private final AtomicLong threadCount;
      
        private final AtomicLong messageCount;

        private final ForumMessage lastPost;

        private final Forum forum;

        private final SubscribedState subscribedState;

        public ForumState(Forum forum, ForumMessage lastPost, long messageCount, long threadCount) {
                super();
                this.forum = forum;
                this.lastPost = lastPost;
                this.messageCount = new AtomicLong(messageCount);
                this.threadCount = new AtomicLong(threadCount);
                this.subscribedState = new SubscribedState(new ForumSubscribed(forum));
        }
<p>

源码见:

http://code.google.com/p/jivejdon/source/browse/trunk/jivejdon/src/com/jdon/jivejdon/model/ForumState.java

Java中体现不可变性的特点主要是final和构造函数,ForumState的就没有了所有setXXXX方法,不能对ForumState内部单独修改,强迫替换整个对象。

有了不可变性的值对象,根据DDD设计要则,还需要一个工厂来维护这种不变性,也就是ForumState的创新new需要通过工厂专门统一实现,不是任意地随意new创建的,

专门创建一个FormStateFactory负责FormState的生命周期维护,FormStateFactory代码:

public interface ForumThreadStateFactory {

        void init(ForumThread forumThread, ForumMessage lastPost);

        void addNewMessage(ForumThread forumThread, ForumMessage newLastPost);

        void updateMessage(ForumThread forumThread, ForumMessage forumMessage);
}
<p>

http://code.google.com/p/jivejdon/source/browse/trunk/jivejdon/src/com/jdon/jivejdon/model/state/ForumThreadStateFactory.java

那么现在来回答上面我提出不可变性编程中第二个问题,不可变性为什么可以获得性能提升呢?

主要是回避了共享锁synchronize,原来在上面的addNewMessage和updateMessage方法前都要加同步锁synchronized的,现在不需要了,因为新对象替换旧对象本身是一个原子操作。对比图见附件图。

在多线程编程中有一个不可变Immutable模式:

Immutable是当被构造以后就不再改变。

Immutable 的对象总是线程安全。

特征:

1. 构造以后就不会改变;

2. 所有字段是 final;

3. 它是正常构造。

发布一个Immutable对象是线程安全的。


banq
2011-10-21 09:14

下面是一些深入思考,欢迎大家探讨:

需要注意的是:不同的事件引起不同的状态,如读取事件引发读状态的变化,比如帖子的浏览数;而写事件引发写状态的变化;越是频繁发生的事件,其不变性的时间周期Scope就越短,实现的手段就有区别。

帖子浏览数是帖子的一种读取次数状态,频繁发生,如果每次读取事件发生,我们象上面采取值对象不变性原则,每次都构造一个新的值对象,无疑会有大量垃圾临时对象产生,从而频繁触发JVM的垃圾回收机制,那么在这种情况下,我们可以采取其他并发措施,比如使用JDK的原子类型AtomicLong等,通过其提供的自增获取的原子功能实现并发。如jivejdon中的ViewCounter模型

还有一种状态是结构关系状态,比如帖子之间的相互回复关系可以组成一个二叉树的模型,如果有新帖写事件发生,会增加一个新帖ID在这个树结构中。这是一种难以避免的在原来数据上进行修改情况。如jivejdon中的ForumThreadTreeModel.java

值得注意的是,上面这些对值对象内部字段的修改一定是要通过聚合根实体的方法进行,不能把值对象暴露给外界直接供外界修改,这样也破坏了聚合边界内的不变性规则约束。比如ForumThread一定要方法对其值对象ViewCounte的修改方法,不能直接将ViewCounte通过getViewCounte()暴露给外部。

由此可见:不变性immutablity设计是贯彻建模设计代码和性能整个环节的一条主要编程线索。这条主要线索又有多少当前程序员能够把握且每天实践的呢?

[该贴被banq于2011-10-21 09:32修改过]

xmuzyu
2011-10-21 11:14

世界上唯一不变的真理就是要变,对于一个事物而言,变化可以通过时间段来表示出来,但是永恒却需要用时间点来概述。比如一个人不同时期可能行为心智都不一样,但是在某个时间点上的行为和心智已经永恒,你不可能回到和一分钟前完全一样的你自己,每个时间点都代表着一种永恒。那么世界上的事物的状态又是如何随着时间来变化的呢?我能想到也就两种,一种是增量型的变化,比如我们每个人的知识的储备就是一个种增量式的变化,这其实可以理解为量变,另外一种就是全新的本质性的变化,比如金庸小说中心中无剑的状态,心中无剑的状态已经不同于以往的状态是一种全新的质的飞跃,这我们可以理解为质变。

那么我们回到软件设计当中的时候,对象状态变化是相对于时间段而言的,但是某一个时间点的状态就已经永恒的存在了,这个时间点的状态其实就是不变性。那么当对象状态在随着时间变化的过程中,我们软件设计人员如何去让对象的状态适应变化呢?我能想到的也就两种,一种就是jivejdon之前采用的synchronized的方式,另外一种就是本帖banq老师所说的不变性状态替换。

下面我再唠叨一下采用不变性的状态的一些想法,对于一个单机的系统,我们是可以通过synchronzied的方式来实现对对象状态的并发修改,而对于一个分布式的系统或者是集群中的时候,就很难使用synchronized了,但是这种情况下,我们转而采用状态替换的方式,就既可以避免分布式加锁带来的复杂性,同时还可以实现对象状态的更新,何乐不为之呢?

jdon007
2011-10-21 11:58

一、唯一不变的是变化本身

早上,我去市场买的蔬菜是新鲜的;晚上,蔬菜却可能不再新鲜。从逻辑上而言,一个命题的真假也可能随着时间的变化而变化。

可以设计具有不变性的状态特征(值对象),但状态特征(值对象)不一定具有不变性,设计不变性的状态特征更多是为了高并发访问(主要是读)的一致性与有效性。

不变性设计的适用的场合—实例的状态被多个线程频繁读取(高并发环境中,实例状态被共享)。

二、Java如何描述可写(可变)与只读(不可变)的状态特征

Java如何描述某个模型(实例)具有不变性的状态特征?

从Java代码外观上看,可描述如下:

1、目标:去掉getter方法的synchronized修饰符

2、手段:

A)去掉setter方法

a) 提供构造器,将需要的字段作为参数(取代setter)

b) 在字段前添加final修饰符(表示不能使用setter)

B) 拆分getter方法

如果字段不具有不变性,也不能直接返回,需要拆分成若干具有不变性的字段(原子)返回。

比如有A类有一个int x, int y私有字段,都具有未经synchronized修饰的公开setter方法;

在B类中将A类作为一个字段,那么就不能直接返回 A getA(); 而应当是 int getX(); int getY()。

如果不这样做,严格意义上讲,A类就不具有不变性。从这点上讲,banq上面所写的ForumState并不具备严格的不变性。

有时候,我们不仅要设计只读的状态特征State,也要设计可写的实例状态StateBuilder。这两种对称的状态特征应当是可以相互转化的,比如ForumStateBuilder 有以ForumState作为参数的构造方法,ForumState有以ForumStateBuilder作为参数的构造方法。

三、OO、FP、LP与Concurrency

在纯函数式(FP)语言中,没有副作用,符号是不可变的。这个特性在OO语言中,可由不变性模式(Immutable pattern)实现。在逻辑式(LP)语言中,被称为“引用透明”。

Immutable Pattern是实现并发的诸多有效手段之一,Actor pattern也一样。但在并发语境中,OO相对于FP、LP这些语言,描述起来可能会比较笨拙一些。

4Go 1 2 3 4 下一页