接口隔离原则 - 面向接口编程

26

我在阅读关于SOLID和其他设计原则的内容。我以为ISP与“面向接口编程而不是实现”是相同的。但看起来这些是不同的原则?

它们之间有什么区别吗?

6个回答

47
在他的书《Java程序员的UML》中,Robert Martin对接口隔离原则(ISP)有一个非常好的解释。基于这个解释,我认为ISP并不是关于一个接口“专注”于一个逻辑上连贯的事物组。因为这是不言自明的;或者至少应该是不言自明的。每个类、接口或抽象类都应该按照这种方式设计。
那么,什么是ISP?让我用一个例子来解释一下。假设你有一个类A和一个类B,类B是类A的客户端。假设类A有十个方法,其中只有两个被B使用。现在,B需要知道A的所有十个方法吗?可能不需要-这就是信息隐藏原则。你暴露的越多,耦合的机会就越大。出于这个原因,你可以在两个类之间插入一个接口C(分离)。该接口将仅声明B使用的两种方法,并且B将依赖于该接口而不是直接依赖于A。
所以,现在,
class A {
  method1()
  method2()
  // more methods
  method10()
}

class B {
   A a = new A()
}

将会变成

interface C {
  method1()
  method2()
}

class A implements C{
  method1()
  method2()
  // more methods
  method10()
}

class B {
  C c = new A()      
}   

这样可以防止B获得比它所知道的更多信息。


4
我认为@Pete Stensønes的解释比我做的更加通用和精确。我建议大家更加关注他的定义。虽然我的解释不一定是错误的,但是它仅从一个使用案例的角度来解释这个概念,忽略了它的通用性。 - Nazar Merza
2
我并不完全理解客户端不应该知道其他方法的论点。使用Java时,如果不使用这些方法,显然当这些未使用的方法发生变化时,它不会产生任何影响。 - Ganesh Kumar
1
@GaneshKumar,请阅读我的评论,就在你的评论上方。 - Nazar Merza

37

ISP关注的是每个接口代表一个独立、连贯的行为的想法。

也就是说,一个对象应该完成的每个逻辑组都应该映射到一个具体的接口。一个类可能希望做很多事情,但每件事情都应该映射到代表该行为的特定接口上。这个想法是每个接口非常专注。


4
经典的“异味”提示您没有遵循这个原则是客户端使用一个接口,其中客户端只调用接口方法的一个子集。 - TrueWill

17
假设您有一个具有许多需要实现的方法的臃肿接口。
任何实现该臃肿接口的类都必须为所有这些方法提供实现。其中一些方法可能对该具体类不适用。但是,在没有界面隔离原则的情况下,它仍必须提供实现。
让我们来看看在“界面隔离”缺失的情况下的示例代码。
interface Shape{
    public int getLength();
    public int getWidth();
    public int getRadius();
    public double getArea();
}

class Rectangle implements Shape{
    int length;
    int width;
    public Rectangle(int length, int width){
        this.length = length;
        this.width = width;
    }
    public int getLength(){
        return length;
    }
    public int getWidth(){
        return width;
    }
    public int getRadius(){
        // Not applicable
        return 0;
    }
    public double getArea(){
        return width * length;
    }
}
class Square implements Shape{
    int length;
    
    public Square(int length){
        this.length = length;
    }
    public int getLength(){
        return length;
    }
    public int getWidth(){
        // Not applicable
        return 0;
    }
    public int getRadius(){
        // Not applicable
        return 0;
    }
    public double getArea(){
        return length * length;
    }
}

class Circle implements Shape{
    int radius;
    public Circle(int radius){
        this.radius = radius;
    }
    public int getLength(){
        // Not applicable
        return 0;
    }
    public int getWidth(){
        // Not applicable
        return 0;
    }
    public int getRadius(){
        return radius;
    }
    public double getArea(){
        return 3.14* radius * radius;
    }
}

public class InterfaceNoSeggration{
    public static void main(String args[]){
        Rectangle r = new Rectangle(10,20);
        Square s = new Square(15);
        Circle c = new Circle(2);
        System.out.println("Rectangle area:"+r.getArea());
        System.out.println("Square area:"+s.getArea());
        System.out.println("Circle area:"+c.getArea());
        
    }
}

输出:

java InterfaceNoSeggration
Rectangle area:200.0
Square area:225.0
Circle area:12.56

备注:

  1. Shape 是一个通用的接口,包含了所有 Shape 实现所需的方法,例如 RectangleCircleSquare。但是,只有在相应的 Shape 子类中需要一些方法。

      Rectangle : getLength(), getWidth(), getArea()
      Square    : getLength() 和 getArea()
      Circle    : getRadius() 和 getArea()
    
  2. 如果没有分离,所有形状都会实现整个 fat 接口:Shape。

如果我们按照以下方式更改代码,就可以通过接口隔离原则实现相同的输出。
interface Length{
    public int getLength();
}
interface Width{
    public int getWidth();
}
interface Radius{
    public int getRadius();
}
interface Area {
    public double getArea();
}


class Rectangle implements Length,Width,Area{
    int length;
    int width;
    public Rectangle(int length, int width){
        this.length = length;
        this.width = width;
    }
    public int getLength(){
        return length;
    }
    public int getWidth(){
        return width;
    }
    
