该用户从未签到
|
abstract class和interface是java语言中对于抽象类定义进行支持的两种机制,正是由于这两种机制的存在,才赋予了Java强大的面向对象能力。abstract class和interface之间在对于抽象类定义的支持方面具有很大的相似性,甚至可以相互替换,因此很多开发者在进行抽象类定义时对于abstract class和interface的选择显得比较随意。其实,两者之间还是有很大区别的,对于它们的选择甚至反映出对于问题领域本质的理解、对于设计意图的理解是否正确、合理。我认为正确运用好抽象类和接口,可以优化软件结构的设计,对软件的扩展、维护带来很大的便利。本文将对它们之间的区别进行一番剖析,试图给开发者提供一个在二者之间进行选择的依据。5 h, C5 F7 `, G l* r
# t6 X" C# L }0 u$ _" }+ x
首先来理解一下抽象类。在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如我们xpads 项目中的交易类就可以理解为一个抽象类,即期交易、远期交易、掉期交易等都是继承交易类的具体类。即期交易、远期交易、掉期交易这些具体概念是实际存在的,而交易这个概念在问题领域中是不存在的,可以理解为一个抽象概念。正是因为抽象概念在实际问题领域中没有东西与它对应,所以用以表征抽象概念的抽象类是不能被实例化的。
( X7 g9 Q1 u# J" h& w" O, [- Y, q) [! S
接下来理解一下接口,接口是面向对象设计中的一组规范,是一组方法的集合体(也可以定义一些常量),在一些软件设计模式中提倡的面向接口编程,其实就是屏蔽实现类的内部处理,增强模块的安全性、灵活性,达到软件设计的高内聚、松耦合。- N+ @. P0 s- J
1 J+ D! ~' s# {( O; H0 ^下面从三个方面进行比较:
/ B. c: D& Z8 H$ q& I/ }3 N3 h$ F% j- n; `# ^
一、从语法定义层面看abstract class和interface
) O. ^4 j$ j( E: u/ ~' L% s* D; b# t9 C7 {0 B# v! n: H
使用abstract class的方式定义Deal抽象类的方式如下:5 X1 d8 a* \3 f, ]$ `, g) C
; H5 e' G4 S: p, x" C6 d8 m% P
abstract class FxDeal {
/ G: v$ B* c- u: ~% z' y
h3 I3 d% F; k4 j7 o& f8 x' u private long dealsId; //交易流水号
3 i: F# W; L4 J+ G$ Q2 `/ C* ~; f. ^# j
private long blockNumber;//套流水号1 v2 W( ~8 O8 E3 {* u4 j* r# r
+ q9 L1 x$ e( G5 t( mprivate String appls;//产品类别
" L9 S/ `7 @! n9 ?
$ f# r, i/ C7 ]. c) X+ Cprivate String inputChannel;//录入渠道
4 x1 h( k3 e( L6 n M
. m) e: w% j- i8 N+ q) R I& ?private String typeOfDeal;//交易类型1 s" n% A4 Q- n% R& t X; b1 a
1 j1 `4 u" V F* m2 g4 h% R& Uprivate String flagOfDeal;// 特殊交易标识
5 Z# | o# [* E, Q2 W- d8 U
7 ~* z, B/ F5 q) Cprivate String bankId;//业务发生行
; z! q0 u7 `3 I( K
" h Y& R! W9 s7 x9 j" bprivate String customerId; //客户号' _7 v2 x! h. F) W* Z- W0 K
) }& A7 V5 z, X$ [# _6 M2 ~" J) j7 Q
private String customerName;//客户名称
1 d/ V7 {* k8 Y$ f: g. D! L
1 n/ y1 t. g, [1 W# y4 Kprivate int customerType;//客户类型
. V W" p6 s. L; {
/ ^ D# H) D, [* W9 z) cprivate double amount1;+ L+ [& C# b) E) Q% G
/ Q' |' {) G, ?: O# p( b
private double amount2;0 d8 @% M" E8 q0 G# K; I
$ k7 p& y" \' v2 \% d, B0 K J
abstract void method1();5 h; i' K8 ^) ^! C% p3 \. s s
- ]6 x3 I( w7 N0 a* Q# }: uabstract void method2(); ) x; t0 x6 I. u5 t% o- l
* f" Q, A& @' B! Z1 z4 O+ A: p, f& J7 o …1 X# O# ?' Z/ V1 E2 l, i( J
4 k) M, R7 N; }5 Y" }
}
) C: V; I1 Z6 }! }3 |! y
% l: f3 ^; n: }. i使用interface的方式定义Deal抽象类的方式如下:) S" i# t. u, }4 W8 m
interface FxDeal {" G& z8 C& x& n' v5 m/ k
void method1();) r( N: P! O: q% U8 H9 f' a
void method2();2 @. Y( [9 r2 o' J
…# O. _ X0 R1 M
}2 }; F7 A! H. s/ C. \: L6 _9 f
: _3 ?& u! t0 l- |# P" T+ n在abstract class方式中,Deal可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface方式的实现中,Deal只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在interface中一般不定义数据成员),所有的成员方法都是abstract的。从某种意义上说,interface是一种特殊形式的abstract class。! x2 d4 x* n( K- z2 m6 \
( D M& G2 }- Q6 s1 N8 l
二、从编程层面看abstract class和interface
! [ ^+ @; _6 i! H1 X1 g2 S6 e. `; M/ o. B/ h
首先,abstract class在Java语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。
. g, _% G4 T9 |2 q. h2 H9 h( r, W9 ?& t2 A$ P3 J K _# l6 [4 }& }
其次,在abstract class的定义中,我们可以赋予方法的默认行为。但是在interface的定义中,方法却不能拥有默认行为,为了绕过这个限制,必须使用委托,但是这会增加一些复杂性,有时会造成很大的麻烦。/ r' ?0 Z$ L n/ \5 A
0 ~# t- C Y C7 T8 `( d# D* N- I在抽象类中不定义默认行为存在一个比较严重的问题,那就是可能会造成维护上的麻烦。因为如果后来想修改类(一般通过abstract class或者interface来表示)以适应新的情况(比如,添加新的方法或者给已用的方法中添加新的参数)时,就会非常的麻烦,可能要花费很多的时间(对于派生类很多的情况,尤为如此)。但是如果它是通过abstract class来实现的,那么可能就只需要修改定义在abstract class中的默认行为就可以了。
) r( W" U6 j# z0 t4 y) P0 Z" }/ r- z/ E- A+ n7 u1 I, q
另一个问题是:如果不是采用抽象类中的默认行为,就会导致同样的方法实现出现在该抽象类(或接口)的每一个派生类(或实现类)中,违反了“one rule,one place”原则,造成代码重复,同样不利于以后的维护。因此,我认为在abstract class和interface间进行选择时还是需要谨慎的,搞不好的话,是会给程序埋下一些隐患的。; A! ]8 g, x* \& }( k
4 {2 ]9 Q& _6 P5 [8 d三、从设计理念层面看abstract class和interface3 H, g3 p0 L! x
4 z% g: E7 C7 w1 P
abstract class在Java语言中体现的是一种继承关系,父类和派生类之间必须存在“is a”关系,即父类和派生类在概念本质上应该是相同的。对于interface 来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的契约而已。为了使论述便于理解,下面将通过一个简单的实例进行说明。( i0 N' J% O" I' j9 S
3 @# g2 d+ v0 e) e s
考虑这样一个例子,假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过abstract class或者interface来定义一个表示该抽象概念的类型,定义方式分别如下所示:% j) ]1 q( h) z1 j
! B& x* C1 L! d" ]9 ?+ {! ?
使用abstract class方式定义Door:' c7 C* u$ p6 E4 i7 Q4 H3 P$ g
- Q J1 o, _$ J7 O6 L2 U
abstract class Door {
8 P" e! i5 \5 y% I% E$ v$ r6 `2 O. d* v& N3 V" y6 _- B
abstract void open();
; z: M( H# n- H( }4 I3 s, h( g( N% t" T! E/ z7 z/ z. {, {% m( Q
abstract void close();) w7 c) g+ F/ \; ?0 ^7 o
4 a8 ~: v, k; i4 y. o
}) `, k, G7 h& m6 S! q( G
7 l" s# }2 O0 g 使用interface方式定义Door:
& ~+ U, Q$ ^+ t1 b
0 ^7 e% i% Y+ T: ` interface Door {
: P, W# O- c; x: P j' s! [: P" G) `0 C
void open();
' I- d, c% t0 j' q
) v: x: G6 [% N2 Y! Q4 i void close();$ C/ h7 G2 w! ?
, ]3 t* A3 b; l' I }
/ p% D: ]. d0 a J; w# Y5 w k. }7 r. \
其他具体的Door类型可以extends使用abstract class方式定义的Door或者implements使用interface方式定义的Door。看起来好像使用abstract class和interface没有大的区别。
# G$ \2 ?- O0 K' i! z9 C% {0 H+ D A' m
如果现在要求Door还要具有报警的功能。我们该如何设计针对该例子的类结构呢(在本例中,主要是为了展示abstract class和interface反映在设计理念上的区别,其他方面无关的问题都做了简化或者忽略),下面将罗列出可能的解决方案,并从设计理念层面对这些不同的方案进行分析。9 ^! ~# _$ t1 h3 K5 x2 I$ f4 V
( P& A% Q$ B" D; ]. }" s
解决方案一:
: y6 O( {5 D* d1 ^. i9 E5 v# G% I3 o, k) X, D3 V, n
简单的在Door的定义中增加一个alarm方法,如下:
g: O2 J/ S% O% T* X: F* X e. C& F) r, R! U
abstract class Door {
4 {; L4 e8 N& q: G* @' D! |# x- N
abstract void open();- T* V% A+ Q8 x4 j; t2 m" B& R
: W9 K7 V- v5 ~! `
abstract void close();7 h s0 R1 i2 `' }
( ~0 n8 ~9 k9 R6 N1 Q abstract void alarm();
# {% b4 G m6 b6 r' z. c
! J% b. K) p2 |! l# z }' Q/ a1 K2 V0 U4 ?! p0 J" ~
1 q T7 _) O) X8 o+ \/ w4 U2 a& `) p 或者! P* V+ `- ~9 Y* S. W( f9 `6 t" L
" [6 I, |6 P9 r/ h
interface Door {
7 Z5 {$ Y( Q, m# j9 s) H1 S. m! }/ o Q. Z. A7 X+ l
void open();
# W/ S# C; N* r: f6 G1 M/ B* w( X) y4 ~9 h
void close();
- F, P/ s& l& a6 h9 Q, N- U5 R7 T9 w# H( {" q2 F% m
void alarm();
M1 M2 a% c4 R1 Q- B. n: K, l9 O8 _0 Y, ?) N$ z
}( R: X8 S" H3 f, S# y
3 C6 v7 f* Y1 | 那么具有报警功能的AlarmDoor的定义方式如下:7 I6 k: ?. m3 ?0 q
: A" {( i5 N: j& k2 b4 @
class AlarmDoor extends Door {
6 O8 n+ i- m$ {9 c1 W
: A/ F* Q: @1 ~6 A+ s, A- T/ ` void open() { … } S) Y& a. ~5 m. L4 s/ \
% D6 Q2 x7 T5 n void close() { … }
5 ]- c) Q# [! y, p- g K) d
5 g k1 i: m/ D$ T; ~6 n8 l' T void alarm() { … }
9 k/ x8 t6 Q5 r! ~- z3 T2 r% ?& r
6 w) z7 L& m; c9 x1 R$ J: t }
0 M$ q1 J! P2 k% t+ d5 _7 V" E
$ Z R: M' c5 Q' q 或者
8 Z3 o% c, @, D4 h% i# u2 ]% a
# t t' ^# R4 n: L class AlarmDoor implements Door {
4 K+ Y4 L' z7 x: W$ d3 K5 o% a% v/ H2 {* K3 h" q- Y5 p
void open() { … }
. Y' t' q) O& ?6 g6 k5 _) I @& D) G6 t; \
void close() { … }# K- V3 C6 i& ~% p6 Q
4 C. K4 W, _& W. l' m$ p7 E5 s2 \
void alarm() { … }
, }" g* ]- \5 E9 R% \/ _( r# t- V3 J$ ^& Z
}! l8 j: | [ W3 o+ a, V' w
; O6 x6 w/ J- q' ]0 i* ]* N
这种方法违反了面向对象设计中的一个核心原则ISP(Interface Segregation Priciple),在Door的定义中把Door概念本身固有的行为方法和另外一个概念“报警器“的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为“报警器“这个概念的改变(比如:修改alarm方法的参数)而改变。
# a$ C3 w; [, t% F7 L0 P3 W* q3 a: T
解决方案二:
; l; N5 a# y2 w0 u; C" r0 M. o/ w9 x4 Z+ K; q. N9 z
既然open、close和alarm属于两个不同的概念,根据ISP原则应该把它们分别定义在代表这两个概念的抽象类中。定义方式有:这两个概念都使用abstract class方式定义;两个概念都使用interface方式定义;一个概念使用abstract class方式定义,另一个概念使用interface方式定义。7 w# \" I5 \9 F& T
1 C8 y/ V2 R O# _8 f
显然,由于Java语言不支持多重继承,所以两个概念都使用abstract class方式定义是不可行的。后面两种方式都是可行的,但是对于它们的选择却反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理。; k0 ~, d& H) D3 I0 l1 L
如果两个概念都使用interface方式来定义,那么就反映出两个问题:1、我们可能没有理解清楚问题领域,AlarmDoor在概念本质上到底是Door还是报警器?2、如果我们对于问题领域的理解没有问题,比如:我们通过对于问题领域的分析发现AlarmDoor在概念本质上和Door是一致的,那么我们在实现时就没有能够正确的揭示我们的设计意图,因为在这两个概念的定义上(均使用interface方式定义)反映不出上述含义。
6 l" y* }: Z+ { e, C1 _
# X& U' B8 Y& Y l 如果我们对于问题领域的理解是:AlarmDoor在概念本质上是Door,同时它有具有报警的功能。我们该如何来设计、实现来明确的反映出我们的意思呢?前面已经说过,abstract class在Java语言中表示一种继承关系,而继承关系在本质上是“is a”关系。所以对于Door这个概念,我们应该使用abstarct class方式来定义。另外,AlarmDoor又具有报警功能,说明它又能够完成报警概念中定义的行为,所以报警概念可以通过interface方式定义。如下所示:
% p6 b- M/ K1 w6 t$ ?
! E! ~5 S( g5 W9 @6 V; U abstract class Door {7 u5 x4 }) y4 [; c4 J, ^. {( D2 K
1 b; U; W c; { _* u
abstract void open();* F+ `8 m8 b- i+ a- Z
5 L" t2 W; V( |& D0 F$ E
abstract void close();
- Z0 S* |) E3 }9 s9 V
) k- {6 P6 l% K- T4 _ }" J! B7 O( n$ e8 U6 A4 _
8 h& C$ K8 D& r# J F+ P9 d interface Alarm {: S7 b- W4 a" _4 m8 C* D
/ h, u7 ^8 W7 L9 N
void alarm();
6 p2 ]; ]: ~5 I6 m+ `# c: f' g/ P
}, H' S1 R' @& g9 v9 P* F0 p
3 l& y) i4 \& w# A; L class AlarmDoor extends Door implements Alarm {
8 g7 B/ u8 L8 e$ g a! A" f7 ^1 N+ t. U+ a8 Q
void open() { … }
9 }; p" l/ H' v( g: S* ?' j! P
" j/ }2 \6 B/ u$ ^9 } void close() { … }7 B1 ?# I2 ^" O9 } K3 L
5 B% F% R$ z2 K k% `) @
void alarm() { … }
& E5 e2 ]7 V3 _$ r( X' R8 f9 |0 x. s! @* s! _
}0 [; d/ ]/ s# d6 A
) k) f" I& }6 e( ]
这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。其实abstract class表示的是“is a”关系,interface表示的是“like a”关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。* T, I' F/ ^' v& B" b3 {
其实在某些时候,这两个特殊的类的使用是可以互为替换的,但是在较大规模的系统工程设计时,如果不能正确运用好它的话,可能会破坏程序的结构,增强模块间的耦合度,给程序的维护与升级带来较大的不便,希望大家能够细细体会。
8 p, D) ^% K4 a |& O- a& k( O0 U" {3 r. i; ~4 k5 k6 A
8 u! Z' O! T' G4 U7 i |
|