One way to design Message model

在一般的企业级应用中,Message是一个很常见的应用,其主要目的是可以让系统内的用户(包括系统用户)可以交流(包括attachment)。

如何去设计一个Message  BE(Business Model)模型?

首先会想到一种简单的做法:

class Message
	attr_accessor(:subject, :content, :readDate,:sender,:isReaded,:recipients,:attachments)
end

这种做法的好处就是简单明了,将所有信息全部都放在了Message 上,检索数据的SQL语句也会写的比较简单。但是这样的设计在系统使用一段时间就会发现系统的performance越来越差!究其原因,是因为每条Message在发出去时都会复制给所有的recipients,造成Message entity膨胀过快,导致performance 降低。

那我们改进的方法就是要避免message的过度增长,分析上面的模型,我们发现如果将isReaded、readDate和recipients抽成一个新entity,似乎就可以避免Message的复制。如下图示:

其中Recipient 中存储消息的接收人,这样在s1发送一条消息M发给r1,r2.r3时,只会产生如下Entity:

 One Sender,

 One Message,

Three Recipient

(其他entity可以暂时忽略掉)

如果sender或者recipient 要删除自己的message时,只需要将自身相应的IsDeleted设置为true即可(想想为什么不能直接删除entity?:)

模型的主要设计思路到此为止。

关于当前项目中出现的一些特殊需求,比如:

Kayla给自己的tax contact发的消息应该自动被kayla所属client的所有tax contacts共享。

这个其实只需要在发送邮件时自动bcc给其他tax contacts 就OK了。

Posted by Leo 2011年5月15日 03:50


Tell Me.Don't Ask!

先看一个简单的例子,假设有这样一种场景:我们需要比较有一个有大小和单位的Length对象的相等性。

通常会这样做:

1、定义Length对象.

 

public class Length
    {
        public int Value { get; private set; }
        public Unit Unit { get; private set; }

        public Length(int count, Unit unit)
        {
            Value = count;
            Unit = unit;
        }
    }

其中Unit是一个表示单位的Enum

2.override Equal 方法.

 

public override bool Equals(object obj)
        {
            if (obj is Length)
            {
                var length = obj as Length;
                if (IsEqual(length)) return true;
            }
            return false;
        }

        private bool IsEqual(Length thatLength)
        {
            var thatBaseLength = ConvertFactory.ConvertToBaseUnit(thatLength);
            var thisBaseLength = ConvertFactory.ConvertToBaseUnit(this);
            return thatBaseLength.Value == thisBaseLength.Value;
        }
   

其中ConvertFactory的作用是把当前/待比较Length对象转换成一个基准单位(Unit)的Length对象工场。来看实现:

 

public class ConvertFactory
    {
        private ConvertFactory(){}

        public static Length ConvertToBaseUnit(Length length)
        {
            IConvert convert = null;
            if (length.Unit == Unit.M)
            {
                convert = new MConvertToCM();
            }
            if(length.Unit == Unit.DM)
            {
                convert = new DMConvertToCM();
            }
           ...................
            return convert.ConvertToBase(length);
        }
    }

代码很清晰的告诉了我们单位是M或者DM的Length对象都将会被转换成为单位是M的对象。随便看一个MConvertToCM的实现:

 

public class MConvertToCM : IConvert
    {
        public Length ConvertToBase(Length length)
        {
            return new Length(length.Value*100, Unit.CM);
        }
    }

很好,完毕了。我们不仅实现了场景的功能,而且还在其中使用了多种OO技术与设计模式。似乎做的很完美(至少我第一次写出这样的代码时是这种感觉:)

但是,让我们再回头看看Length对象,两个属性都是public getter,

-------------Q&A------------------------------

为什么要公开这两个属性呢?因为我们在MConvertToCM里会用到啊!

为什么非要在MConvertToCM里用这两个属性?.......我们就这样设计的啊!

为什么要这样设计?........无语了

-------------Q&A------------------------------

好吧,问题似乎陷入了僵局,为什么我们非要把自己的属性直接暴露给其他对象?我们如果不这样做又可以有什么办法达到将自身的数据传递给其他对象呢?

参见Tell,Don't Ask这篇文章

文章中提到如果将一个对象的数据暴露出来的目的是为了在外部改变这个对象的数据/行为的做法是不合适的。

回到我们这个场景来看,Value与Unit公开的目的是为了在构建另一个自己,看清楚了这点,后面的操作也就变得比较容易了。直接上代码

 

 public interface LengthAndUnitAction
    {
        void action(int value, Unit unit);
    }

 

public class Length
    {
        private int Value { get; set; }
        private Unit Unit { get; set; }

        public Length(int count, Unit unit)
        {
            Value = count;
            Unit = unit;
        }
        .......................
        public void Execute(LengthAndUnitAction action)
        {
            action.action(Value, Unit);
        }
    }

我们把行为单独抽成一个接口,然后让Length类再调用接口的同时将内部属性传递出去。这样Value/Unit就可以变为私有变量,从而更好的起到了数据封装的作用。

Posted by Leo 2011年5月08日 01:56