阅读 516

【译】什么是SOLID原则(第3部分)

翻译自:What’s the deal with the SOLID principles? (part 3)

让我们从最后一个 SOLID 原则开始吧,即依赖倒置原则(Dependency Inversion Principle,简称 DIP)(不要和依赖注入Dependency Injection ,DI 弄混淆了)。这个原则所说的是高级模块不应该依赖具象的低级模块,它们都应该依赖相应模块的抽象层。

我仍将使用自行车的示例来尝试给你解释这个原则。首选看下这个 Bike 接口:

interface Bike {
  void pedal()
  void backPedal()
}
复制代码

MountainBikeClassicBike 这两个类实现了上面的接口:

// 山地车
class MountainBike implements Bike {
  override void pedal() {
    // complex code that computes the inner workings of what happens 
    // when pedalling on a mountain bike, which includes taking into 
    // account the gear in which the bike currently is.
  }
  
  override void backPedal() {
    // complex code that computes what happens when we back pedal    
    // on a mountain bike, which is that you pedal in the wrong   
    // direction with no discernible effect on the bike
  }
}

// 传统自行车
class ClassicBike implements Bike {
  override void pedal() {
    // the same as for the mountain bike with the distinction that 
    // there is a single gear on a classic bike
  }
  
  override void backPedal() {
    // complex code that actually triggers the brake function on the     
    // bike
  }
}
复制代码

正如你所看到的,踩脚踏板(pedal)会让自行车向前行驶,但是山地车 MountainBike 因为有多个齿轮,所以它的 pedal 会更加复杂。另外,向后踩脚踏板(back pedal)时,山地车不会做任何事,而传统自行车 ClassicBike 则会触发刹车操作。

我之所以在每个方法的注释中都有提到“complex code”,是因为我想指出我们应该把上述代码移动到不同的模块中。我们这样做是为了简化自行车类以及遵循单一职责原则(自行车类不应该担起在你向前或向后踩脚踏板时究竟发生了什么的计算工作,它们应该处理有关自行车的更高级别的事情)。

为了做到这一点,我们将为每种类型的 pedalling 创建一些行为类。

class MountainBikePedalBehaviour {
  void pedal() {
    //complex code
  }
}

class MountainBikeBackPedalBehaviour {
  void backPedal() {
    // complex code
  }
}

class ClassicBikePedalBehaviour {
  void pedal() {
    // complex code
  }
}

class ClassicBikeBackPedalBehaviour {
  void backPedal() {
    // complex code
  }
}
复制代码

然后像下面这样使用这些类:

// 山地车
class MountainBike implements Bike {
  override void pedal() {
    var pedalBehaviour = new MountainBikePedalBehaviour()
    pedalBehaviour.pedal()
  }
  
  override void backPedal() {
    var backPedalBehaviour = new MountainBikeBackPedalBehaviour()
    backPedalBehaviour.backPedal()
  }
}

// 传统自行车
class ClassicBike implements Bike {
  override void pedal() {
    var pedalBehaviour = new ClassicBikePedalBehaviour()
    pedalBehaviour.pedal()
  }
  
  override void backPedal() {
    var backPedalBehaviour = new ClassicBikeBackPedalBehaviour()
    backPedalBehaviour.backPedal()
  }
}
复制代码

这个时候,我们可以很清楚地看到高级模块 MountainBike 依赖于某些具体的低级模块 MountainBikePedalBehaviourMountainBikeBackPedalBehaviourClassicBike 以及它的低级模块同样如此。根据依赖倒置原则,高级模块和低级模块都应该依赖抽象。为此,我们需要以下接口:

interface PedalBehaviour {
  void pedal()
}

interface BackPedalBehaviour {
  void backPedal()
}
复制代码

除了需要实现上面的接口外,行为类的代码与之前无异:

class MountainBikePedalBehaviour implements PedalBehaviour {
  override void pedal() {
    // same as before
  }
}
复制代码

剩下的其他行为类同上。

现在我们需要一种方法将 PedalBehaviourBackPedalBehaviour 传递给 MountainBikeClassicBike 类。我们可以选择在构造方法、pedal()pedalBack() 中完成这件事。本例中,我们使用构造方法。

class MountainBike implements Bike {
  
  PedalBehaviour pedalBehaviour;
  BackPedalBehaviour backPedalBehaviour;
  
  public MountainBike(PedalBehaviour pedalBehaviour,
                      BackPedalBehaviour backPedalBehaviour) {
    this.pedalBehaviour = pedalBehaviour;
    this.backPedalBehaviour = backPedalBehaviour;
  }
  
  override void pedal() {
    pedalBehaviour.pedal();
  }
  
  override void backPedal() {
    backPedalBehaviour.backPedal();
  }
}
复制代码

ClassicBike 类同上。

我们的高级模块(MountainBikeClassicBike)不再依赖于具体的低级模块,而是依赖于抽象的 PedalBehaviourBackPedalBehaviour

在我们的例子中,我们应用的主模块可能看起来向下面这样:

class MainModule {
  MountainBike mountainBike;
  ClassicBike classicBike;
  MountainBikePedalBehaviour mountainBikePedalBehaviour;
  ClassicBikePedalBehaviour classicBikePedalBehaviour;
  MountainBikeBackPedalBehaviour mountainBikeBackPedalBehaviour;
  ClassicBikeBackPedalBehaviour classicBikeBackPedalBehaviour;
  
