我希望能够编写优美的代码。
优美的代码就像一篇散文,易懂易读,而且看起来很漂亮。在《代码之美》一书中,收录了Ruby之父松本行宏的一篇文章,名为《把代码当作文章》,大约表达了同样的含义。Thoughtworks的一位工程师在《软件开发沉思录》一书中提出,每个类的方法最好不要超过5行。最初让我感觉很惊诧,继而觉得不可能。虽然这位工程师言之凿凿,提到在自己参与的项目中,所有代码都完全遵循了这一规范,我仍然表示怀疑。最近,阅读了Robert C. Martin的著作《代码整洁之道》(英文版名为Clean Code),看到Uncle Bob演示的代码,真是漂亮极了。仔细一看,这些好的代码在每个方法中大多数都没有超过5行。诀窍在哪里?那就是重构手法中最常用的Extract Method。进一步讲,如果我们能够为每个类与方法以及变量定义出好的名字,代码确实可以变成一篇散文。当然,是英文散文。
今天,我在重温.NET的序列化时,在MSDN上找到一篇演示Xml序列化的示范代码。或许是因为示范代码的缘故,这一段代码写得极其地不优雅,甚至显得有些丑陋:
public class Test { public static void Main() { // Read and write purchase orders. Test t = new Test(); t.CreatePO("po.xml"); t.ReadPO("po.xml"); } private void CreatePO(string filename) { // Create an instance of the XmlSerializer class; // specify the type of object to serialize. XmlSerializer serializer = new XmlSerializer(typeof(PurchaseOrder)); TextWriter writer = new StreamWriter(filename); PurchaseOrder po = new PurchaseOrder(); // Create an address to ship and bill to. Address billAddress = new Address(); billAddress.Name = "Teresa Atkinson"; billAddress.Line1 = "1 Main St."; billAddress.City = "AnyTown"; billAddress.State = "WA"; billAddress.Zip = "00000"; // Set ShipTo and BillTo to the same addressee. po.ShipTo = billAddress; po.OrderDate = System.DateTime.Now.ToLongDateString(); // Create an OrderedItem object. OrderedItem i1 = new OrderedItem(); i1.ItemName = "Widget S"; i1.Description = "Small widget"; i1.UnitPrice = (decimal)5.23; i1.Quantity = 3; i1.Calculate(); // Insert the item into the array. OrderedItem[] items = { i1 }; po.OrderedItems = items; // Calculate the total cost. decimal subTotal = new decimal(); foreach (OrderedItem oi in items) { subTotal += oi.LineTotal; } po.SubTotal = subTotal; po.ShipCost = (decimal)12.51; po.TotalCost = po.SubTotal + po.ShipCost; // Serialize the purchase order, and close the TextWriter. serializer.Serialize(writer, po); writer.Close(); } protected void ReadPO(string filename) { // Create an instance of the XmlSerializer class; // specify the type of object to be deserialized. XmlSerializer serializer = new XmlSerializer(typeof(PurchaseOrder)); /* If the XML document has been altered with unknown nodes or attributes, handle them with the UnknownNode and UnknownAttribute events.*/ serializer.UnknownNode += new XmlNodeEventHandler(serializer_UnknownNode); serializer.UnknownAttribute += new XmlAttributeEventHandler(serializer_UnknownAttribute); // A FileStream is needed to read the XML document. FileStream fs = new FileStream(filename, FileMode.Open); // Declare an object variable of the type to be deserialized. PurchaseOrder po; /* Use the Deserialize method to restore the object's state with data from the XML document. */ po = (PurchaseOrder)serializer.Deserialize(fs); // Read the order date. Console.WriteLine("OrderDate: " + po.OrderDate); // Read the shipping address. Address shipTo = po.ShipTo; ReadAddress(shipTo, "Ship To:"); // Read the list of ordered items. OrderedItem[] items = po.OrderedItems; Console.WriteLine("Items to be shipped:"); foreach (OrderedItem oi in items) { Console.WriteLine("\t" + oi.ItemName + "\t" + oi.Description + "\t" + oi.UnitPrice + "\t" + oi.Quantity + "\t" + oi.LineTotal); } // Read the subtotal, shipping cost, and total cost. Console.WriteLine("\t\t\t\t\t Subtotal\t" + po.SubTotal); Console.WriteLine("\t\t\t\t\t Shipping\t" + po.ShipCost); Console.WriteLine("\t\t\t\t\t Total\t\t" + po.TotalCost); } protected void ReadAddress(Address a, string label) { // Read the fields of the Address object. Console.WriteLine(label); Console.WriteLine("\t" + a.Name); Console.WriteLine("\t" + a.Line1); Console.WriteLine("\t" + a.City); Console.WriteLine("\t" + a.State); Console.WriteLine("\t" + a.Zip); Console.WriteLine(); } private void serializer_UnknownNode (object sender, XmlNodeEventArgs e) { Console.WriteLine("Unknown Node:" + e.Name + "\t" + e.Text); } private void serializer_UnknownAttribute (object sender, XmlAttributeEventArgs e) { System.Xml.XmlAttribute attr = e.Attr; Console.WriteLine("Unknown attribute " + attr.Name + "='" + attr.Value + "'"); } }
看看CreatePO()和ReadPO(),多么地冗长。虽然这个实现极为简单,但对于代码的阅读者而言,想要一下子抓住该方法的中心思想,仍然比较困难。此外,方法中的注释也显得多余,因为,代码本身就可以给予很好的说明。
下面,是我对这段代码的重构,大家可以对比对比,是否更加容易阅读呢?
public static class PurchaseOrderHandler { public static void CreatePurchaseOrder(string filename) { PurchaseOrder po = BuildPurchaseOrder(); XmlSerializer serializer = new XmlSerializer(typeof(PurchaseOrder)); using (var writer = new StreamWriter(filename)) { serializer.Serialize(writer, po); } } private static PurchaseOrder BuildPurchaseOrder() { Address address = CreateAddress(); OrderedItem i1 = CreateOrderedItem(); OrderedItem[] items = { i1 }; PurchaseOrder po = new PurchaseOrder(); po.ShipTo = address; po.OrderDate = System.DateTime.Now.ToLongDateString(); po.OrderedItems = items; po.SubTotal = CalculateSubTotal(items); po.ShipCost = (decimal)12.51; po.TotalCost = po.SubTotal + po.ShipCost; return po; } private static decimal CalculateSubTotal(OrderedItem[] items) { decimal subTotal = new decimal(); foreach (OrderedItem oi in items) { subTotal += oi.LineTotal; } return subTotal; } private static OrderedItem CreateOrderedItem() { OrderedItem i1 = new OrderedItem(); i1.ItemName = "Widget S"; i1.Description = "Small widget"; i1.UnitPrice = (decimal)5.23; i1.Quantity = 3; i1.Calculate(); return i1; } private static Address CreateAddress() { Address billAddress = new Address(); billAddress.Name = "Bruce Zhang"; billAddress.Line1 = "1 Main St."; billAddress.City = "Chong Qing"; billAddress.State = "Chong Qing"; billAddress.Zip = "400000"; return billAddress; } public static void ReadPurchaseOrder(string filename) { XmlSerializer serializer = new XmlSerializer(typeof(PurchaseOrder)); serializer.UnknownNode += new XmlNodeEventHandler(serializer_UnknownNode); serializer.UnknownAttribute += new XmlAttributeEventHandler(serializer_UnknownAttribute); FileStream fs = new FileStream(filename, FileMode.Open); PurchaseOrder po; po = (PurchaseOrder)serializer.Deserialize(fs); PurchaseOrderPrinter.PrintPurchaseOrder(po); } private static void serializer_UnknownNode (object sender, XmlNodeEventArgs e) { Console.WriteLine("Unknown Node:" + e.Name + "\t" + e.Text); } private static void serializer_UnknownAttribute (object sender, XmlAttributeEventArgs e) { System.Xml.XmlAttribute attr = e.Attr; Console.WriteLine("Unknown attribute " + attr.Name + "='" + attr.Value + "'"); } private static class PurchaseOrderPrinter { public static void PrintPurchaseOrder(PurchaseOrder po) { PrintOrderDate(po); PrintAddress(po.ShipTo); PrintOrderedItem(po.OrderedItems); PrintOrderCost(po); } private static void PrintOrderCost(PurchaseOrder po) { Console.WriteLine("\t\t\t\t\t Subtotal\t" + po.SubTotal); Console.WriteLine("\t\t\t\t\t Shipping\t" + po.ShipCost); Console.WriteLine("\t\t\t\t\t Total\t\t" + po.TotalCost); } private static void PrintOrderDate(PurchaseOrder po) { Console.WriteLine("OrderDate: " + po.OrderDate); } private static void PrintOrderedItem(OrderedItem[] items) { Console.WriteLine("Items to be shipped:"); foreach (OrderedItem oi in items) { Console.WriteLine("\t" + oi.ItemName + "\t" + oi.Description + "\t" + oi.UnitPrice + "\t" + oi.Quantity + "\t" + oi.LineTotal); } } private static void PrintAddress(Address a) { // Read the fields of the Address object. Console.WriteLine("Ship To:"); Console.WriteLine("\t" + a.Name); Console.WriteLine("\t" + a.Line1); Console.WriteLine("\t" + a.City); Console.WriteLine("\t" + a.State); Console.WriteLine("\t" + a.Zip); Console.WriteLine(); } } }
阅读代码时,我们可以先关注最主要的方法,即CreatePurchaseOrder()和ReadPurchaseOrder()方法。如果并不希望了解过多构造PO对象的细节,通过阅读这样简短的方法,可以很容易地抓住这两个方法的实现,那就是通过构建一个PO对象,进行序列化,而在反序列化时,将获得的PO对象信息打印出来。
其实,糟糕的代码不一定就是初学者的“专利”,让我们看看NHibernate中的一段代码:
public SessionFactoryImpl(Configuration cfg, IMapping mapping, Settings settings, EventListeners listeners) { Init(); log.Info("building session factory"); properties = new Dictionary<string, string>(cfg.Properties); interceptor = cfg.Interceptor; this.settings = settings; sqlFunctionRegistry = new SQLFunctionRegistry(settings.Dialect, cfg.SqlFunctions); eventListeners = listeners; filters = new Dictionary<string, FilterDefinition>(cfg.FilterDefinitions); if (log.IsDebugEnabled) { log.Debug("Session factory constructed with filter configurations : " + CollectionPrinter.ToString(filters)); } if (log.IsDebugEnabled) { log.Debug("instantiating session factory with properties: " + CollectionPrinter.ToString(properties)); } try { if (settings.IsKeywordsImportEnabled) { SchemaMetadataUpdater.Update(this); } if (settings.IsAutoQuoteEnabled) { SchemaMetadataUpdater.QuoteTableAndColumns(cfg); } } catch (NotSupportedException) { // Ignore if the Dialect does not provide DataBaseSchema } #region Caches settings.CacheProvider.Start(properties); #endregion #region Generators identifierGenerators = new Dictionary<string, IIdentifierGenerator>(); foreach (PersistentClass model in cfg.ClassMappings) { if (!model.IsInherited) { IIdentifierGenerator generator = model.Identifier.CreateIdentifierGenerator(settings.Dialect, settings.DefaultCatalogName, settings.DefaultSchemaName, (RootClass) model); identifierGenerators[model.EntityName] = generator; } } #endregion #region Persisters Dictionary<string, ICacheConcurrencyStrategy> caches = new Dictionary<string, ICacheConcurrencyStrategy>(); entityPersisters = new Dictionary<string, IEntityPersister>(); implementorToEntityName = new Dictionary<System.Type, string>(); Dictionary<string, IClassMetadata> classMeta = new Dictionary<string, IClassMetadata>(); foreach (PersistentClass model in cfg.ClassMappings) { model.PrepareTemporaryTables(mapping, settings.Dialect); string cacheRegion = model.RootClazz.CacheRegionName; ICacheConcurrencyStrategy cache; if (!caches.TryGetValue(cacheRegion, out cache)) { cache = CacheFactory.CreateCache(model.CacheConcurrencyStrategy, cacheRegion, model.IsMutable, settings, properties); if (cache != null) { caches.Add(cacheRegion, cache); allCacheRegions.Add(cache.RegionName, cache.Cache); } } IEntityPersister cp = PersisterFactory.CreateClassPersister(model, cache, this, mapping); entityPersisters[model.EntityName] = cp; classMeta[model.EntityName] = cp.ClassMetadata; if (model.HasPocoRepresentation) { implementorToEntityName[model.MappedClass] = model.EntityName; } } classMetadata = new UnmodifiableDictionary<string, IClassMetadata>(classMeta); Dictionary<string, ISet<string>> tmpEntityToCollectionRoleMap = new Dictionary<string, ISet<string>>(); collectionPersisters = new Dictionary<string, ICollectionPersister>(); foreach (Mapping.Collection model in cfg.CollectionMappings) { ICacheConcurrencyStrategy cache = CacheFactory.CreateCache(model.CacheConcurrencyStrategy, model.CacheRegionName, model.Owner.IsMutable, settings, properties); if (cache != null) { allCacheRegions[cache.RegionName] = cache.Cache; } ICollectionPersister persister = PersisterFactory.CreateCollectionPersister(cfg, model, cache, this); collectionPersisters[model.Role] = persister; IType indexType = persister.IndexType; if (indexType != null && indexType.IsAssociationType && !indexType.IsAnyType) { string entityName = ((IAssociationType) indexType).GetAssociatedEntityName(this); ISet<string> roles; if (!tmpEntityToCollectionRoleMap.TryGetValue(entityName, out roles)) { roles = new HashedSet<string>(); tmpEntityToCollectionRoleMap[entityName] = roles; } roles.Add(persister.Role); } IType elementType = persister.ElementType; if (elementType.IsAssociationType && !elementType.IsAnyType) { string entityName = ((IAssociationType) elementType).GetAssociatedEntityName(this); ISet<string> roles; if (!tmpEntityToCollectionRoleMap.TryGetValue(entityName, out roles)) { roles = new HashedSet<string>(); tmpEntityToCollectionRoleMap[entityName] = roles; } roles.Add(persister.Role); } } Dictionary<string, ICollectionMetadata> tmpcollectionMetadata = new Dictionary<string, ICollectionMetadata>(collectionPersisters.Count); foreach (KeyValuePair<string, ICollectionPersister> collectionPersister in collectionPersisters) { tmpcollectionMetadata.Add(collectionPersister.Key, collectionPersister.Value.CollectionMetadata); } collectionMetadata = new UnmodifiableDictionary<string, ICollectionMetadata>(tmpcollectionMetadata); collectionRolesByEntityParticipant = new UnmodifiableDictionary<string, ISet<string>>(tmpEntityToCollectionRoleMap); #endregion #region Named Queries namedQueries = new Dictionary<string, NamedQueryDefinition>(cfg.NamedQueries); namedSqlQueries = new Dictionary<string, NamedSQLQueryDefinition>(cfg.NamedSQLQueries); sqlResultSetMappings = new Dictionary<string, ResultSetMappingDefinition>(cfg.SqlResultSetMappings); #endregion imports = new Dictionary<string, string>(cfg.Imports); #region after *all* persisters and named queries are registered foreach (IEntityPersister persister in entityPersisters.Values) { persister.PostInstantiate(); } foreach (ICollectionPersister persister in collectionPersisters.Values) { persister.PostInstantiate(); } #endregion #region Serialization info name = settings.SessionFactoryName; try { uuid = (string) UuidGenerator.Generate(null, null); } catch (Exception) { throw new AssertionFailure("Could not generate UUID"); } SessionFactoryObjectFactory.AddInstance(uuid, name, this, properties); #endregion log.Debug("Instantiated session factory"); #region Schema management if (settings.IsAutoCreateSchema) { new SchemaExport(cfg).Create(false, true); } if ( settings.IsAutoUpdateSchema ) { new SchemaUpdate(cfg).Execute(false, true); } if (settings.IsAutoValidateSchema) { new SchemaValidator(cfg, settings).Validate(); } if (settings.IsAutoDropSchema) { schemaExport = new SchemaExport(cfg); } #endregion #region Obtaining TransactionManager // not ported yet #endregion currentSessionContext = BuildCurrentSessionContext(); if (settings.IsQueryCacheEnabled) { updateTimestampsCache = new UpdateTimestampsCache(settings, properties); queryCache = settings.QueryCacheFactory.GetQueryCache(null, updateTimestampsCache, settings, properties); queryCaches = new ThreadSafeDictionary<string, IQueryCache>(new Dictionary<string, IQueryCache>()); } else { updateTimestampsCache = null; queryCache = null; queryCaches = null; } #region Checking for named queries if (settings.IsNamedQueryStartupCheckingEnabled) { IDictionary<string, HibernateException> errors = CheckNamedQueries(); if (errors.Count > 0) { StringBuilder failingQueries = new StringBuilder("Errors in named queries: "); foreach (KeyValuePair<string, HibernateException> pair in errors) { failingQueries.Append('{').Append(pair.Key).Append('}'); log.Error("Error in named query: " + pair.Key, pair.Value); } throw new HibernateException(failingQueries.ToString()); } } #endregion Statistics.IsStatisticsEnabled = settings.IsStatisticsEnabled; // EntityNotFoundDelegate IEntityNotFoundDelegate enfd = cfg.EntityNotFoundDelegate; if (enfd == null) { enfd = new DefaultEntityNotFoundDelegate(); } entityNotFoundDelegate = enfd; }
这是类SessionFactoryImpl(它实现了ISessionFactoryImplementor接口)的构造函数,其目的时是通过Configuration以及Setting中的某些值,去初始化SessionFactoryImpl,然后构建该类的对象。坦白说,我从来没有看过如此“浩瀚无垠”的构造函数。幸好,Visual Studio提高了Region,否则,更让人头疼。(我在想,既然代码的编写者已经利用了Region来分割实现,为何不进一步将其分割为小的方法呢?)
看这样的代码,我们能够轻易读懂吗?
拙劣代码可谓遗患无穷。在《程序员修炼之道》一书中,提到了所谓“破窗效应”,即“没修复的破窗,导致更多的窗户被打破”。丑陋的代码如果只有一个小的片段,看似无关紧要,就像一幢大楼的一扇破窗一般容易让人忘记。随着时间的推移,当这些丑陋代码不知不觉蔓延到整个项目中时,我们才发现这一幢大楼已经满目疮痍了。“一屋不扫,何以扫天下”,程序员应该从小处着手,未来才可能写出优雅的代码。