接口隔离原则的官方解释是接口的调用方不应该被强迫依赖它不需要的接口。这里所说的接口不是指接口类,而是指方法。
这个原则涵盖了以下含义:
1、一个实现类不应该实现它不需要用到的方法。而要做到这一点就需要做到2点。
a. 多态子类中某些子类需要,而另一些子类不需要的方法不要放到父类或者抽象类中。而是放到接口中由需要的子类来实现或者放到另一个专门的类中并通过组合委托调用。
这里其实就是说尽量用组合+委托代替继承。
b. 应该将接口类按方法用途进行拆分,让实现类实现一个拥有少量方法、特定用途的接口类,而非实现一个拥有大量方法、广泛用途的接口类。
也就是说,我们设计出来的接口类的方法应该是尽可能少,且功能专一的。如果实现类需要多个不同的功能,那就实现多个接口类即可,而非将多个接口类的方法汇合到一个接口类中,让实现类单独实现这一个接口。因为其他实现类可能用不到这个大接口类的所有方法,而只用到其中的一部分方法。
这里其实就是说要将单一职责原则实践在接口类上。
2、如果调用类的方法中只用到依赖类的某个方法,那么调用类的方法在声明依赖类的类型时,要声明成依赖类对应的接口类,而非声明成依赖类本身、或者依赖类的父类或抽象类,否则就算是依赖了调用方不需要的方法(也就是依赖类的父类中其他方法)。
这里其实就是指 基于接口而非实现编程,或者说依赖抽象而非依赖实现。
下面我们来看一个具体例子来加深我们对接口隔离原则的了解和如何使用该原则。
假设项目中用到了三个外部系统:Redis、MySQL、Kafka。每个系统都有对应的配置信息,比如地址、端口、访问超时时间等。为了在内存中存储这些配置信息,供项目中的其他模块来使用,我们分别设计实现了三个 Configuration 类:RedisConfig、MysqlConfig、KafkaConfig。
<?php
class RedisConfig {
private $configSource; //配置中心(比如zookeeper)
private $address;
private $timeout;
private $maxTotal;
//省略其他配置: maxWaitMillis,maxIdle,minIdle...
public function __construct($configSource) {
$this->configSource = $configSource;
}
public function getAddress() {
return $this->address;
}
//...省略其他get()、init()方法...
public function update() {
//从configSource上的配置信息加载到本对象的address/timeout/maxTotal等属性
}
}
class KafkaConfig { //...省略... }
class MysqlConfig { //...省略... }
现在我们希望支持 Redis 和 Kafka 配置信息的热更新,也就是说如果在配置中心中更改了配置信息,在不用重启kafka、mysql和redis的情况下,就将最新的配置信息加载到内存中(也就是 RedisConfig、KafkaConfig 类中)。但是,因为某些原因,我们并不希望对 MySQL 的配置信息进行热更新。
为了实现这个功能,我们设计实现了一个专门的 ScheduledUpdater 类以固定时间间隔来调用 RedisConfig、KafkaConfig 的 update() 方法更新配置信息。具体的代码实现如下所示:
<?php
interface Updater {
public function update();
}
class RedisConfig implements Updater {
//...省略其他属性和方法...
public function update() {
//...
}
}
class KafkaConfig implements Updater {
//...省略其他属性和方法...
public function update() {
//...
}
}
class MysqlConfig {
//...省略其他属性和方法...
}
class ScheduledUpdater {
private $executor;
private $initialDelayInSeconds;
private $periodInSeconds;
private $updater;
public function __construct(Updater $updater, $initialDelayInSeconds, $periodInSeconds) {
$this->updater = $updater;
$this->initialDelayInSeconds = $initialDelayInSeconds;
$this->periodInSeconds = $periodInSeconds;
$this->executor = new ThreadScheduledExecutor(); // 一个处理任务计划的线程池对象
}
public function run() {
$this->executor->scheduleAtFixedRate(function(){
$this->updater->update();
},
$this->initialDelayInSeconds,
$this->periodInSeconds,
TimeUnit.SECONDS
);
}
}
class Application {
public static $configSource;
public static $redisConfig;
public static $kafkaConfig;
public static $mysqlConfig;
public static function init(){
self::$configSource = new ZookeeperConfigSource(/*省略参数*/);
self::$redisConfig = new RedisConfig(self::$configSource);
self::$kafkaConfig = new KafkaConfig(self::$configSource);
self::$mysqlConfig = new MysqlConfig(self::$configSource);
}
public static function main($args) {
self::init();
$redisConfigUpdater = new ScheduledUpdater(self::$redisConfig, 300, 300);
$redisConfigUpdater->run();
$kafkaConfigUpdater = new ScheduledUpdater(self::$kafkaConfig, 60, 60);
$kafkaConfigUpdater->run();
}
}
之后产品又有一个新监控功能需求,为了方便查看zookeeper中的配置信息,需要在项目中开发一个内嵌的 SimpleHttpServer,通过访问http://127.0.0.1:2389/config ,就可以显示出系统的配置信息。
不过,出于某些原因,我们只想在这个地址中暴露 MySQL 和 Redis 的配置信息,不想暴露 Kafka 的配置信息。为了实现这样一个功能,我们还需要对上面的代码做进一步改造。改造之后的代码如下所示:
<?php
interface Updater {
public function update();
}
interface Viewer {
public function outputInPlainText();
public function output();
}
class RedisConfig implements Updater,Viewer {
//...省略其他属性和方法...
public function update() {
//...
}
public function outputInPlainText(){
// ...
}
public function output(){
// ...
}
}
class KafkaConfig implements Updater {
//...省略其他属性和方法...
public function update() {
//...
}
}
class MysqlConfig implements Viewer{
//...省略其他属性和方法...
public function outputInPlainText(){
// ...
}
public function output(){
// ...
}
}
class SimpleHttpServer {
private $host;
private $port;
private $viewers = [];
public function __construct($host, $port) {
$this->host = $host;
$this->port = $port;
}
public function addViewer($urlDirectory, Viewer $viewer) {
if (!isset($this->viewers[$urlDirectory])) {
$this->viewers[$urlDirectory] = $viewer;
}
}
public function run() {
//...
}
}
class ScheduledUpdater {
private $executor;
private $initialDelayInSeconds;
private $periodInSeconds;
private $updater;
public function __construct(Updater $updater, $initialDelayInSeconds, $periodInSeconds) {
$this->updater = $updater;
$this->initialDelayInSeconds = $initialDelayInSeconds;
$this->periodInSeconds = $periodInSeconds;
$this->executor = Executors.newSingleThreadScheduledExecutor(); // 一个处理任务计划的线程池对象
}
public function run() {
$this->executor->scheduleAtFixedRate(function(){
$this->updater->update();
},
$this->initialDelayInSeconds,
$this->periodInSeconds,
TimeUnit.SECONDS
);
}
}
class Application {
public static $configSource;
public static $redisConfig;
public static $kafkaConfig;
public static $mysqlConfig;
public static function init(){
self::$configSource = new ZookeeperConfigSource(/*省略参数*/);
self::$redisConfig = new RedisConfig(self::$configSource);
self::$kafkaConfig = new KafkaConfig(self::$configSource);
self::$mysqlConfig = new MysqlConfig(self::$configSource);
}
public static function main($args) {
self::init();
$redisConfigUpdater = new ScheduledUpdater(self::$redisConfig, 300, 300);
$redisConfigUpdater->run();
$kafkaConfigUpdater = new ScheduledUpdater(self::$kafkaConfig, 60, 60);
$kafkaConfigUpdater->run();
$simpleHttpServer = new SimpleHttpServer("127.0.0.1", 2389);
$simpleHttpServer->addViewer("/config", self::$redisConfig);
$simpleHttpServer->addViewer("/config", self::$mysqlConfig);
$simpleHttpServer->run();
}
}
我们设计了两个功能非常单一的接口:Updater 和 Viewer,并且让需要实现update()和output()方法的MysqlConfig、RedisConfig和KafkaConfig类分别实现对应的接口。而不是将update()和output()方法一股脑的写入到一个Config接口或者Config抽象类中让MysqlConfig、RedisConfig和KafkaConfig类都实现或继承这个Config接口或Config抽象。
如此一来,调用方 ScheduledUpdater 只依赖 Updater 这个跟热更新相关的接口,不需要被强迫去依赖不需要的 Viewer 接口,满足接口隔离原则。调用方SimpleHttpServer同理也满足接口隔离原则。
接口隔离原则的好处:通过划分粒度小的接口进行方法隔离,能提高实现类的扩展性和灵活性,也提高了接口类的复用性。
假如我们不采用接口隔离,而是设计一个大而全的接口类,那么意味着我每加一个具有特异性的新功能需求,下面的3个Config 类都要实现新增需求对应的这个方法,尽管这个方法并非所有Config类都会用到。
举个例子,如果上面例子中需要增加一个新需求,开发一个 Metrics 性能统计模块,并要求MysqlConfig和KafkaConfig实现metrics()方法,而RedisConfig不暴露性能统计信息不实现metrics()方法。不采用接口隔离意味着RedisConfig为了满足接口类的所有方法都要实现的规则,不得不实现一个空的 metrics 方法。
如此一来,Config 接口类的复用性不强,且实现类的扩展性也不好。
总结一下:
为了做到接口隔离原则,需要设计粒度较小的接口,且调用方声明依赖的类型时应该声明接口类而非实现类,带来的好处是提高接口类的复用性 和实现类的灵活性、扩展性。
如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息
张柏沛IT技术博客 > 面向对象和设计模式(八)设计原则之接口隔离原则 ISP(Interface Segregation Principle)