NingG +

面向对象设计原则

SOLID是面向对象设计和编程(OOD&OOP)中的重要编码原则:

SRP The Single Responsibility Principle 单一责任 一个类,有且只有一个更改的原因
OCP The Open Closed Principle 开放封闭 不能修改类,可以扩展类
LSP The Liskov Substitution Principle 里氏替换 子类,可以替换基类
ISP The Interface Segregation Principle 接口分离 细粒度的接口
DIP The Dependency Inversion Principle 依赖反转 依赖抽象而不是具体实现

额外说几点:

代码腐坏的几个现象:

代码变质,有几个现象:

一个软件项目,满足当前功能,是最基本的要求;而实际上,良好实现的软件项目,能在满足当前功能基础上,满足未来二次开发、维护的需要。软件项目的完整成本分为:开发成本、二次开发成本、维护成本。

单一责任(Single Responsibility)

一个类只做一种类型的责任,当需要承担其他类型责任时,分解这个类。

下面的代码有多少职责?

class Employee {
  public Pay calculatePay() {...}
  public void save() {...}
  public String describeEmployee() {...}
}  

正确答案是3个。

在一个类中混合了:

如果你将多个职责结合在一个类中,可能很难实现修改一部分时不会破坏其他部分。混合职责也使这个类难以理解,测试,降低了内聚性。修改它的最简单方法是将这个类分割为三个不同的相互分离的类,每个类仅仅有一个职责:数据库访问,支付计算和描述。

开放封闭(Open Closed)

对继承、扩展,是开放的;对修改,是封闭的。使用抽象类和接口。

为依赖关系使用接口的另一个作用是减少耦合和增加灵活性。

void checkOut(Receipt receipt) {
  Money total = Money.zero;
  for (item : items) {
	total += item.getPrice();
	receipt.addItem(item);
  }
  Payment p = acceptCash(total);
  receipt.addPayment(p);
}

那么增加信用卡支持该怎么做?你可能像下面的增加if语句,但这违反OCP原则。

Payment p;
if (credit)
  p = acceptCredit(total);
else
  p = acceptCash(total);
receipt.addPayment(p);

更好的解决方案是:

public interface PaymentMethod {void acceptPayment(Money total);}
  
void checkOut(Receipt receipt, PaymentMethod pm) {
  Money total = Money.zero;
  for (item : items) {
	total += item.getPrice();
	receipt.addItem(item);
  }
  Payment p = pm.acceptPayment(total);
  receipt.addPayment(p);
}

这儿有一个小秘密:OCP仅仅用于未来变化可预见的情况,当未来变化发生时,采用OCP。因此,需要准确地预见将来的变化。 这意味着等待用户做出改变,然后使用抽象应对将来的类似变化。

个人理解:通过抽象类、接口,实现,对扩展、新增开放,对修改关闭。

里氏替换(Liskov Substitution)

子类的对象实例,应该能够替换任何其超类的实例,即,子类必须符合父类的预期行为(隐含的行为约束)

下面几种情况,违反了LSP原则:

一个违反LSP的典型例子是Square类派生于Rectangle类。Square类总是假定宽度与高度相等。如果一个正方形对象用于期望一个长方形的上下文中,可能会出现意外行为,因为一个正方形的宽高不能(或者说不应该)被独立修改。

解决这个问题并不容易:如果修改Square类的setter方法,使它们保持正方形不变(即保持宽高相等),那么这些方法将弱化(违反)Rectangle类setter方法,在长方形中宽高可以单独修改。

public class Rectangle {
  private double height;
  private double width;
  
  public double area();
  
  public void setHeight(double height);
  public void setWidth(double width);
}

以上代码违反了LSP。

public class Square extends Rectangle {  
  public void setHeight(double height) {
	super.setHeight(height);
	super.setWidth(height);
  }
  
  public void setWidth(double width) {
	setHeight(width);
  }
}

违反LSP导致不明确的行为。不明确的行为意味着它在开发过程中运行良好但在产品中出现问题,或者要花费几个星期调试每天只出现一次的bug,或者不得不查阅数百兆日志找出什么地方发生错误。

个人理解:子类可以替换父类。

接口分离(Interface Segregation)

不能强迫用户去依赖那些他们不适用的接口。换句话说,使用多个专门的接口,比使用单一的总接口要好。

想象一个ATM取款机,通过一个屏幕显示我们想要的不同信息。你会如何解决显示不同信息的问题?我们使用SRP,OCP和LSP想出一个方案,但是这个系统仍然很难维护。这是为什么?

想象ATM的所有者想要添加仅在取款功能出现的一条信息,“ATM机将在您取款时收取一些费用,您同意吗”。你会如何解决?

可能你会给Messenger接口增加一个方法并使用这个方法完成。但是这会导致重新编译这个接口的所有使用者,几乎所有的系统需要重新部署,这直接违反了OCP。让代码腐坏开始了!

这里出现了这样的情形:对于取款功能的改变导致其他全部非相关功能也变化,我们现在知道这并不是我们想要的。这是怎么回事?

其实,这里是向后依赖在作怪,使用了该Messenger接口每个功能依赖了它不需要,但是被其他功能需要的方法,这正是我们想要避免的。

public interface Messenger {
  askForCard();
  tellInvalidCard();
  askForPin();
  tellInvalidPin();
  tellCardWasSiezed();
  askForAccount();
  tellNotEnoughMoneyInAccount();
  tellAmountDeposited();
  tellBalance();
}

相反,将Messenger接口分割,不同的ATM功能依赖于分离的Messenger。

public interface LoginMessenger {
  askForCard();
  tellInvalidCard();
  askForPin();
  tellInvalidPin(); 
}
  
public interface WithdrawalMessenger {
  tellNotEnoughMoneyInAccount();
  askForFeeConfirmation();
}
  
publc class EnglishMessenger implements LoginMessenger, WithdrawalMessenger {
  ...   
}

个人理解:接口功能单一,多接口之间隔离;避免单一的总接口。

依赖反转(Dependency Inversion)

例子:一个程序依赖于Reader和Writer接口,Keyboard和Printer作为依赖于这些抽象的细节实现了这些接口。CharCopier是依赖于Reader和Writer实现类的低层细节,可以传入任何实现了Reader和Writer接口的设备正确地工作。

public interface Reader { char getchar(); }
public interface Writer { void putchar(char c)}
  
class CharCopier {
  
  void copy(Reader reader, Writer writer) {
	int c;
	while ((c = reader.getchar()) != EOF) {
	  writer.putchar();
	}
  }
}
  
public Keyboard implements Reader {...}
public Printer implements Writer {...}

个人理解:通过接口,实现上下层之间的隔离。

参考来源

Top