    public double getArea(){
        return width * length;
    }
}
class Square implements Length,Area{
    int length;
    
    public Square(int length){
        this.length = length;
    }
    public int getLength(){
        return length;
    }
    
    public double getArea(){
        return length * length;
    }
}

class Circle implements Radius,Area{
    int radius;
    public Circle(int radius){
        this.radius = radius;
    }
    
    public int getRadius(){
        return radius;
    }
    public double getArea(){
        return 3.14* radius * radius;
    }
}

public class InterfaceSeggration{
    public static void main(String args[]){
        Rectangle r = new Rectangle(10,20);
        Square s = new Square(15);
        Circle c = new Circle(2);
        System.out.println("Rectangle area:"+r.getArea());
        System.out.println("Square area:"+s.getArea());
        System.out.println("Circle area:"+c.getArea());
        
    }
}

注释:

现在,像矩形正方形圆形这样的单个形状只实现了必要的接口,并且摆脱了未使用的方法。


我认为这个例子可以改进,也许可以扩展这个例子来处理计算 List<Area> 集合中所有形状的总面积。 - Basil Musa
我喜欢这个想法,但我看到的问题是,在现实世界中,可能希望将所有这些对象视为形状(Shapes),它们提供了getArea()的公共契约。这些形状可能由一个工厂创建,该工厂返回一个Shape对象。我不需要关心它是什么形状,我只想在我的代码中的某个时刻调用它的getArea()方法。 - drlobo
当您实现诸如Circle、Square和Rectangle之类的具体类时,为什么仍要实现不适用的方法?例如,在Circle类中实现getWidth()和getLength()等方法吗?否则,示例看起来很好。 - Paul Phoenix
1
这是一个复制粘贴的问题。我现在更关注于“实施”。已经纠正了。 - Ravindra babu
1
这是复制和粘贴的问题。我更专注于“实现”。现在已经纠正了。 - Ravindra babu
显示剩余5条评论

1
  1. IWorker Interface:

    public interface IWorker {
        public void work();
        public void eat();
    
    }
    
  2. Developer Class :

    public class Developer implements IWorker {
    
         @Override
         public void work() {
               // TODO Auto-generated method stub
               System.out.println("Developer working");
    
         }
    
         @Override
         public void eat() {
               // TODO Auto-generated method stub
               System.out.println("developer eating");
    
         }
    
    }
    
  3. Robot Class:

    public class Robot implements IWorker {
    
         @Override
         public void work() {
               // TODO Auto-generated method stub
               System.out.println("robot is working");
    
         }
    
         @Override
         public void eat() {
               // TODO Auto-generated method stub
               throw new UnsupportedOperationException("cannot eat");
    
         }
    
    }
    

要查看更完整的示例,请单击这里


1
我同意上面两个回答。为了举一个例子,对于 TrueWill 上面提到的代码异味,你不应该这样做:
@Override
public void foo() {
    //Not used: just needed to implement interface
}

与 Java API 类相比,它只会抛出 NotImplementedException,特别是在集合中。适度使用可以用于接口中部分使用的方法,但是扩展 Map 与 MutableMap 可能是另一种选择。 - nanofarad

0

这是一个实际在 PHP 中的应用示例。

问题陈述:

我想要为不同形式的内容添加评论和讨论。内容可以是任何形式,如论坛主题、新闻文章、用户档案,或是对话式私信。

架构

我们需要一个可重用的 DiscussionManager 类,将 Discussion 附加到特定的内容实体上。然而,以上四个示例(以及更多)都有其独特的概念差异。如果我们希望 DiscussionManager 使用它们,则所有四个(或更多)示例都需要具有一种共同的接口,以确保它们之间兼容。否则,DiscussionManager 将无法使用它们,除非您想使用裸参数(例如没有类型检查)。

解决方案:定义 Discussable 接口,其中包含以下方法:

  • attachDiscussion($topic_id)
  • detachDiscussion()
  • getDiscussionID()

然后,DiscussionManager 可能会像这样:

class DiscussionManager
{
    public function addDiscussionToContent(Discussable $Content)
    {
        $Discussion = $this->DiscussionFactory->make( ...some data...);
        $Discussion->save() // Or $this->DiscussionRepository->save($Discussion);
        $Content->attachDiscussion($Discussion->getID()); // Maybe saves itself, or you can save through a repository
    }

    public function deleteDiscussion(Discussable $Content)
    {
        $id = $Content->getDiscussionID();
        $Content->detatchDiscussion();
        $this->DiscussionRepository->delete($id);
    }

    public function closeDiscussion($discussion_id) { ... }
}

这样,DiscussionManager 就不会关心它使用的各种内容类型的任何不相关行为。它只关心它需要的行为,而不管这些行为与什么相关联。因此,通过为您想要进行讨论的每种内容类型提供一个 Discussable 接口,您正在使用接口隔离原则。

这也是抽象基类不是一个好主意的好例子。论坛主题、用户资料和新闻文章在概念上甚至都不相同,因此试图让它们继承讨论行为会导致与不相关的父级产生奇怪的耦合。使用代表讨论的特定接口,您可以确保您想要进行讨论的实体与将管理这些讨论的客户端代码兼容。

对于 PHP 来说,这个例子也可能是 Traits 的一个很好的使用候选。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接