  public MainModule() {
    mountainBikePedalBehaviour = new MountainBikePedalBehaviour();
    mountainBikeBackPedalBehaviour = 
      new MountainBikeBackPedalBehaviour();
    mountainBike = new MountainBike(mountainBikePedalBehaviour,   
                     mountainBikeBackPedalBehaviour);
    
    classicBikePedalBehaviour = new ClassicBikePedalBehaviour();
    classicBikeBackPedalBehaviour = 
      new ClassicBikeBackPedalBehaviour();
    classicBike = new ClassicBike(classicBikePedalBehaviour,
                    classicBikeBackPedalBehaviour);
  }
  
  public void pedalBikes() {
    mountainBike.pedal()
    classicBike.pedal()
  }
  
  public void backPedalBikes() {
    mountainBike.backPedal();
    classicBike.backPedal();
  }
}
复制代码

可以看到,我们的 MainModule 依赖了具体的低级模块而不是抽象层。我们可以通过向构造方法中传递依赖来改善这种情况:

public MainModule(Bike mountainBike, Bike classicBike, PedalBehaviour mBikePB, BackPedalBehaviour mBikeBPB, PedalBehaviour cBikePB, BackPedalBehaviour cBikeBPB)...
复制代码

现在,MainModule 部分依赖了抽象层,部分依赖了低级模块,这些低级模块也依赖了那些抽象层。所有这些模块之间的关系不再依赖于实现细节。

在我们到达应用程序中的最高模块之前,为了尽可能地延迟一个具体类的实例化,我们通常要靠依赖注入和实现了依赖注入的框架。你可以在 这里 找到更多有关依赖注入的信息。我们可以将依赖注入视为帮助我们实现依赖倒置的工具。我们不断地向依赖链中传递依赖关系以避免具体类的实例化。

那么为什么要经历这一切呢?不依赖于具象的一个优点就是我们可以模拟一个类,从而使测试更容易进行。我们来看一个简单的例子。

interface Network {
  public String getServerResponse(URL serverURL);
}

class NetworkRequestHandler implements Network {
  override public String getServerResponse(URL serverURL) {
    // network code implementation
  }
}
复制代码

假设我们还有一个 NetworkManager 类,它有一个公共方法,通过使用一个 Network 的实例返回服务器响应:

public String getResponse(Network networkRequestHandler, URL url) {
  return networkRequestHandler.getServerResponse(url)
}
复制代码

因为这样的代码结构,我们可以测试代码如何处理来自服务器的“404”响应。为此,我们将创 NetworkRequestHandler的模拟版本。我们之所以可以这么做,是因为 NetworkManager 依赖于抽象层,即 Network,而不是某个具体的 NetworkRequestHandler

class Mock404 implements Network {
  override public String getServerResponse(URL serverURL) {
    return "404"
  }
}
复制代码

通过调用 getResponse 方法,传递 Mock404 类的实例,我们可以很容易地测试我们期望的行为。像 Mockito 这样的模拟库可以帮助你模拟某些类,而无需编写单独的类来执行此操作。

除了易于测试,我们的应用在多变情景下也能应对自如。因为模块之间的关系是基于抽象的,我们可以更改具体模块的实现,而无需大范围地更改代码。

最后同样重要的是这会让事情变得更简单。如果你有留意自行车的示例,你会发现 MountainBikeClassicBike 类非常相似。这就意味着我们不再需要单独的类了。我们可以创建一个简单的实现了 Bike 接口的类 GenericBike,然后山地车和传统自行车的实例化就像下面这样:

GenericBike mountainBike = new GenericBike(mbPedalB, mbBackPedalB);
GenericBike classicBike = new GenericBike(cbPedalB, cbBackPedalB);
复制代码

我们减少了一半数量的具体自行车类的实现,这意味着我们的代码更容易管理。

总结

所有这些原则可能看起来有点矫枉过正,你可能会排斥它们。在很长的一段时间里,我和你一样。随着时间的推移,我开始逐渐把我的代码向增强可测试性和更易于维护的方向转变。渐渐地,我开始这样来思考事情:“如果只有一种方法可以把两个部分的内容分开,并将其放在不同的类中,以便我能……”。通常,答案是的确存在这样的一种方法,并且别人已经实现过了。大多数时候,这种方法都受到 SOLID 原则的启发。当然,紧迫的工期和其他现实生活中的因素可能会不允许你遵守所有这些原则。虽然很难 100% 实现 SOLID 原则,但是有比没有强吧。也许你可以尝试只在那些当需求变更时最容易受影响的部分遵守这些原则。你不必过分遵循它们,可以把这些原则视为你提高代码质量的指南。如果你不得不需要制作一个快速原型或者验证一个概念应用的可行性,那么你没有必要尽力去搭一个最佳架构。SOLID更像是一个长期策略,对于必须经得起时间考验的软件非常有用。

在这篇由三部分组成的文章中,我试图给你展示了一些有关 SOLID的我发现比较有趣的东西。关于 SOLID,还有很多看法和解释,为了更好地理解和从多个角度获取知识,请多阅读些其他文章。

我希望这篇文章对你有所帮助。

……

如果你还没有看过另两个部分,这里是它们的链接,第1部分第2部分 。如果你喜欢这篇文章,你可以在我们的 官方站点 上找到更多信息。

关注下面的标签,发现更多相似文章
评论