我希望能够编写优美的代码。7 ^6 j# O$ z7 ]& Z' Q
优美的代码就像一篇散文,易懂易读,而且看起来很漂亮。在《代码之美》一书中,收录了Ruby之父松本行宏的一篇文章,名为《把代码当作文章》,大约表达了同样的含义。Thoughtworks的一位工程师在《软件开发沉思录》一书中提出,每个类的方法最好不要超过5行。最初让我感觉很惊诧,继而觉得不可能。虽然这位工程师言之凿凿,提到在自己参与的项目中,所有代码都完全遵循了这一规范,我仍然表示怀疑。最近,阅读了Robert C. Martin的著作《代码整洁之道》(英文版名为Clean Code),看到Uncle Bob演示的代码,真是漂亮极了。仔细一看,这些好的代码在每个方法中大多数都没有超过5行。诀窍在哪里?那就是重构手法中最常用的Extract Method。进一步讲,如果我们能够为每个类与方法以及变量定义出好的名字,代码确实可以变成一篇散文。当然,是英文散文。4 x8 H' W6 a. ^: ~) C
今天,我在重温.NET的序列化时,在MSDN上找到一篇演示Xml序列化的示范代码。或许是因为示范代码的缘故,这一段代码写得极其地不优雅,甚至显得有些丑陋:public class Test { 0 x) h7 C0 f3 y# P$ X- s public static void Main() {2 D& {: @/ r# Q1 H/ b
// Read and write purchase orders.- _6 @ Z: a6 Y4 j* k; J
Test t = new Test();* k9 v I; l. i5 m- `, J3 Y9 G6 Y
t.CreatePO("po.xml"); + r' L0 y* q0 i- O5 d t.ReadPO("po.xml");. T) D/ Y( m7 }
} & d; O: v) K' E1 l! n2 q! b. ^0 [7 f8 H; |& b
private void CreatePO(string filename) { . w9 n1 N! R! O" A9 H+ s- ^" A // Create an instance of the XmlSerializer class; : v9 G+ C/ o1 k // specify the type of object to serialize. 7 a B, p, x% l XmlSerializer serializer = 0 b9 i5 N( Z2 t4 P2 r# M; f) z new XmlSerializer(typeof(PurchaseOrder));3 h5 T3 s7 ^5 i. c- Y) m
TextWriter writer = new StreamWriter(filename);- P( R5 L8 I' ?
PurchaseOrder po = new PurchaseOrder();* @/ `' S; T W" h) [& k: r I5 e) e9 a" Y
) i$ A- [- t& S/ l$ I // Create an address to ship and bill to. : I' b2 z5 a8 s6 C2 @ Address billAddress = new Address();% l, _& i+ \1 ~/ C+ X
billAddress.Name = "Teresa Atkinson"; ( k7 Y3 a' k' k" K7 A9 i8 n* I billAddress.Line1 = "1 Main St.";( c4 n. h5 D6 D
billAddress.City = "AnyTown"; - G3 t! R3 _' m billAddress.State = "WA";& G+ a- Y8 D: x& P6 f
billAddress.Zip = "00000";7 k9 {; s3 v# W2 i/ @
// Set ShipTo and BillTo to the same addressee.9 O+ B! j* |) C& a" [
po.ShipTo = billAddress; . X/ @$ _ P! k po.OrderDate = System.DateTime.Now.ToLongDateString(); / f$ [& Y; d+ A) z ( [- A* v/ ?5 L* e; i z // Create an OrderedItem object.- O& k4 O* u. c
OrderedItem i1 = new OrderedItem();2 j* ?* F2 O9 K
i1.ItemName = "Widget S"; 8 U- N, y3 [( @ i1.Description = "Small widget"; ) `& a/ u" {1 `( L i1.UnitPrice = (decimal)5.23; # v, u9 R! f# ~5 U$ Y( |* { i1.Quantity = 3; - ~) F! V2 |. i/ _& K i1.Calculate(); " u, @5 \8 f, M2 d0 S2 f4 W, `: R+ s# t5 W" ^
// Insert the item into the array. 5 h3 G5 Y- s$ V& A$ z OrderedItem[] items = { i1 }; ' L4 B8 h% a8 ]8 X po.OrderedItems = items;. g ?4 f" L! ^+ c$ c
// Calculate the total cost.% g0 J3 v- s% c7 G
decimal subTotal = new decimal(); 6 c. s' f8 a, s1 M- t" z) R foreach (OrderedItem oi in items) { 9 E/ d) k2 h$ U. j+ I% ` subTotal += oi.LineTotal;: H, Y& W+ U# `8 ]" Y/ m
}4 m, n- f0 K8 k. a! Q
po.SubTotal = subTotal; 2 P2 O& k: Y% q+ [# I: ? po.ShipCost = (decimal)12.51; / M% l3 ]! i& b- l3 ~ po.TotalCost = po.SubTotal + po.ShipCost; G5 h V% `, e& v# |* B y( h5 \# K3 J
// Serialize the purchase order, and close the TextWriter. ( { A' S( z ~# r" B serializer.Serialize(writer, po); * ^7 k: D3 Z6 M8 F# f0 O! t writer.Close();3 B! l! w x/ } |
}8 z4 I. L+ A; z9 `" U* T
3 v+ G( s- c+ {) a protected void ReadPO(string filename) { ( T" @4 W. m3 p% ^% ~2 }8 d // Create an instance of the XmlSerializer class;3 f3 H, z2 ^+ u! _2 H1 r
// specify the type of object to be deserialized.& t; b( i: r! l, a& ~
XmlSerializer serializer = new XmlSerializer(typeof(PurchaseOrder)); * y6 U1 W$ F; | /* If the XML document has been altered with unknown 8 U$ K6 \- [5 l {3 X, n nodes or attributes, handle them with the 0 r0 d! p" O. @& A UnknownNode and UnknownAttribute events.*/ / }. x8 M) n! r4 y! Q' S serializer.UnknownNode += new 6 o8 |1 C$ M6 q+ R( ? XmlNodeEventHandler(serializer_UnknownNode);$ p7 X5 B% ~. U- K: v6 ^5 l
serializer.UnknownAttribute += new. z: o5 Z; I+ \: |; x# u9 Y3 U
XmlAttributeEventHandler(serializer_UnknownAttribute);+ j. H! ]8 s! B0 i2 {
: O& o7 @- a+ }" U! g0 S1 y( ]1 C2 X // A FileStream is needed to read the XML document. * ]$ T7 o2 O9 e& z. ? G3 V, C FileStream fs = new FileStream(filename, FileMode.Open);% Z0 N. C } m, K& v4 ^2 R" N
// Declare an object variable of the type to be deserialized.5 a" z, p. U, q6 j1 r
PurchaseOrder po; j' G/ U, }- g5 i, x: b /* Use the Deserialize method to restore the object's state with' j+ y& ?5 V7 y9 o
data from the XML document. */0 r$ f. [$ |" x% _7 x
po = (PurchaseOrder)serializer.Deserialize(fs); $ G* d2 C1 V$ I0 p+ l // Read the order date.% e2 V0 z/ M+ z2 H& \& ~
Console.WriteLine("OrderDate: " + po.OrderDate); . Q- A- `! h3 X3 T" D% x: K * e) R% t& e z8 ]; t // Read the shipping address.+ Z/ t$ D, z( ] }
Address shipTo = po.ShipTo;9 P1 T! \5 b- [% [$ r2 H0 ~& t& R& M
ReadAddress(shipTo, "Ship To:"); 7 F7 J* t5 q" @) J // Read the list of ordered items.' @8 L( P- t( G; C, P4 N$ T. A( s
OrderedItem[] items = po.OrderedItems;- H0 n1 z4 H9 E" P
Console.WriteLine("Items to be shipped:");# D, `; s( n$ L) a
foreach (OrderedItem oi in items) { : i5 Q9 h2 K( b+ M) M5 u1 K Console.WriteLine("\t" + 3 x! p: w$ t1 J. b oi.ItemName + "\t" +) P1 z% k9 Y, I- G& \/ N9 H5 n
oi.Description + "\t" + , x6 \) \/ O8 W6 \+ U. `/ ` oi.UnitPrice + "\t" +, j9 j6 y. p* ^, g0 n" Z3 A
oi.Quantity + "\t" +3 I: P& z6 [' Z7 P; W9 C0 `1 w
oi.LineTotal); 7 J: W) `8 L$ r% h } g; X" H# k" {
// Read the subtotal, shipping cost, and total cost.1 w( L. O, N, M" p& w
Console.WriteLine("\t\t\t\t\t Subtotal\t" + po.SubTotal);' U9 h5 F9 h' Y+ Y# ]" a
Console.WriteLine("\t\t\t\t\t Shipping\t" + po.ShipCost); ! f- b6 ?- @% B( m9 K( Z& B Console.WriteLine("\t\t\t\t\t Total\t\t" + po.TotalCost);1 y. U( _. W' G- B
}# @8 M% _2 W2 u. o9 n* ^
& } \5 v1 i n. L. a1 {
protected void ReadAddress(Address a, string label) {8 v- W7 b3 l! h% S' S; ]
// Read the fields of the Address object. . p6 ^: C+ ^! `& m Console.WriteLine(label); g5 j- n7 a, c0 L3 u5 A Console.WriteLine("\t" + a.Name);8 d' f* N% y3 V6 n8 |+ x$ T
Console.WriteLine("\t" + a.Line1);- p: ]1 F+ v$ i* o7 @$ @ }
Console.WriteLine("\t" + a.City); 2 B# ^3 T9 E4 e3 M% {7 [ Console.WriteLine("\t" + a.State); * c7 e! k$ u& S6 W' e Console.WriteLine("\t" + a.Zip); 0 I) B8 d$ M) `4 x9 M7 z Console.WriteLine();' `. f) e" m1 X- P, z& S- V9 a( d
}6 X1 n/ M6 h7 G. p* }9 [2 N
! D7 [, g" R K3 q3 {
private void serializer_UnknownNode K* h" O# Y/ p4 o+ R6 r (object sender, XmlNodeEventArgs e) {$ w. j$ o* g$ Z1 @! }
Console.WriteLine("Unknown Node:" + e.Name + "\t" + e.Text);: o. ^- v; C' E5 _9 i4 j2 s
}- g; Y& x3 o: V5 q: l0 O3 }/ L
9 K4 g# \. R0 o; F1 N, q0 o
private void serializer_UnknownAttribute 8 z& l9 r" n0 i (object sender, XmlAttributeEventArgs e) { $ x/ y U2 C6 e$ P: C" G6 _/ S System.Xml.XmlAttribute attr = e.Attr; 8 _7 o. |/ W2 t0 x# P& M Console.WriteLine("Unknown attribute " + ' X. d- E4 Q' H2 K attr.Name + "='" + attr.Value + "'");4 ?9 i, z- Z' f, @& H9 j
}9 K T# W9 V& ^0 [! ], J3 \
} ! n7 A) g. N* c3 `+ f 2 O/ k; }1 {- K, y( i F! i) V" A' B' O+ d9 \+ j
z; s3 u- C) ~, N5 f9 v% E) U看看CreatePO()和ReadPO(),多么地冗长。虽然这个实现极为简单,但对于代码的阅读者而言,想要一下子抓住该方法的中心思想,仍然比较困难。此外,方法中的注释也显得多余,因为,代码本身就可以给予很好的说明。* o$ T# m2 }0 i5 o0 C3 D Y+ J
下面,是我对这段代码的重构,大家可以对比对比,是否更加容易阅读呢? public static class PurchaseOrderHandler {5 K, o* |; q' S3 i9 K& o! ?
public static void CreatePurchaseOrder(string filename) {6 B, W0 E% G- v
PurchaseOrder po = BuildPurchaseOrder();7 k) r1 o" s' l {
XmlSerializer serializer = new XmlSerializer(typeof(PurchaseOrder));8 f- i9 u6 ?5 A" f( s! E' I3 \
using (var writer = new StreamWriter(filename)) { & J9 O4 i8 T: y+ ^4 Z( Z, D serializer.Serialize(writer, po);6 G3 t* @# G( N
}1 H; {+ p; ]1 R- Q" y5 k) W, T
} * M- f! L$ _" u4 d) y0 L+ Y; \8 s& [
private static PurchaseOrder BuildPurchaseOrder() {4 j1 w* u. x* t/ m1 z1 w% q E
Address address = CreateAddress(); ' Q6 d3 N1 B3 t0 u3 n: s) j OrderedItem i1 = CreateOrderedItem(); & W; F4 e3 D4 V OrderedItem[] items = { i1 }; " c# q* Y. ?, `8 W; Y1 X# _1 f- }( E z( O! X5 Z2 c# a1 ^: S' J PurchaseOrder po = new PurchaseOrder();' T" v4 N; {! P
po.ShipTo = address;, q- g; v& }/ e4 }
po.OrderDate = System.DateTime.Now.ToLongDateString();( k8 j' s0 t0 y x
po.OrderedItems = items;) A: P5 I/ A* y, r S
po.SubTotal = CalculateSubTotal(items); 9 Z4 Y& h) p) ]6 f5 q( |! |1 { po.ShipCost = (decimal)12.51; ; g; L0 P" T4 h' _& { po.TotalCost = po.SubTotal + po.ShipCost;4 ]1 i3 S9 x: Q8 s
6 ?) p5 E& p; h2 d9 q7 v return po;7 u& V2 q- q+ W
}# B/ N, }. u, b }) B# b
* I+ l7 P. z" Y3 R; q- d- W private static decimal CalculateSubTotal(OrderedItem[] items) {- D- @0 Q* Y H2 A/ n7 u; v% _
decimal subTotal = new decimal(); & V* E: k4 g- {/ b+ ]; Y8 f3 ^; Q, t4 p foreach (OrderedItem oi in items) { " \% @1 y K9 E9 \2 g" K subTotal += oi.LineTotal;% J! v$ c l& `+ F1 {# t; [8 K
}5 h6 G3 z/ b8 G
0 i* h2 F/ h# b* Y return subTotal;. | ^$ w1 o2 p) n
} $ a/ u9 |8 E$ L7 s; [$ h4 n7 X# p5 T% P0 F( b7 C3 [9 Q
$ b2 d' X F) u: g
private static OrderedItem CreateOrderedItem() {( [6 q5 _6 u5 G1 l0 F5 _
OrderedItem i1 = new OrderedItem(); * A! v1 S' }9 Q3 y! S. c i1.ItemName = "Widget S"; 8 ~# D" j) f4 W9 W6 G$ n/ B5 ~6 J, t i1.Description = "Small widget"; : P4 w$ c+ e6 T: j* I8 ] i1.UnitPrice = (decimal)5.23; + Z% Q( {# t( q0 E9 X3 C i1.Quantity = 3;& C. u. ~6 H w; L u
i1.Calculate(); 4 J: |0 Q" R- G+ A. b8 c return i1;6 [0 ]# ^* N4 W# e7 Q
}. u P3 B- ^; D, z: s6 }& U0 s6 x8 w
+ i" \6 M" \9 i4 w private static Address CreateAddress() { + M, k/ ?8 x- H Address billAddress = new Address(); 0 o( D" t, T+ q6 [) L' C billAddress.Name = "Bruce Zhang"; ; r6 o8 {6 L* v! h billAddress.Line1 = "1 Main St."; ! @9 _8 O& S$ L6 c" X, k billAddress.City = "Chong Qing"; # ^# c5 k- e0 Q8 T billAddress.State = "Chong Qing";2 t$ K: d+ m! L0 @/ j
billAddress.Zip = "400000"; 3 C3 S( Q, \! B! ]& b4 D: N2 ], \+ ]4 ?! K2 H0 |9 V* P
return billAddress;4 Q* I: N9 f( o6 R! L; n! L
} B( Y: a ~7 ~" L5 r5 r& R$ o5 ?" a: F* }
public static void ReadPurchaseOrder(string filename) { ) v4 r7 J$ U! s2 r XmlSerializer serializer = new XmlSerializer(typeof(PurchaseOrder)); + e. O& R# S3 F& {$ j: S: ~: ^- {# T# P$ b: `! _6 Q$ o
serializer.UnknownNode += new XmlNodeEventHandler(serializer_UnknownNode);# M! m* I" c4 [0 A! X! a
serializer.UnknownAttribute += new XmlAttributeEventHandler(serializer_UnknownAttribute); 3 P7 I" X/ ^& k: O- t0 M0 d, `; z! I8 u" s
FileStream fs = new FileStream(filename, FileMode.Open); 3 [4 I) g8 C% D6 f+ B, g3 |9 m- ^! F1 w9 O% ]
PurchaseOrder po;& T2 M- y3 k. K" I' w4 k) f
po = (PurchaseOrder)serializer.Deserialize(fs);! [7 s" c7 z1 t( S& M! Y# ^
PurchaseOrderPrinter.PrintPurchaseOrder(po); T1 d% S( h/ x# c3 Q
} 0 G' P! `3 M1 j: o( O( _; k# D- e# T) ^& z+ @- v8 N! q
3 n- k7 I" N( `) c+ A. Q7 u# n7 r private static void serializer_UnknownNode 8 Q3 B1 H# V+ b7 d7 M$ F2 t, s$ n (object sender, XmlNodeEventArgs e) {( f7 B7 c; _8 n) z
Console.WriteLine("Unknown Node:" + e.Name + "\t" + e.Text); 7 d0 _" S+ k3 |& C( ^ }. b+ |6 u [- O; u( F0 h
3 `1 `/ a, x8 m% P; {, X8 }3 s
private static void serializer_UnknownAttribute |; v8 ]) B1 K+ r% X2 p) E1 b
(object sender, XmlAttributeEventArgs e) {- r0 V! {: y* I3 d$ t* o
System.Xml.XmlAttribute attr = e.Attr;$ v! V' l' k4 Z) @- E! B3 a
Console.WriteLine("Unknown attribute " +4 a: D& q6 v( M4 `) w
attr.Name + "='" + attr.Value + "'"); $ O3 g* z9 Z6 \ }$ @3 Y% y/ b& X( S9 `, Z. N
1 @6 _$ A" y0 t: M! L) A
private static class PurchaseOrderPrinter {9 B; I, t* ^. k8 b& ?8 w [; b9 a
public static void PrintPurchaseOrder(PurchaseOrder po) { - B/ m9 e B. z PrintOrderDate(po); r" M- T' g0 W$ b6 E9 I% l: c
PrintAddress(po.ShipTo);/ A; p9 L; o! V' Z* A- }, I
PrintOrderedItem(po.OrderedItems); 0 n& [4 T) m# H/ g# J: v& u2 I# \+ L PrintOrderCost(po); * E' R- [0 d5 @4 F }; h& i! I4 G# b7 {( y
7 ?+ n( @6 h' N$ e& ]+ A: { private static void PrintOrderCost(PurchaseOrder po) { + r t4 H0 s) c; ? | Console.WriteLine("\t\t\t\t\t Subtotal\t" + po.SubTotal); 7 v* M h9 S6 d7 a$ L4 I4 Y Console.WriteLine("\t\t\t\t\t Shipping\t" + po.ShipCost); ) ]* {6 v$ @" Q; J$ c& u, G Console.WriteLine("\t\t\t\t\t Total\t\t" + po.TotalCost);& I) g* m' b9 L. m! n
}+ G) Q+ Z6 [+ L" ^, `
; p6 `4 S: j0 U
private static void PrintOrderDate(PurchaseOrder po) { 1 T8 G9 B% B; \3 f4 I$ W6 b Console.WriteLine("OrderDate: " + po.OrderDate); ; j3 u* A3 {, X; o9 u6 M } 0 {5 _+ q3 V1 f" u- n+ u J, m0 e2 `! o
private static void PrintOrderedItem(OrderedItem[] items) {5 p. v2 p( q3 C+ `7 }; q/ a
Console.WriteLine("Items to be shipped:");7 A5 m: t3 c5 b( z s
foreach (OrderedItem oi in items) {. h) f" V( O6 K- M+ u7 `8 ?& V
Console.WriteLine("\t" + $ |: o6 `' y5 M: r0 G oi.ItemName + "\t" +1 l; E" f' J& b1 O! J0 U; {
oi.Description + "\t" + t$ A) f* V0 [: c8 V: R6 J- l9 p
oi.UnitPrice + "\t" +0 J7 e7 x8 q0 _5 f4 A) l
oi.Quantity + "\t" + 5 b$ ^/ V2 C% x; @# j* U7 b. \ oi.LineTotal);, j* e+ N2 s6 M# `- B
}! G' ^! u6 q7 D$ L# ?# \8 w
}$ x8 f+ m% X3 W2 j8 w1 Q
3 l0 }$ ]$ H5 x, ^. z$ q private static void PrintAddress(Address a) { ! v+ z* S; v' S" A( y: q: | // Read the fields of the Address object.& Q8 X( \, ^+ f5 `" M
Console.WriteLine("Ship To:"); 1 B6 y) L' }( F& F Console.WriteLine("\t" + a.Name); % O1 C, P. c% M; |+ G3 v Console.WriteLine("\t" + a.Line1);" @; x/ n' t) l h5 ]
Console.WriteLine("\t" + a.City); % G) b b U' E+ p& y( D0 I Console.WriteLine("\t" + a.State); 5 x3 M8 ]/ m# k. r$ x4 m Console.WriteLine("\t" + a.Zip); 3 a8 K, S$ T" D, H# Q1 ? Console.WriteLine();4 c, E. \: [* ~4 \: q, V% C
}, W+ ^+ o5 f( d' G* e
}( Y2 o% D1 ]) e: D. d
}9 U9 U& w5 j3 w2 \ z; J