在企业级软件开发中,代码的复杂性是一个主要问题。代码的可读性应该是在构建软件项目时首先尝试实现的目标。没有它,将无法对软件的正确性做出合格的判断,或者至少对其的推理能力将显著降低。
可变对象是增加还是减少了代码的可读性?让来看一个例子:
var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10);
IReadOnlyCollection<Customer> customers = Search(queryObject);
if (customers.Count == 0)
AdjustSearchCriteria(queryObject, name);
Search(queryObject);
这里,不能确定queryObject
是否在第二次搜索客户时被更改了。这取决于第一次是否找到了任何东西,以及AdjustSearchCriteria
方法是否更改了标准。要找出确切发生了什么,需要查看AdjustSearchCriteria
方法的代码。不能仅通过查看方法签名就确定。
现在,让比较以下代码:
var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10);
IReadOnlyCollection<Customer> customers = Search(queryObject);
if (customers.Count == 0)
{
QueryObject<Customer> newQueryObject = AdjustSearchCriteria(queryObject, name);
Search(newQueryObject);
}
现在很明显,AdjustSearchCriteria
方法创建了新的标准,用于执行新的搜索。
如果不确定数据是否被更改,那么很难对代码进行推理。如果需要查看它调用的方法而不仅仅是方法本身,那么跟踪流程将变得困难。如果正在构建一个多线程应用程序,跟踪和调试代码将变得更加困难。
如果有一个相对简单的类,应该总是考虑使其不可变。这个经验法则与Value Objects
的概念相关:值对象是简单的,并且很容易变得不可变。
那么,如何构建不可变类型呢?让以一个名为ProductPile
的类为例,它代表出售的一堆产品:
public class ProductPile
{
public string ProductName { get; set; }
public int Amount { get; set; }
public decimal Price { get; set; }
}
要使其不可变,需要将其属性标记为只读,并创建一个构造函数:
public class ProductPile
{
public string ProductName { get; private set; }
public int Amount { get; private set; }
public decimal Price { get; private set; }
public ProductPile(string productName, int amount, decimal price)
{
Contracts.Require(!string.IsNullOrWhiteSpace(productName));
Contracts.Require(amount >= 0);
Contracts.Require(price > 0);
ProductName = productName;
Amount = amount;
Price = price;
}
}
假设需要在销售一件商品时将产品数量减少一个。不应该更改现有对象,而应该基于当前对象创建一个新的对象:
public class ProductPile
{
public string ProductName { get; private set; }
public int Amount { get; private set; }
public decimal Price { get; private set; }
public ProductPile(string productName, int amount, decimal price)
{
Contracts.Require(!string.IsNullOrWhiteSpace(productName));
Contracts.Require(amount >= 0);
Contracts.Require(price > 0);
ProductName = productName;
Amount = amount;
Price = price;
}
public ProductPile SubtractOne()
{
return new ProductPile(ProductName, Amount - 1, Price);
}
}
这里得到了什么?有了不可变类,只需要在构造函数中验证一次它的代码契约。绝对确定对象总是处于正确的状态。对象自动是线程安全的。代码的可读性提高了,因为没有必要进入方法以确保它们不会更改任何东西。
当然,一切都有代价。虽然小型和简单的类最受益于不可变性,但这种方法并不总是适用于较大的类。
首先,与性能问题相关。如果对象相当大,每次更改都需要创建它的副本可能会影响应用程序的性能。
一个好例子是不可变集合。它们的作者考虑到了潜在的性能问题,并添加了一个Builder
类,允许修改集合。准备完成后,可以将其转换为不可变集合:
var builder = ImmutableList.CreateBuilder<string>();
builder.Add("1");
ImmutableList<string> list = builder.ToImmutable();
ImmutableList<string> list2 = list.Add("2");
另一个问题是,有些类本质上是可变的,试图使它们不可变会带来更多的问题而不是解决。
但不要因为这些问题而阻止创建不可变数据类型。考虑每个设计决策的利弊,并始终考虑常识。