被遗忘的设计模式——1.不变模式(Immutable)

本系列目录
代码示例下载

变模式,这是一个非常基础的模式。

从对象的健壮性来思考:
这些对象共享相同对象的引用,为此,在对象构造好之后,不允许改变共享对象的内容。这就是不变模式,在程序中使用它的地方越合适,程序将越健壮,可维护性就越强。

从并发的角度来思考:
不变模式解决的是如何处理共享对象的问题。当多个对象使用或修改同一个类实例——也就是共享对象的时候,往往会有同步问题;而在多线程异步调用的情况下,则更难以控制这个共享对象的状态。
一种解决办法是,使用了Lock的机制来锁住对象,强制进行同步,但是会增加额外的开销。所以,能避免尽量避免使用Lock。故而,有另一种解决方案:即新建一个有着不同内容的对象,来替换原对象,其中,旧对象中未改变的值会复制到新对象中,而改变的值会使用新值——这种实现方式就称为不变模式。

Flyweight模式一般使用不变模式来实现享元,参见我的《我也设计模式》的Flyweight一章。

关键是这个基础类的设计,让我们考察一下复数类Plural的加法:



注意到,x和y字段都是私有且只读的,不对外暴露,它们相应的属性也是只读的:
        private readonly double x;
        
private readonly double y;

        
public double X
        
{
            
get return x; }
        }


        
public double Y
        
{
            
get return y; }
        }
 

我们只能在构造函数中初始化这两个字段,以后不能再进行修改——这也是“不变”一词的由来:
        public Plural(double x, double y)
        
{
            
this.x = x;
            
this.y = y;
        }

需要指出的是,对于我们这个复数类的加减乘除运算,一般使用运算符重载的技术,其实,也可以使用不变模式来实现,逻辑是:每个复数实例都是不可改变的,在加减乘除运算方法中,创建新的实例,并在其构造函数中设定新的参数,如下面的Add方法:
        public Plural Add(Plural P)
        
{
            
return new Plural(this.x + P.x, this.y + P.y);
        }

对于基础类Plural,确实没有暴露他自身的属性,但是,他的子类却有可能将其暴露出来,所以,一般要把这个基础类设为不可派生的:
    public sealed class Plural

不变模式在.NET中最广泛的使用就是String类的实现。我们知道,字符串是不可改变的。对于以下操作:
            String s = "Jax";
            s 
+= " Bao";
            s 
= s.Substring(3);

无论是ToUpper还是Substring等函数,都是新建了一个字符串,并重新将该字符串的引用添加到原来的变量s上。另外运算符重载+=,也是这么实现的。


几点注意:
1.构造函数是不需要同步控制的。对CLR认识深刻的人,会认为这是一句废话。毕竟,new还没做好呢,又怎么能Lock一个对象,hoho。
2.仔细观察上面的Add方法:
    P.x为什么可以使用呢?x不是P对象的一个私有成员么?至少一开始我是这么认为的,但是编译期和runtime却运行良好。
    OK,在Position类外部,确实是访问不了Position对象的x私有字段,为此需要公开其只读属性;但是,就在Position类内部,p作为Add方法的一个参数,却可以访问它自身的这个私有对象。重温private的定义:完全私有的,只有当前类中的成员能访问到。以上是我暂时能想到的一些思路,希望大家能给我更合理的解释。

下一篇:过滤器模式(Filter)

posted @ 2008-06-30 14:07 包建强 阅读(2201) 评论(16)  编辑 收藏 所属分类: Design Patterns

  回复  引用  查看    
#1楼 2008-06-30 14:52 | 杨同学      
immutable 不算是一种设计模式吧,博主
  回复  引用  查看    
#2楼 [楼主]2008-06-30 14:59 | 包建强      
@杨同学
何以见得?难道不在GOF中的,都不算么?
  回复  引用  查看    
#3楼 2008-06-30 15:05 | taowen      
再深入谈一下什么时候适用immutable吧.
  回复  引用  查看    
#4楼 2008-06-30 15:41 | U2U      
这个模式太耗资源了吧
  回复  引用  查看    
#5楼 2008-06-30 15:43 | Allen Lee      
说到“不变性”(immutability),就不能不提函数式编程了,在诸如F#的函数式编程语言里,对象默认就是“不可变的”(immutable)。在F#里,上面的Plural类可以这样写:

type Plural =
    { X: double; Y: double }
    member this.Add p = { X = this.X + p.X; Y = this.Y + p.Y }

Plural的对象实例一旦创建完毕就不能修改了:

let p1 = { X = 2.1; Y = 3.2 }
  回复  引用    
