开发者都应该了解的SOLID原则(下)

2019年06月22日 由 sunlei 发表 879811 0
上一次的文章中我们讲到了开发者都应该了解的SOLID原则中的S和O原则,即:单一功能原则和开闭原则,今天我们继续来了解一下其他三个原则。

前文回顾:开发者都应该了解的SOLID原则(上)

里氏替换原则 Liskov Substitution Principle


A sub-class must be substitutable for its super-class(子类一定能用父级类替换)

这个原则的目的是确定一个子类可以毫无错误地代替父类的位置。如果代码会检查自己类的类型,它一定违反了这个原则。

继续Animal例子。
//...
function AnimalLegCount(a: Array) {
for(int i = 0; i <= a.length; i++) {
if(typeof a[i] == Lion)
log(LionLegCount(a[i]));
if(typeof a[i] == Mouse)
log(MouseLegCount(a[i]));
if(typeof a[i] == Snake)
log(SnakeLegCount(a[i]));
}
}
AnimalLegCount(animals);

这已经违反了里氏替换(也违反了OCP原则)。它必须知道每个Animal的类型并且调用leg-conunting相关(计算动物腿数)的方法。

每当新增一种动物,这个函数都需要做出修改来适应。
//...
class Pigeon extends Animal {

}
const animals[]: Array = [
//...,
new Pigeon();
]
function AnimalLegCount(a: Array) {
for(int i = 0; i <= a.length; i++) {
if(typeof a[i] == Lion)
log(LionLegCount(a[i]));
if(typeof a[i] == Mouse)
log(MouseLegCount(a[i]));
if(typeof a[i] == Snake)
log(SnakeLegCount(a[i]));
if(typeof a[i] == Pigeon)
log(PigeonLegCount(a[i]));
}
}
AnimalLegCount(animals);

要使这个函数符合LSP,需要遵循Steven Fenton 提出的以下要求:

  • 如果父类(Animal)有一个接受父类类型(Animal)的参数的方法,它的子类(Pigeon)应该接受一个父类类型(Animal)或子类类型(Pigeon)作为参数

  • 如果父类返回一个父类类型(Animal),其子类应当返回一个父类类型(Animal)或子类类型(Pigeon)。


现在来重新实现AnimalLegCount函数:
function AnimalLegCount(a: Array) {
for(let i = 0; i <= a.length; i++) {
a[i].LegCount();
}
}
AnimalLegCount(animals);

AnimalLegCount函数现在更少关心传递的Animal的类型,它只是调用LegCount方法。它只知道传入的参数必须是Animal类型,无论是Animal类型还是他的子类。

Animal类型现在需要实现/定义一个LegCount方法:
class Animal {
//...
LegCount();
}

然后它的子类就需要实现LegCount方法:
//...
class Lion extends Animal{
//...
LegCount() {
//...
}
}
//...

当它被传递给AnimalLegCount函数时,他将返回一头狮子的腿数。

可见AnimalLegCount函数不需要知道Animal的具体类型,只需要调用Animal类的LegCount方法,因为按约定Animal类的子类都必须实现LegCount函数。

接口分离原则 Interface Segregation Principle


Make fine grained interfaces that are client specific
为特定客户制作细粒度的接口
Clients should not be forced to depend upon interfacees that they do not use
客户应当不会被迫以来他们不会使用的接口

这条原则用于处理实现大型接口时的弊端。来看如下接口IShape:
interface IShape {
drawCircle();
drawSquare();
drawRectangle();
}

这个接口可以画圆形,方形,矩形。Circle类,Square类,Rectangel类实现IShape接口的时候必须定义drawCircle(),drawSqure(),drawRectangle()方法。
class Circle implements IShape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}
class Square implements IShape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}
class Rectangle implements IShape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}

上面的代码看起来就很怪。Rectangle类药实现它用不上的drawCircle(),drawSquare()方法,Square类和Circle类也同理。

如果我们向Ishape中增加一个接口,如drawTriangle():
interface IShape {
drawCircle();
drawSquare();
drawRectangle();
drawTriangle();
}

所有子类都需要实现这个新方法,否则就会报错。

也能看出不可能实现一个可以画圆但是不能画方,或画矩形及三角形的图形类。我们可以只是为上述子类都实现所有方法但是抛出错误指明不正确的操作不能被执行。

