SOLID 是让软件设计更易于理解、更加灵活和更易于维护的五个原则的简称。
修改一个类的原因只能有一个
尽量让每个类只负责软件中的一个功能,并将该功能完全封装在该类中。
这条原则的主要目的是减少复杂度。将原本复杂的业务拆分到若干个清晰地方法中去,避免费尽心思构思代码实现复杂设计。
如果类负责的东西太多,那么当其中任何一个功能发生改变时,你都必须对类进行修改。而在进行修改时,你就有可能改动类中自己并不希望改动的部分。
对于扩展,类应该是“开放”的;对于修改,类则应是“封闭”的。
本原则地核心理念是在实现新功能时保持已有代码不变。
开放 :如果你可以对一个类进行扩展,可以创建它的子类并对其做任何事情(如新增方法或成员变量、重写基类行为等),那么它就是开放的。
封闭:如果某个类已经做好了充分的准备并可供其他类使用的话(即其接口已明确定义且以后不会修改),那么该类就是封闭(你可以称之为完整)的。
如果一个类已经完成开发、测试和审核工作,而且属于某个框架或者可被其他类的代码直接使用的话,对其代码进行修改就是有风险的。你可以创建一个子类并重写原始类的部分内容以完成不同的行为,而不是直接对原始类的代码进行修改。这样你既可以达成自己的目标,但同时又无需修改已有的原始类。
这条原则并不能应用于所有对类进行的修改中。如果你发现类中存在缺陷,直接对其进行修复即可,不要为它创建子类。子类不应该对其父类的问题负责。
当你扩展一个类时, 记住你应该要能在不修改客户端代码的情况下将子类的对象作为父类对象进行传递。
这意味着子类必须保持与父类行为的兼容。在重写一个方法时,你要对基类行为进行扩展,而不是将其完全替换。
里氏替换原则由一组对子类形式的要求构成:
假设某个类有个方法用于给狗子喂食:feed(Dog g)。客户端代码总是会将“狗(dog)”对象传递给该方法。
- 正确的子类:基于上述类创建了一个子类并重写了前面的方法,使其能够给任何“”“动物(animal,即‘狗’的超类)”喂食:feed(Animal d)。如果现在将该子类的对象而非超类的对象传递给客户端代码,程序仍能正常工作。
该方法可以用于给传递给任何动物喂食,因此它仍然可以用于给传递给客户端的任何“狗”喂食。- 不正确的子类:基于上述类创建了另一个子类且限制喂食方法仅接受“哈士奇(Husky,一个‘狗’的子类)”:feed(Husky d)。如果你用他来替代链接在某个对象中的原始类,客户端代码会由于该方法只能对特殊种类的狗进行喂食,而无法为传递给客户端的普通狗提供服务,从而将破坏所有相关的功能。
对于返回值类型的要求与对于参数类型的要求相反。
假设某个类中有一个方法 buyDog(): Dog 。 客户端代码执行该方法后的预期返回结果是任意类型的“狗”。
- 正确的子类:子类将该方法重写为: buyDog(): Husky 。客户端将获得一只“哈士奇”,自然它也是一只“狗”,因此一切正常。
- 不正确的子类:子类将该方法重写为: buyDog(): Animal 。现在客户端代码将会出错,因为它获得的是自己未知的动物种类(短吻鳄?熊?),不适用于为一只“狗”而设计的结构。
换句话说,异常类型必须与基础方法能抛出的异常或是其子类别相匹配。这条规则源于一个事实:客户端代码的 try-catch代码块针对的是基础方法可能抛出的异常类型。因此,预期之外的异常可能会穿透客户端的防御代码,从而使整个应用崩溃。
对于绝大部分现代编程语言, 特别是静态类型的编程语言(Java 和 C# 等等),已经内置上述三条规则。如果违反了这些规则,你将无法对程序进行编译。
例如,基类的方法有一个 int类型的参数。如果子类重写该方法时,要求传递给该方法的参数值必须为正数(如果该值为负则抛出异常),这就是加强了前置条件。客户端代码之前将负数传递给该方法时程序能够正常运行,但现在使用子类的对象时会使程序出错。
假如你的某个类中有个方法需要使用数据库,该方法应该在接收到返回值后关闭所有活跃的数据库连接。你创建了一个子类并对其进行了修改,使得数据库保持连接以便重用。但客户端可能对你的意图一无所知。由于它认为该方法会关闭所有的连接,因此可能会在调用该方法后就马上关闭程序,使得无用的数据库连接对系统造成“污染”。
这很可能是所有规则中最不“形式”的一条。不变量是让对象有意义的条件。例如,猫的不变量是有四条腿、一条尾巴和能够喵喵叫等。不变量让人疑惑的地方在于它们既可通过接口契约或方法内的一组断言来明确定义,又可暗含在特定的单元测试和客户代码预期中。不变量的规则是最容易违反的,因为你可能会误解或没有意识到一个复杂类中的所有不变量。因此,扩展一个类的最安全做法是引入新的成员变量和方法,而不要去招惹超类中已有的成员。当然在实际中,这并非总是可行。
什么?这难道可能吗?原来有些编程语言允许通过反射机制来访问类的私有成员。还有一些语言(Python 和 JavaScript)没有对私有成员进行任何保护。
客户端不应被强迫依赖于其不使用的方法
尽量缩小接口的范围,使得客户端的类不实现其不需要的行为。
根据接口隔离原则,你必须将“臃肿”的方法拆分为多个颗粒度更小的具体方法。客户端必须仅实现其实际需要的方法。否则,对于“臃肿”接口的修改可能会导致程序出错,即使客户端根本没有使用修改后的方法。
继承只允许类拥有一个超类,但是它并不限制类可同时实现的接口的数量。因此,你不需要将大量无关的类塞进单个接口。你可将其拆分为更精细的接口,如有需要可在单个类中实现所有接口,某些类也可只实现其中的一个接口。
高层次的类不应该依赖于低层次的类。 两者都应该依赖于抽象接口。抽象接口不应依赖于具体实现。具体实现应该依赖于抽象接口。
有时人们会先设计低层次的类,然后才会开发高层次的类。当你在新系统上开发原型产品时,这种情况很常见。由于低层次的东西还没有实现或不确定,你甚至无法确定高层次类能实现哪些功能。如果采用这种方式,业务逻辑类可能会更依赖于低层原语类。
依赖倒置原则建议改变这种依赖方式。
依赖倒置原则通常和开闭原则共同发挥作用:你无需修改已有类就能用不同的业务逻辑类扩展低层次的类。