接口和抽象类的设计

原创
2012/12/14 21:23
阅读数 2.4K

其实在刚接触java的时候我一直有这么一个疑问,接口和抽象类的区别是什么,甚至到现在还不能很确切的说明白这之间的关系和区别。以前也就大概的能说一下,接口用来多继承,抽象类可以有具体实现。可是这些毕竟只是很肤浅的认识,对结构设计并没有多大的帮助,直到最近写框架的时候,要大量使用抽象类和接口的时候,才慢慢开始理解他们之间的关系。本文,我想结合NIO的一些源码和最近在写的nSocket写点儿在类结构设计中使用接口和抽象类的一些操作来隐藏或暴露操作的一些做法。反正这种东西每个人有每个人的说法,我也没很多时间去研究设计模式,纯属经验之谈。

我在写nSocket的时候遇到了这么一个问题,由于类之间的关联比较复杂,最后对外暴露的类必然是继承或者实现多个类的,这样一来,就很容易出现一个问题,有些在父类具体实现的方法我不想暴露给使用者,我只想提供那些我自己规定的操作。这样描述有点儿模糊,我们来看一下在java NIO中的一个例子,这个是java7 NIO.2的ServerSocketChannel中的操作,和NIO.1中的操作有更新:

ServerSocketChannel继承了抽象类AbstractSelectableChannel并实现NetworkChannel接口。我们主要关注AbstractSelectableChannel中的implCloseChannel方法。左边红框表示ServerSocketChannel可以执行的所有操作,右边绿色的是ServerSocketChannel类中定义的操作,右下方,是ServerSocketChannel的父类中的所有操作。


问题就出在这里,为什么我们不能直接使用implCloseChannel方法。而且你会发现那几个黄色菱形的protected方法没有对外暴露,隐藏的很好。这里就要引出第一种方式来对外隐藏操作细节,采用多态。在找出这个原因之前我也饶了一个大弯,也好至少把ServerSocketChannel的实现读了一遍,我想就顺着这个步骤写下去:

图上没有标interface的都是抽象类。最左边的NetworkChanneljdk1.7新增的接口,定义了一些基本操作,具体可看:NIO.2 更新

implCloseChannel方法在AbstractInterruptibleChannel中首次定义,在AbstractSelectableChannel中具体实现,我们看下代码:

protected final void implCloseChannel() throws IOException {
        implCloseSelectableChannel();
        synchronized (keyLock) {
            int count = (keys == null) ? 0 : keys.length;
            for (int i = 0; i < count; i++) {
                SelectionKey k = keys[i];
                if (k != null)
                    k.cancel();
            }
        }
    }

这个方法主要是为了关闭通道用的,所以ServerSocketChannel对外提供了close的方法给开发者调用,而对内采用implCloseChannel来做具体实现,同时要隐藏implCloseChannel方法,使它不被调用。Close的具体实现在AbstractInterruptibleChannel中:

public final void close() throws IOException {
        synchronized (closeLock) {
            if (!open)
                return;
            open = false;
            implCloseChannel();
        }
}

    protected abstract void implCloseChannel() throws IOException;

可以为什么直接集成的类却用不了父类的方法。我们看看这里的实现细节:

1.   ServerSocketChannel是抽象类。这就意味着不能直接new一个ServerSocketChannel

2.   implCloseChannel是一个protected final修饰的方法,并且它位于抽象类中(这里当然可以用public来修饰,但是不够严谨)。

3.  回到第一点,ServerSocketChannel不能直接new,那我们可以通过返回一个实例。我们回忆一下,我们拿到ServerSocketChannel对象的方法:

ServerSocketChannel ssc=ServerSocketChannel.open();

所以问题就出在,返回给我们的还是ServerSocketChannel么?当然不是:

public static ServerSocketChannel open() throws IOException {
        return SelectorProvider.provider().openServerSocketChannel();
    }

这个方法返回的是ServerSocketChannel的一个子类ServerSocketChannelImpl(这个类的代码可以去http://www.docjar.com/html/api/sun/nio/ch/ServerSocketChannelImpl.java.html看),这里就用到了多态。在这个子类中控制对外的操作,由于采用了多态,所以即使new的是子类,也只能执行父类和子类中共有的方法。

通过这三点,我们就可以将我们想要暴露的方法写在子类中去实现,而具体内部的实现细节放一部分在父类(抽象类)中,当然父类中不想对外暴露的方法要用protected来修饰。这种实现很简单,但是也有缺陷,通常我们不会去new抽象类,所以你只能通过多态来解决这个问题。当然最简单的多态也能实现这样的方式,但是在比较复杂的设计中,都会用到多个抽象类和接口来分担职责,从这方面说,所以可以遵循上面三点来设计。

下面说第二种方式,这是我根据mina的源码抽象出来,并在nSocket中应用:

nSocket中,IoSession是对外提供使用的,所以定义在IoSession中的操作都是暴露在外的,而在内部实现中,我们可能还有其他特殊的,细节性操作,所以针对socket通信又设计了SocketSession接口,这样内部使用NioConnectorSession或者NioAcceptorSession时候都能完整的使用所有操作,而对外的IoSession则隐藏了不必要的细节。这中间也需要用抽象类来过渡,对外操作的实现都在抽象类中实现。

当然这个session的设计本身存在了理论性错误,这个会在0.1.2版本中加以改进,acceptor端的session应该是从connector端拿到的,而不是自己持有一个session。这方面的内容会在nSocket系列文章中讲到。

最后关于接口和抽象类,我推荐一篇文章:http://dev.yesky.com/436/7581936.shtml。先不说他观点的正确性或者好不好,但是就其中两点我觉得还是很有借鉴意义的:

1.  abstract classinterface所反映出的设计理念不同。其实abstract class表示的是"is-a"关系,interface表示的是"like-a"关系。

2.  面向对象设计中的一个核心原则 ISP (Interface Segregation Principle)


展开阅读全文
打赏
3
9 收藏
分享
加载中
更多评论
打赏
0 评论
9 收藏
3
分享
返回顶部
顶部