Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 34 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
34
Dung lượng
3,81 MB
Nội dung
ptg serialization properly, you create more work for all developers who intend to use your types as a member or base class. When your type does not sup- port serialization, they must work around it, adding their own imple- mentation of a standard feature. It’s unlikely that clients could properly implement serialization for your types without access to private details in your types. If you don’t supply serialization, it’s difficult or impossible for users of your class to add it. Instead, prefer adding serialization to your types when practical. It should be practical for all types that do not represent UI widgets, windows, or forms. The extra perceived work is no excuse. .NET serialization support is so simple that you don’t have any reasonable excuse not to support it. In many cases, adding the Serializable attribute is enough: [Serializable] public class MyType { private string label; private int value; } Adding the Serializable attribute works because all the members of this type are serializable: string and int both support .NET serialization. The reason it’s important for you to support serialization wherever possible becomes obvious when you add another field of a custom type: [Serializable] public class MyType { private string label; private int value; private OtherClass otherThing; } The Serializable attribute works here only if the OtherClass type supports .NET serialization. If OtherClass is not serializable, you get a runtime error and you have to write your own code to serialize MyType and the OtherClass object inside it. That’s just not possible without extensive knowledge of the internals defined in OtherClass. .NET serialization saves all member variables in your object to the output stream. In addition, the .NET serialization code supports arbitrary object 158 ❘ Chapter 3 Expressing Designs in C# From the Library of Wow! eBook ptg graphs: Even if you have circular references in your objects, the serialize and deserialize methods will save and restore each actual object only once. The .NET Serialization Framework also will re-create the web of refer- ences when the web of objects is deserialized. Any web of related objects that you have created is restored correctly when the object graph is dese- rialized. A last important note is that the Serializable attribute supports both binary and SOAP serialization. All the techniques in this item will support both serialization formats. But remember that this works only if all the types in an object graph support serialization. That’s why it’s important to support serialization in all your types. As soon as you leave out one class, you create a hole in the object graph that makes it harder for anyone using your types to support serialization easily. Before long, every- one is writing their own serialization code again. Adding the Serializable attribute is the simplest technique to support seri- alizable objects. But the simplest solution is not always the right solution. Sometimes, you do not want to serialize all the members of an object: Some members might exist only to cache the result of a lengthy operation. Other members might hold on to runtime resources that are needed only for in-memory operations. You can manage these possibilities using attri - butes as well. Attach the [NonSerialized] attribute to any of the data mem- bers that should not be saved as part of the object state. This marks them as nonserializable attributes: [Serializable] public class MyType { private string label; [NonSerialized] private int cachedValue; private OtherClass otherThing; } Nonserialized members add a little more work for you, the class designer. The serialization APIs do not initialize nonserialized members for you dur- ing the deserialization process. None of your types’ constructors is called, so the member initializers are not executed, either. When you use the seri- alizable attributes, nonserialized members get the default system-initialized value: 0 or null. When the default 0 initialization is not right, you need to Item 27: Prefer Making Your Types Serializable ❘ 159 From the Library of Wow! eBook ptg implement the IDeserializationCallback interface to initialize these non- serializable members. IDeserializationCallback contains one method: OnDeserialization. The framework calls this method after the entire object graph has been deserialized. You use this method to initialize any non- serialized members in your object. Because the entire object graph has been read, you know that any function you might want to call on your object or any of its serialized members is safe. Unfortunately, it’s not fool- proof. After the entire object graph has been read, the framework calls OnDeserialization on every object in the graph that supports the IDeserializationCallback interface. Any other objects in the object graph can call your object’s public members when processing OnDeserialization. If they go first, your object’s nonserialized members are null, or 0. Order is not guaranteed, so you must ensure that all your public methods han- dle the case in which nonserialized members have not been initialized. So far, you’ve learned about why you should add serialization to all your types: Nonserializable types cause more work when used in types that should be serialized. You’ve learned about the simplest serialization meth- ods using attributes, including how to initialize nonserialized members. Serialized data has a way of living on between versions of your program. Adding serialization to your types means that one day you will need to read an older version. The code generated by the Serializable attribute throws exceptions when it finds fields that have been added or removed from the object graph. When you find yourself ready to support multiple versions and you need more control over the serialization process, use the ISerializable interface. This interface defines the hooks for you to cus- tomize the serialization of your types. The methods and storage that the ISerializable interface uses are consistent with the methods and storage that the default serialization methods use. That means you can use the serialization attributes when you create a class. If it ever becomes necessary to provide your own extensions, you then add support for the ISerializable interface. As an example, consider how you would support MyType, version 2, when you add another field to your type. Simply adding a new field produces a new format that is incompatible with the previously stored versions on disk: [Serializable] public class MyType { 160 ❘ Chapter 3 Expressing Designs in C# From the Library of Wow! eBook ptg private MyType(SerializationInfo info, StreamingContext cntxt) { } private string label; [NonSerialized] private int value; private OtherClass otherThing; // Added in version 2 // The runtime throws Exceptions // with it finds this field missing in version 1.0 // files. private int value2; } Yo u a d d s u p p o r t f o r I S e r i a l i z a b l e t o a d d r e s s t h i s b e h a v i o r. T h e I S e r i a l i z a b l e interface defines one method, but you have to implement two. ISerializable defines the GetObjectData() method that is used to write data to a stream. In addition, you must provide a serialization constructor to initialize the object from the stream: private MyType(SerializationInfo info, StreamingContext cntxt) The serialization constructor in the following class shows how to read a previous version of the type and read the current version consistently with the default implementation generated by adding the Serializable attribute: using global::System.Runtime.Serialization; using global::System.Security.Permissions; [Serializable] public sealed class MyType : ISerializable { private string label; [NonSerialized] private int value; Item 27: Prefer Making Your Types Serializable ❘ 161 From the Library of Wow! eBook ptg private OtherClass otherThing; private const int DEFAULT_VALUE = 5; private int value2; // public constructors elided. // Private constructor used only // by the Serialization framework. private MyType(SerializationInfo info, StreamingContext cntxt) { label = info.GetString("label"); otherThing = (OtherClass)info.GetValue("otherThing", typeof(OtherClass)); try { value2 = info.GetInt32("value2"); } catch (SerializationException) { // Found version 1. value2 = DEFAULT_VALUE; } } [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] void ISerializable.GetObjectData(SerializationInfo inf, StreamingContext cxt) { inf.AddValue("label", label); inf.AddValue("otherThing", otherThing); inf.AddValue("value2", value2); } } The serialization stream stores each item as a key/value pair. The code gen- erated from the attributes uses the variable name as the key for each value. When you add the ISerializable interface, you must match the key name 162 ❘ Chapter 3 Expressing Designs in C# From the Library of Wow! eBook ptg and the order of the variables. The order is the order declared in the class. (By the way, this fact means that rearranging the order of variables in a class or renaming variables breaks the compatibility with files already created.) Also, I have demanded the SerializationFormatter security permission. GetObjectData could be a security hole into your class if it is not properly protected. Malicious code could create a StreamingContext, get the values from an object using GetObjectData, serialize modified versions to another SerializationInfo, and reconstitute a modified object. It would allow a malicious developer to access the internal state of your object, modify it in the stream, and send the changes back to you. Demanding the SerializationFormatter permission seals this potential hole. It ensures that only properly trusted code can access this routine to get at the internal state of the object. But there’s a downside to implementing the ISerializable interface. You can see that I made MyType sealed earlier. That forces it to be a leaf class. Implementing the ISerializable interface in a base class complicates serial- ization for all derived classes. Implementing ISerializable means that every derived class must create the protected constructor for deserialization. In addition, to support nonsealed classes, you need to create hooks in the GetObjectData method for derived classes to add their own data to the stream. The compiler does not catch either of these errors. The lack of a proper constructor causes the runtime to throw an exception when read- ing a derived object from a stream. The lack of a hook for GetObjectData() means that the data from the derived portion of the object never gets saved to the file. No errors are thrown. I’d like the recommendation to be “imple- ment Serializable in leaf classes.” I did not say that because that won’t work. Your base classes must be serializable for the derived classes to be serializable. To modify MyType so that it can be a serializable base class, you change the serializable constructor to protected and create a virtual method that derived classes can override to store their data: using global::System.Runtime.Serialization; using global::System.Security.Permissions; [Serializable] public class MyType : ISerializable { private string label; Item 27: Prefer Making Your Types Serializable ❘ 163 From the Library of Wow! eBook ptg [NonSerialized] private int value; private OtherClass otherThing; private const int DEFAULT_VALUE = 5; private int value2; // public constructors elided. // Protected constructor used only by the // Serialization framework. protected MyType(SerializationInfo info, StreamingContext cntxt) { label = info.GetString("label"); otherThing = (OtherClass)info.GetValue("otherThing", typeof(OtherClass)); try { value2 = info.GetInt32("value2"); } catch (SerializationException e) { // Found version 1. value2 = DEFAULT_VALUE; } } [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] void ISerializable.GetObjectData( SerializationInfo inf, StreamingContext cxt) { inf.AddValue("label", label); inf.AddValue("otherThing", otherThing); inf.AddValue("value2", value2); WriteObjectData(inf, cxt); } 164 ❘ Chapter 3 Expressing Designs in C# From the Library of Wow! eBook ptg // Overridden in derived classes to write // derived class data: protected virtual void WriteObjectData( SerializationInfo inf, StreamingContext cxt) { // Should be an abstract method, // if MyType should be an abstract class. } } A derived class would provide its own serialization constructor and over- ride the WriteObjectData method: public class DerivedType : MyType { private int derivedVal; private DerivedType(SerializationInfo info, StreamingContext cntxt) : base(info, cntxt) { derivedVal = info.GetInt32("_DerivedVal"); } protected override void WriteObjectData( SerializationInfo inf, StreamingContext cxt) { inf.AddValue("_DerivedVal", derivedVal); } } The order of writing and retrieving values from the serialization stream must be consistent. I’ve chosen to read and write the base class values first because I believe it is simpler. If your read and write code does not serial- ize the entire hierarchy in the exact same order, your serialization code won’t work. None of the code samples in this item use automatic properties. That’s by design. Automatic properties use a compiler-generated backing field for Item 27: Prefer Making Your Types Serializable ❘ 165 From the Library of Wow! eBook ptg their storage. You can’t access that backing field, because the field name is an invalid C# token (it is a valid CLR symbol). That makes binary seriali- zation very brittle for types that use automatic properties. You cannot write your own serialization constructor, or GetObjectData methods to access those backing fields. Serialization will work for the simplest types, but any derived classes, or future additional fields will break code. And, by the time you discover the problem, you’ll have persisted the original version in the field, and you won’t be able to fix the issue. Anytime you add the Serializable attribute to a class, you must concretely implement the properties with your own backing store. The .NET Framework provides a simple, standard algorithm for serializ- ing your objects. If your type should be persisted, you should follow the standard implementation. If you don’t support serialization in your types, other classes that use your type can’t support serialization, either. Make it as easy as possible for clients of your class. Use the default methods when you can, and implement the ISerializable interface when the default attrib- utes don’t suffice. Item 28: Create Large-Grain Internet Service APIs The cost and inconvenience of a communication protocol dictates how you should use the medium. You communicate differently using the phone, fax, letters, and email. Think back on the last time you ordered from a catalog. When you order by phone, you engage in a question-and- answer session with the sales staff: “Can I have your first item?” “Item number 123-456.” “How many would you like?” “Three.” This conversation continues until the sales staff has your entire order, your billing address, your credit card information, your shipping address, and any other information necessary to complete the transaction. It’s com- forting on the phone to have this back-and-forth discussion. You never give long soliloquies with no feedback. You never endure long periods of silence wondering if the salesperson is still there. Contrast that with ordering by fax. You fill out the entire document and fax the completed document to the company. One document, one transac- 166 ❘ Chapter 3 Expressing Designs in C# From the Library of Wow! eBook ptg tion. You do not fill out one product line, fax it, add your address, fax again, add your credit card number, and fax again. This illustrates the common pitfalls of a poorly defined service interface. Whether you use a Web service, .NET Remoting, or Azure-based pro- gramming, you must remember that the most expensive part of the oper- ation comes when you transfer objects between distant machines. You must stop creating remote APIs that are simply a repackaging of the same local interfaces that you use. It works, but it reeks of inefficiency. It’s using the phone call metaphor to process your catalog request via fax. Your appli- cation waits for the network each time you make a round-trip to pass a new piece of information through the pipe. The more granular the API is, the higher percentage of time your application spends waiting for data to return from the server. Instead, create Web-based interfaces based on serializing documents or sets of objects between client and server. Your remote communications should work like the order form you fax to the catalog company: The client machine should be capable of working for extended periods of time with- out contacting the server. Then, when all the information to complete the transaction is filled in, the client can send the entire document to the server. The server’s responses work the same way: When information gets sent from the server to the client, the client receives all the information necessary to complete all the tasks at hand. Sticking with the customer order metaphor, we’ll design a customer order- processing system that consists of a central server and desktop clients accessing information via Web services. One class in the system is the cus- tomer class. If you ignore the transport issues, the customer class might look something like this, which allows client code to retrieve or modify the name, shipping address, and account information: public class Customer { public Customer() { } // Properties to access and modify customer fields: public string Name { get; set; } public Address ShippingAddr { get; set; } Item 28: Create Large-Grain Internet Service APIs ❘ 167 From the Library of Wow! eBook [...]... otherCustomer = (Customer)obj; return this.CompareTo(otherCustomer); } #endregion // Relational Operators public static bool operator . but you have to implement two. ISerializable defines the GetObjectData() method that is used to write data to a stream. In addition, you must provide a serialization constructor to initialize. protected constructor for deserialization. In addition, to support nonsealed classes, you need to create hooks in the GetObjectData method for derived classes to add their own data to the stream customer on the server. Customer c = Server.NewCustomer(); // round trip to set the name. c.Name = dlg.Name; // round trip to set the addr. c.ShippingAddr = dlg.ShippingAddr; // round trip to