黑格尔有句名言:存在即合理。以此为论据的话,静态类的使用必然有其合理性。不过物极必反,一旦代码过于依赖静态类,其劣化的解决则不可避免。这就好比罂粟作为一种草本植物,有其在药理上的价值,但如果肆无忌惮的大量使用,它就变成了毒品。! ?4 |- x2 X' U, i9 W2 ~
( E8 E3 D) d, E- G1 @ 什么是静态类
5 u! C) u1 N0 J& X0 Z2 r, X
* o& _0 s" ^+ s& | 所谓静态类指的是无需实例化成对象,直接通过静态方式调用的类。代码如下:6 L% o0 T7 K3 R% d* H' l
, I4 x/ U- j( ?0 Q, J class Math { public static function ceil($value) { return ceil($value); }/ ?& c, B6 i$ C) R: d% A
5 @* w/ s2 T" T0 ]
public static function floor($value) { return floor($value); } }
w+ N2 b0 c, s- ?: v' G
9 F8 ~! [) \1 k0 c8 `% u- a ?>
; d1 W0 L* ^) |+ K# ~( b/ Y# A3 [& `# W4 L* e- X* d/ k$ n+ t
此时类所扮演的角色更像是命名空间,这或许是很多人喜欢使用静态类最直接的原因。
% h) W5 j m( L0 i* F* K3 L% N5 R
静态类的问题
7 l) Q4 [* P" U1 `9 \" k5 s2 U4 [1 V8 q/ `( _5 g
本质上讲,静态类是面向过程的,因为通常它只是机械的把原本面向过程的代码集合到一起,虽然结果是以类的方式存在,但此时的类更像是一件皇帝的新衣,所以可以说静态类实际上是披着面向对象的壳儿,干着面向过程的事儿。3 d; I2 i( Q4 p" z
* B( z T8 W' c) W3 U) [( U5 R3 k
面向对象的设计原则之一:针对接口编程,而不是针对实现编程。这有什么不同?打个比方来说:抛开价格因素,你喜欢独立显卡的电脑还是集成显卡的电脑?我想绝大多数人会选择独立显卡。独立显卡可以看做是针对接口编程,而集成显卡就就可以看做是针对实现编程。如此说来针对实现编程的弊端就跃然纸上了:它丧失了变化的可能性。
# Q% ?3 t4 u$ |. N0 |$ v A( T! s4 U5 Q, k9 H, s% {
下面杜撰一个文章管理系统的例子来具体说明一下:
3 g. l @& e+ H O" o
8 ?+ B. |2 `! a0 U class Article { public function save() { ArticleDAO::save(); } }$ Z8 \6 |2 a H/ O# K% i
7 f, ~) C' S# P$ y4 d0 |8 f
?>1 n8 D# C. H3 n) g3 j
( W% ~. Q0 L z/ f, x
Article实现必要的领域逻辑,然后把数据持久化交给ArticleDAO去做,而ArticleDAO是一个静态类,就好像焊在主板上的集成显卡一样难以改变,假设我们为了测试代码可能需要Mock掉ArticleDAO的实现,但因为调用时使用的是静态类的名字,等同于已经绑定了具体的实现方式,Mock几乎不可能,当然,实际上有一些方法可以实现:$ x) G! E$ o! h
5 Y( P5 {4 K: E8 L class Article { private static $dao = 'ArticleDAO'; ~) n' T( u* c1 {
8 r# B, A& b& e. }6 S7 L4 Q public static funciton setDao($dao) { self: dao = $dao; }, b7 B4 V# @1 v- [. T$ g
. n* w- j2 f9 V" K: H# {+ W0 d
public static function save() { $dao = self: dao;
, x% _& h; _1 E" h* K0 U0 Q8 U/ N6 C9 T# e$ ]
$dao::save(); } }
& j. n+ q) y! K) J0 N" C) I5 m
* J, i; F4 {4 r B- ] ?>/ i5 V |# p) o/ Y5 [4 l h" [2 X
9 ?# F1 |, r+ G% U) p 有了变量的介入,可以在运行时设定具体使用哪个静态类:
/ V+ \5 X: ]7 r; t4 q! w# y J
Article::setDao('MockArticleDAO');! a- e' u7 Z% i3 N$ L" |% h
0 j+ {/ z3 h- I! s, A9 f
Article::save();( i9 T4 M1 c. C3 w) n
& J! m) ]( _9 o3 U5 V ?> L7 T" c4 h# y2 ?
# G! u8 h I: R
虽然这样的实现方式看似解决了Mock的问题,但是首先它修改的原有的代码,违反了开闭原则,其次它引入了静态变量,而静态变量是共享的状态,有可能会干扰其它代码的执行,所以并不是一个完美的解决方案。
, s2 S( k- o0 x6 ?& x, I# C9 p+ k3 L* Z& K5 }8 F( o9 J8 m
补充说明,利用动态语言的特性,其实可以简单的通过require一个不同的类定义文件来实现Mock,但这样做同样有弊端,设想我们在脚本里需要多次变换实现方式,但实际上我们只有一次require的机会,否则就会出现重复定义的错误。
, Z" _. ^' q" o7 L/ H2 T' M+ s n0 J9 w! ^4 Y9 T* o
注:某些情况下,利用静态延迟绑定也可以提高静态类的可测试性,参考PHPUnit。% b) @' {' d" p6 X
# x# S* a) E8 w8 d2 ^& l 对象的价值
6 J1 L" |0 c9 ^" F l8 l. P$ v# Q5 d, l* T
如果放弃静态类,转而使用对象,应该如何实现文章管理系统的例子?代码如下:
/ N- B7 Z2 p3 {) I5 D; K8 P9 n' u" v, n0 a# j( o4 p
class Article { private $dao;5 d4 o7 I2 g E9 @, k
- c7 T. C; M2 ^& O. ?, ` v! w public function __construct($dao = null) { if ($dao === null) { $dao = new ArticleDAO(); }
6 |' h9 u* n8 m- C* R# f U; @5 I0 `# F/ W3 L/ B! N0 u2 D3 C; V
$this->setDao($dao); }4 P- D: } I7 g7 v- o; K- G n
5 U- k' d" Q& U! C! x; f, H h# n
public function setDao($dao) { $this->dao = $dao; }
7 T; ]* s3 ^+ e* m
) C9 j: o' P" b# n4 V6 P public function save() { $this->dao->save(); } }% S2 [* n& s% ^4 e/ G2 n3 F
0 ~+ ?0 X9 @; \/ V. G ?>
" M& {6 m6 g D6 E
5 A: ?. D# o/ H" _9 X$ q$ q 实际上,这里用到了人们常说的依赖注入技术,通过构造器或者Setter注入依赖的对象:
) [+ A+ h7 j% ]' P
- L6 |7 y8 i: O6 I- b2 v) _* V V; M3 a ` $article = new Article(new MockArticleDAO());
/ V( `3 |3 H5 H* W7 s9 L* A- J3 k, M# M! B0 {3 \- }5 L# L @
$article->save();
; F1 m! ~0 d8 n; D" C S% V% r1 ^" _/ I, ]/ i* b# d, E" {
?>! z' C5 J5 U! u2 s. V5 @/ w/ `; G
7 o' [- _: C9 U: u 对象有自己的状态,不会发生共享状态干扰其它代码的执行的情况。% P' V( x: N. X5 j
) V9 r! k$ P! ]8 C6 z# v! B
… q# q$ ?' R- |+ s0 n
- K+ s. X4 i" t M0 J 当然,静态类有好的一面,比如说很适合实现一些无状态的工具类,但多数时候,我的主观倾向很明确,多用对象,少用静态类,避免系统过早的固化。顺便说一句,希望别有人告诉我静态类比对象快之类的说教,谢谢。 |