目录:
一.什么样的方法被称作是有”副作用的”
二.如何减少方法的副作用
1.分离修改与查询。
2.将查询分离到一个value object
3 .demeter定律,也叫得墨忒耳定律,迪米特法则。
一,什么是副作用
为了方便下面的讨论,有必要对副作用做一个定义。
如果一个函数修改了对象的状态,无论这个修改操作是有意还是无意的,都被称作有副作用。
副作用意味着会出现预料之外的结果,这在调试和维护阶段将非常困难。我很懒,在将来调试和维护阶段,我想悠闲的喝着茶水,完成经理交付给我的任务,然后听着舒心的音乐,度过一天的时光。
为了减少方法的副作用,很多大师提出了一些优秀的设计原则,其中比较有价值的建议有meyer提出的“任何有返回值的函数,都不应该有看的到的副作用”,后来martin fowler 将其描述为“将修改于查询分离”。后来,Eric Evans提出了一个“Side Effect Free Function”概念。
和任何一个优秀的原则一样,我并不总是刻意的遵循他们,只有再他们能给我带来好处时,我才会去遵循他们。
接下来你需要了解的是,什么是查询函数与修改函数。
二,查询函数的好处
如果一个函数没有修改对象状态,仅仅只是返回一些数据,这样的方法被称为查询函数。譬如List<T>的Find函数,该函数只是在集合中查找并返回符合条件的值;再如一个Customer类中有一个GetCustomerID()函数,该函数只是简单的返回customer的ID值。像这样的函数都是查询函数。
查询函数的好处是你可以任意的调用它,而不用担心它会带来什么副作用,因为他没有修改对象的状态。同时查询函数的好处还在于它的结果是可以预测的,我们通过函数名就可以预测他的行为,而不用去了解的实现。
同样,你也不用担心查询函数的嵌套问题,一个查询函数可以调用另一个查询函数,你可以形成任意的嵌套深度,而不用担心这种深层的嵌套带来什么副作用。
查询函数的结果只有两种,要么找到符合结果的值,要么没有找到。他很少会抛出一个异常,遗憾的是很多类库中都抛出了异常。而一个修改函数就不同了,修改函数可能修改成功,也可能修改失败,并且失败的原因常常不只一种。如果想知道哪一种失败原因,你通常不得不去阅读该函数的源代码,在你无法获得源代码的情况下,你就必须去阅读相关的说明和文档。
正是由于查询函数的结果简单且可预测,因此他具有极好的可测试性。
一旦你体会到了查询函数带来的种种好处,你就会尝试在你的代码中写更多的查询函数,呵呵,我就是这样。
三 ,什么是修改函数
如果一个函数修改了对象的状态,并且没有返回”领域数据”,这样的函数被称作修改函数。譬如List<T>的Add(T)函数,该函数值是像集合中添加一个元素,并没有返回任何值。
请注意我并没有说,修改函数不能有返回值。我只说了修改函数不应该返回“领域数据”。
举个例子,请你思考一下C#类库中的List<T>的Remove<T>方法是否满足修改函数的条件。该函数的原型如下:
Public bool Remove<T>(T value);
该函数的意图很简单,删除成功就返回true,失败返回false。
怎样,你认为这个函数是否是一个修改函数?答案我就不说了,请你自己思考。
由于修改函数可以修改对象的状态,所以他常常在我们不知道的情况下,将对象的状态修改成非法状态,这是非常糟糕的。
修改函数的结果只有两种,一种是修改成功,一种是失败,在失败的情况下可能会抛出一个异常,因此你会在调用的写入try catch语句来捕获异常,毫无疑问,这时代码变得越来越复杂。
并且,函数失败的原因通常有多种,为了知道是哪一种原因导致函数操作失败,你必须去阅读该函数的源代码,以了解他的实现。在无法获得源代码的情况下,你只能去查看该函数的说明及文档,看看他们都会在什么情况下,抛出什么异常。
由于函数的失败原因有多种,所以在将来调试和维护的过程中将会带来非常大的麻烦——你不知道是什么原因导致函数的失败。
在一个深度嵌套的函数中,一个修改函数可能会调用另一个修改函数,这种嵌套可能会持续很久。这样的函数调试和维护更加困难。
毫无疑问,在系统中使用修改函数的使用是不可避免的,即使他有那么多的缺点。但是如果采取一些好的作法,依然可以使修改函数的副作用降至最低。譬如你可以将修改函数和查询函数放置不同的操作中,一个函数要么是修改函数,要么是查询函数,并且通过将查询函数分离到一个value object对象中,达到更好的效果。你也可以通过适当的遵循demeter定律,来减少方法的副作用。这就是接下来要讨论的内容。
四.分离修改与查询
如果一个函数既是修改函数,又是查询函数,既函数在修改对象状态的同时,又返回了领域数据。这样的方法你应当进行“将修改与查询分离”重构,通过把修改函数与查询函数放置两个不同的操作中,可以实现修改与查询的分离。
具体的重构手法可以参阅MartinFowler的《重构》一书。
五,将查询函数分离到一个ValueObject中
如果采取了上面的建议,把修改与查询放在不同的函数去,那么你就进步一大截了。如果你还知道把查询函数放在一个Value Object中,你的设计就会更进一步。这就是接下来要介绍的内容,最后会给出一段代码示例。
只有在查询函数非常复杂的情况下,,或者提取出一个value object概念对于我们的设计有好处时,我才建议你这么做。在绝大多数的时候你都不需要。
将复杂的查询函数提炼到一个Valueobject中,是有很多好处的。
先来看一下代码:
public classOrderAddress
{
stringprovince;
public string Province
{
get{ return province; }
}
stringcity;
public string City
{
get{ return city; }
}
stringarea;
public string Area
{
get{ return area; }
}
stringtown;
public string Town
{
get{ return town; }
}
stringzipCode;
public string ZipCode
{
get{ return zipCode; }
}
stringcontacterName;
public string ContacterName
{
get{ return contacterName; }
set{ contacterName = value; }
}
stringcontacterNumber;
public string ContacterNumber
{
get{ return contacterNumber; }
set{ contacterNumber = value; }
}
publicOrderAddress(string province,string city,string area,string town,stringzipCode,string contacterName,string contacterNumber)
{
this.province= province;
this.city= city;
this.area= area;
this.town= town;
this.zipCode= zipCode;
this.contacterName= contacterName;
this.contacterNumber= contacterNumber;
}
}
OrderAddress类有如下的属性,可以访问和修改收货人姓名和电话:
string contacterName;
public string ContacterName
{
get{ return contacterName; }
set{ contacterName = value; }
}
stringcontacterNumber;
public string ContacterNumber
{
get{ return contacterNumber; }
set{ contacterNumber = value; }
}
你可以一眼就看出get访问器就是我们所说的查询方法,因为他仅仅是返回一个值,没有修改对象的任何状态。
一旦产生了一个订单,用户只能修改联系人姓名和电话,而省,市等其他地址信息是不能修改的。同时,联系人姓名和电话一般都是同时变化的,修改联系人姓名可能也同时修改联系人电话。
这里我们将查询方法提炼到一个Value Object 对象中,如下:
public structConsignee
{
stringcontacterName;
public string ContacterName
{
get{ return contacterName; }
}
stringcontacterNumber;
public string ContacterNumber
{
get{ return contacterNumber; }
}
publicConsignee(string contacterName,string contacterNumber)
{
this.contacterName= contacterName;
this.contacterNumber= contacterNumber;
}
}
这就实现了我么所说的将查询分离到Value Object对象中。
四,demeter定律
网上有很多关于Demeter定律的博客,其中很多都是泛泛而谈,甚至有些博客还有错误。我并不想在这篇博客里介绍demeter定律。如果要详细的介绍他,需要一个单独写一个文章。
我只是想说,如果你懂得demeter定律,那么适当的采用它,你可以避免方法的副作用。