ISP不提倡IShape的上述实现。客户(这里的Circle, Rectangle, Square, Triangle)不应被强迫依赖于它们不需要或用不上的方法。ISP还指出一个接口只做一件事(与SRP类似),所有其他分组的行为都应当被抽象到其他的接口中。

这里, Ishape接口执行了本应由其他接口独立处理的行为。

为了使IShape符合ISP原则,我们将这些行为分离到不同的接口中去:
interface IShape {
draw();
}
interface ICircle {
drawCircle();
}
interface ISquare {
drawSquare();
}
interface IRectangle {
drawRectangle();
}
interface ITriangle {
drawTriangle();
}
class Circle implements ICircle {
drawCircle() {
//...
}
}
class Square implements ISquare {
drawSquare() {
//...
}
}
class Rectangle implements IRectangle {
drawRectangle() {
//...
}
}
class Triangle implements ITriangle {
drawTriangle() {
//...
}
}
class CustomShape implements IShape {
draw(){
//...
}
}

ICircle接口只处理圆形绘制,IShape处理任意图形的绘制,ISquare只处理方形的绘制,IRectangle只处理矩形的绘制。

或者

子类可以直接从Ishape接口继承并实现自己draw()方法:
class Circle implements IShape {
draw(){
//...
}
}

class Triangle implements IShape {
draw(){
//...
}
}

class Square implements IShape {
draw(){
//...
}
}

class Rectangle implements IShape {
draw(){
//...
}
}

我现在还可以使用I-接口来创建更多特殊形状,如Semi

circle, Right-Angled Triangle, Equilateral Triangle, Blunt-Edged Rectangle等等。

依赖反转


依赖反转 Dependency Inverse Principle


Dependency should be on abstractions not concretion
依赖于抽象而非具体实例

A. 上层模块不应该依赖于下层模块。它们都应该依赖于抽象。
B. 抽象不应该依赖于细节。细节应该依赖于抽象。

这对开发由许多模块构成的应用程序十分重要。这时候,我们必须使用依赖注入(dependency injection) 来理清关系、上层元件依赖于下层元件来工作。
class XMLHttpService extends XMLHttpRequestService {}
class Http {
constructor(private xmlhttpService: XMLHttpService) { }
get(url: string , options: any) {
this.xmlhttpService.request(url,'GET');
}
post() {
this.xmlhttpService.request(url,'POST');
}
//...
}

这里Http是上层元件,而HttpService则是下层元件。这个设计违背了DIP原则A: 上层模块不应该依赖于下层模块。它们都应该依赖于抽象。

这个Http类被迫依赖于XMLHttpService类。如果我们想要改变Http连接服务, 我们可能通过Nodejs甚至模拟http服务。我们就要痛苦地移动到所有Http的实例来编辑代码,这将违背OCP(开放闭合)。

Http类应当减少关心使用的Http 服务的类型, 我们建立一个Connection 接口:
interface Connection {
request(url: string, opts:any);
}

Connection接口有一个request方法。我们通过他传递一个Connection类型的参数给Http类:
class Http {
constructor(private httpConnection: Connection) { }
get(url: string , options: any) {
this.httpConnection.request(url,'GET');
}
post() {
this.httpConnection.request(url,'POST');
}
//...
}

现在,无论什么类型的Http连接服务传递过来,Http类都可以轻松的连接到网络,无需关心网络连接的类型。

现在我们可以重新实现XMLHttpService类来实现Connection 接口:
class XMLHttpService implements Connection {
const xhr = new XMLHttpRequest();
//...
request(url: string, opts:any) {
xhr.open();
xhr.send();
}
}

我们可以创建许多的Http Connection类型然后传递给Http类但不会引发任何错误。
class NodeHttpService implements Connection {
request(url: string, opts:any) {
//...
}
}
class MockHttpService implements Connection {
request(url: string, opts:any) {
//...
}
}

现在,可以看到上层模块和下层模块都依赖于抽象。 Http类(上层模块)依赖于Connection接口(抽象),而且Http服务类型(下层模块)也依赖于Connection接口(抽象)。

结语


我们讨论了每个软件开发者都需要遵从的五大原则。刚开始的时候要遵守这些原则可能会有点难,但是通过持续的练习和坚持,它将成为我们的一部分并且对维护我们的应用程序产生巨大的影响。

 
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消