#6楼 2008-06-30 15:48 | 未登录的包包 [未注册用户]
小A,
你帮我仔细分析一下我在文章下面提出的问题吧——注意2。我至今还找不到一个合理的解释。多谢多谢!
  回复  引用  查看    
#7楼 2008-06-30 16:16 | Allen Lee      
@未登录的包包
这个问题曾经在 http://space.cnblogs.com/group/topic/1919/ 提出过,脑袋在那里的4楼给出了解释。
  回复  引用    
#8楼 2008-06-30 16:33 | 未登录的包包 [未注册用户]
@小A
多谢,已经看到。
类的私有成员只能从位于该类内部的【词法环境】中访问。至于是否真正是类内部发生的访问是无法限制的。因此,一个对象的私有成员是可以被别的对象访问的,只要位于该类内部的词法环境中。

  回复  引用  查看    
#9楼 2008-06-30 16:56 | Gray Zhang      
我记得private是“只能由本类的对象访问”而不是“只能有对象自身访问”,因此在类中访问同类型的另一对象的private字段是可以的
  回复  引用  查看    
#10楼 2008-06-30 16:57 | 杨同学      
严格的说Immutable 应该不算是设计模式。
参照维基百科: http://en.wikipedia.org/wiki/Design_pattern_(computer_science)


1. 把类设计成不变的还有一个好处就是有利于提高性能上。特别对于体积比较小并且被重复创建多个实例的类。

2. 如果一个物体是不可修改的, 那么就无需监听 PropertyChanged 事件。 在数据绑定方面有利于精简代码清晰逻辑。

假设Survey类中有这么一个DepthUnit 属性:
class Survey : INotifiedPropertyChanged
{
public Unit DepthUnit
{
get{ return this.depthUnit;}
set{ this.depthUnit = value;
PropertyChanged(this,new PropertyChangedEventArgs("DepthUnit"));

}
}

如果Unit 类不是 immutable 的, 有两种方式篡改这个属性。

-通过属性赋值:
Unit meter = new Unit("m");
Survey.DepthUnit =meter;

-通过修改Unit实例: Survey.DepthUnit.Name = "m";

由于第二种方式的存在。想监听 DepthUnit 属性变化这个事件就需要使 Unit 类实现 INotifiedPropertyChanged 接口。 在DepthUnit 中注册并监听 Unit类的 Name 属性事件,再而触发 Survey 类中的 DepthUnit 属性改变事件。 在每一个有Unit类作为属性的类里面都要使用同样的方法, 代码变得冗长并且很乱。

目前最简单的方法就是吧Unit的 Name属性改成只读的。 这样第二种篡改的方式将不再存在,一切简单了很多。

不清楚这个算不算是immutable 类的一个好处... , 或者对这个问题有想法的请赐教.....
  回复  引用  查看    
#11楼 2008-06-30 23:46 | 笑笑江南      
这个是设计模式?
  回复  引用  查看    
#12楼 2008-07-01 00:09 | Angel Lucifer      
/* 故而,有另一种解决方案:即新建一个有着不同内容的对象,来替换原对象,其中,旧对象中未改变的值会复制到新对象中,而改变的值会使用新值——这种实现方式就称为不变模式。*/

Immutable 的对象对于并发来说因为不需要同步,从这点上看的确有性能上的提升。

但是也应当注意到,这种场景有限。

同时,在高负载,高并发的情况下,如4楼 U2U 老兄所说,非常耗费资源。这种情况下,垃圾对象频繁创建,导致 GC 频繁进行回收。.NET 新建一个 Heap 对象速度非常快(但稍慢于在栈上分配对象),但是回收起来却很麻烦。还不如 Copy on Write好用。
  回复  引用  查看    
#13楼 2008-07-01 00:36 | 曲滨*銘龘鶽      
@杨同学
设计模式的定义是什么?你知道吗?

估计圆子里能说清楚的不多、呵呵?
  回复  引用  查看    
#14楼 2008-07-01 01:01 | 杨同学      
@曲滨*銘龘鶽

定义在维基百科里面有, 我给了连接的,你去看看吧。
  回复  引用  查看    
#15楼 2008-07-01 06:20 | 怪怪      
稍微有点偏重于实现模式, 而不是设计, 不过这种思路还是有很多用武之地的~
  回复  引用  查看    
#16楼 2008-07-01 14:38 | 装配脑袋      
VB的匿名类,凡是标注Key的属性都是不变属性;而C#的匿名类则必须就是不变类型。比如
Dim a = New {Key .Name = "Harry", Key .Age = 12 } '不变的说

Dim b = New {.Name = "Potter", .Age = 14 } '可变的说

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-06-30 14:17 编辑过


相关链接: