在软件开发中,经常听到“让代码更健壮”的建议。那么,究竟什么是健壮的代码呢?SOLID原则是五个面向对象设计原则的集合,旨在提高代码的可维护性、可扩展性和灵活性。本文将探讨SOLID原则中的两个关键原则:依赖倒置原则和单一职责/开闭原则,并展示它们在实际编程中的应用。
依赖倒置原则的核心思想是,高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这个原则对于单元测试尤为重要,因为它允许模拟和控制底层模块的行为。
考虑以下代码示例:
public class DateBasedTaxFactory : ITaxFactory {
Customer _customer;
ITaxFactory _taxFactory;
public DateBasedTaxFactory(Customer c, ITaxFactory cb) {
_customer = c;
_taxFactory = cb;
}
public ITax GetTaxObject() {
if (_customer.StateCode == "TX" && DateTime.Now.Month == 4 && DateTime.Now.Day == 4) {
return new NoTax();
} else {
return _taxFactory.GetTaxObject();
}
}
}
在这个例子中,DateBasedTaxFactory
类直接依赖于DateTime.Now
属性来确定日期。这使得在不改变系统时间的情况下进行单元测试变得困难。为了解决这个问题,可以引入依赖注入,让外部决定类的依赖项。
public class DateBasedTaxFactory : ITaxFactory {
Customer _customer;
ITaxFactory _taxFactory;
DateTime _dt;
public DateBasedTaxFactory(Customer c, ITaxFactory cb, DateTime dt) {
_customer = c;
_taxFactory = cb;
_dt = dt;
}
public ITax GetTaxObject() {
if (_customer.StateCode == "TX" && _dt.Month == 4 && _dt.Day == 4) {
return new NoTax();
} else {
return _taxFactory.GetTaxObject();
}
}
}
通过这种方式,可以在测试中传递任意日期,从而验证DateBasedTaxFactory
类的行为。
单一职责原则指出,一个类应该只有一个引起它变化的原因。开闭原则则表明,软件实体应当对扩展开放,对修改封闭。这意味着应该设计出易于扩展但不需要修改现有代码的系统。
考虑以下订单计算总金额的代码:
public class Order {
List _orderItems = new List();
public decimal CalculateTotal(Customer customer) {
decimal total = _orderItems.Sum(item => item.Cost * item.Quantity);
decimal tax;
if (customer.StateCode == "TX") {
tax = total * .08m;
} else if (customer.StateCode == "FL") {
tax = total * .09m;
} else {
tax = .03m;
}
total = total + tax;
return total;
}
}
如果税率逻辑发生变化,或者需要添加新的州税率,就需要修改Order
类。这可能会导致需要重新测试和发布整个BusinessLogic.dll
,这在实际操作中是非常不方便的。为了解决这个问题,可以将税率逻辑抽象到单独的类中。
public interface ITax {
decimal CalculateTax(decimal total);
}
public class TXTax : ITax {
public decimal CalculateTax(decimal total) {
return total * .08m;
}
}
public class CustomerBasedTaxFactory : ITaxFactory {
Customer _customer;
static Dictionary stateTaxObjects = new Dictionary();
static Dictionary countyTaxObjects = new Dictionary();
public CustomerBasedTaxFactory(Customer customer) {
_customer = customer;
}
public ITax GetTaxObject() {
ITax tax;
if (!string.IsNullOrEmpty(_customer.County)) {
if (!countyTaxObjects.Keys.Contains(_customer.StateCode)) {
tax = (ITax)Activator.CreateInstance("Tax", "solid.taxes." + _customer.County + "CountyTax");
countyTaxObjects.Add(_customer.StateCode, tax);
} else {
tax = countyTaxObjects[_customer.StateCode];
}
} else {
if (!stateTaxObjects.Keys.Contains(_customer.StateCode)) {
tax = (ITax)Activator.CreateInstance("Tax", "solid.taxes." + _customer.StateCode + "Tax");
stateTaxObjects.Add(_customer.StateCode, tax);
} else {
tax = stateTaxObjects[_customer.StateCode];
}
}
return tax;
}
}