软件设计中的SOLID原则

简介

面向对象软件设计中,如何使软件易读、易维护和灵活适应需求是一个永恒的话题。SOLID原则即为大家在概念上指明了我们应如何达到这些设计目标。相较于设计模式,SOLID是高层次的抽象思想指导,可以说具体的设计模式是SOLID原则的实现。

在具体讨论这些原则之前,需要说明SOLID并不是软件设计中必须遵从的铁律,落地的软件开发还需要根据实际需求进行折衷与取舍。在我看来,软件架构与设计并不完全是技术,而更像是艺术。一个巧夺天工的设计常令人拍案叫绝,生搬硬套地应用所谓设计原则只能是弄巧成拙。

SOLID原则

Single Responsibility Principle

单一职责原则。顾名思义,一个类只做一件事情。此原则与Unix Philosophy中"Make each program do one thing well"有异曲同工之妙。一个类的单一职责其实是把整个系统功能有更为细粒度的切分,从而更易于复用和解耦。举例说明,设计一个数据展示程序,基本功能如下:

  • 从DB查询数据
  • 数据聚合加工
  • 数据展示

根据单一职责原则,可将此三个功能分别写一个类:DbAccessor, DataProcessor, Displayer。如果将来再有显示数据的需求,则可直接复用Displayer。反之,如果将三个功能写到一个类中,则将此类聚合到其他类中会产生冗余,因为调用者可能只需要DB查询功能。此外,如果三个功能中的任何一个需求改变,都可能需要对整个类进行重写。

此原则看起来十分简单,而实际应用却并不容易。继续以上的例子,DbAccessor是否需要进一步拆分?如DB连接器是否需要进一步抽象?答案并不唯一,应根据实际和将来可能的需求决定。比如将来有可能从Ado.Net切换为Entity Framework,则进一步的抽象是有意义的,反之可能就是过度设计。

Open/Closed Principle

开闭原则。指的是类对于扩展开放,而对于修改关闭。即类允许扩展,而不允许修改。这种思想源自于封装,一个封装好的类,调用者看起来像是一个黑盒,如果有新的需求,则继承或聚合它进行扩展是最好的选择,直接对此类进行修改则不是一个优雅的方案。

例如一个List类支持Sort方法,但它会返回一个新的排好序的List,为了使它支持在原List中排序,则可根据开闭原则对原List继承,并扩展实现InplaceSort方法。

Liskov Substitution Principle

里式替换原则。一个类的父类总是可以被它的子类替换,而不影响原有程序的正确性。其实就是子类可以继承父类的方法,但不建议直接重写父类的接口,以免产生不可预料的错误。

Interface Segregation Principle

接口分离原则。具体的接口定义总是优于普适的接口定义。假设类A与类B都实现了同一个接口I,完成了功能F1,现在B需要增加一个功能F2。如果在接口I中增加F2,则类A需要实现一个它并不需要的功能F2。更好的办法是设计两个接口I1和I2分别实现F1与F2,类A仅实现I1,类B实现I1与I2。

接口的改变常常影响巨大,所以更好的方案是再创建一个新的接口让需要的类实现它,而非改变已有的接口。

Dependency Inversion Principle

依赖倒置原则。程序依赖于抽象,而不要依赖于具体实现。如果说开闭原则指出了面向对象设计的目标,则依赖倒置原则就是达到目标主要的机制。类与类之间的耦合都通过接口完成,而非具体实现。

总结

软件架构与设计是门艺术。Under-design与over-design之间的把握并非易事。SOLID原则只是阐述了一个设计得很好的软件应该符合的原则,但实际应用中还须根据需求变通,灵活运用。

References

SOLID