Reputation: 1520
I have an interface which defines a class to be serializable to a byte array.
public interface IByteSerializable
{
byte[] GetBytes();
}
The natural partner to this is a deserialize method, which I want to return an object that implements IByteSerializable
.
I'm stuggling with how to design such an interface.
This doesn't seem to make sense:
public interface IByteSerializable
{
byte[] GetBytes();
IByteSerializable GetObject(byte[] bytes);
}
Because the implementation of GetObject()
can't be static
and it doesn't make sense to use a dummy IByteSerializable
object just to call the GetObject()
method to deserialize the actual object I'm after.
It also doesn't seem to make sense to do this:
public interface IByteSerializableFactory
{
IByteSerializable GetObject(byte[] bytes);
}
A Factory class could solve the problem, but this feels like it will result in class explosion. Also, the details of how a given IByteSerializable
subclass are serialized then deserialized are co-dependent, so it makes sense to keep them in the same place, and not in two different classes. Obviously, the exact process required to deserialize a given IByteSerializable
object depends entirely on how that object's GetBytes()
method was written.
Is there a common design or pattern I can use to solve this issue?
Upvotes: 5
Views: 1395
Reputation: 1212
There are a lot of different opinions on interfaces, classes, and patterns when it comes to your question. My personal preference would be implementing an interface with a byte[] property and an abstract class with a virtual method (or even losing the interface altogether, which might not be an option for you and does not play well with DI and unit testing):
public interface IByteSerializable
{
byte[] SerializableByteObject { get; }
}
public abstract class ByteSerializable : IByteSerializable
{
public byte[] SerializableByteObject { get; }
protected virtual byte[] GetBytes() {
return SerializableByteObject;
}
public abstract IByteSerializable GetObject();
//{ // You can make this method virtual and use Impl method:
// GetObjectImpl(SerializableByteObject);
//}
protected internal IByteSerializable GetObjectImpl(byte[] bytes) {
// If you need a default implementation (GetObject() should be protected virtual then)
// return IByteSerializable...;
}
}
I want to stress that interfaces VS abstract classes is an endless discussion. If you can do stuff without implementing interfaces and use just abstract classes - I would strongly recommend doing so.
Update 3/18/17: to reply to a comment (defining behavior is the purpose of an interface) and explain how I see it adding the explanation below.
In this scenario, the "behavior" we are defining is "An object should be convertible into a byte array. Conversion result should be convertible back into the same object." So we're actually defining behavior for an object and for a byte array (because after an object was deserialized - it's no longer the same object, it's just a byte array).
From my perspective, that's pure factory pattern scenario.
// Let's define an interface for our serializable type of objects factory
public interface IByteSerializableFactory<T>
{
T CreateFromBytes(byte[] objectDataToUse);
byte[] CovertToBytes(T objectToConvert);
}
// Interface for any class that needs a serialization factory
// This is not even necessary, but I like it to enforce people to implement simple methods that reference the factory.
public interface IByteSerializable<T>
{
IByteSerializableFactory<T> GetFactory();
}
// Now a moment comes for us to have this kind of class. We need to build a factory first (because our interface requires a GetFactory() implementation. We can lose the IByteSerializable interface altogether, but then we lose a way to let people know which factory should be used.
public class SomeBaseClassSerializationFactory : IByteSerializableFactory<SomeBaseClass>
{
public SomeBaseClass CreateFromBytes(byte[] objectDataToUse) { //...
return new SomeClass();
}
public byte[] CovertToBytes(SomeBaseClass objectToConvert) { //...
return new byte[1];
}
}
// We have a factory, let's implement a class.
public abstract class SomeBaseClass : IByteSerializable<SomeBaseClass>
{
public virtual IByteSerializableFactory<SomeBaseClass> GetFactory() {
return new SomeBaseClassSerializationFactory();
}
}
public class SomeClass : SomeBaseClass {
// Now we're independent. Our derived classes do not need to implement anything.
// If the way the derived class is serialized is different - we simply override the method
}
Update 2 3/18/17: to reply to a comment under a different answer (generic implementation with simple use of interface).
Unfortunately, there will be no clean way to do it. There is a dirty (my personal opinion: "BAD BAD BAD!") way by using some cheating, defining classes that define serialization methods and using reflection to return correct type. The example below will require a lot of custom logic in the serialization method for using correct fields with different types:
// You define an enum with action and a dictionary with a collection of serialization methods.
public enum SerializationAction {
ToBytes,
ToObject
}
// It can also be an enum, but it's easier to test with a collection of strings.
public static readonly string[] SerializationKindList = new string[] {
"FirstKind",
"SecondKind"
};
// This generic class can have an implementation of all the handlers. Additional switching can be done by type, or reflection can be used to find properties for different classes and construct different classes.
public class SerializationMethod {
public object ProcessByKind (string kindToUse, SerializationAction action, object objectToProcess) {
if (kindToUse == "FirstKind") {
if (action == SerializationAction.ToBytes) {
return new byte[1];
}
return new SomeClass(); // These would need to be your hard implementations. Not clean.
} else {
throw new NotImplementedException();
}
}
}
// This struct type defines the serialization method and is required for the interface implementation
public struct ByteSerialization
{
public string SerializationTypeName { get; private set; }
public ByteSerialization(string kindToUse) {
if (!SerializationKindList.Contains(kindToUse)) {
throw new ArgumentException();
}
SerializationTypeName = kindToUse;
}
public byte[] Deserialize(object objectToProcess) {
var serializationMethod = new SerializationMethod();
return (byte[])serializationMethod.ProcessByKind(this.SerializationTypeName, SerializationAction.ToBytes, objectToProcess);
}
public object Serialize(byte[] byteArrayToProcess) {
var serializationMethod = new SerializationMethod();
return serializationMethod.ProcessByKind(this.SerializationTypeName, SerializationAction.ToObject, byteArrayToProcess);
}
}
// Interface for any class that needs to use generic serialization
public interface IByteSerializable
{
ByteSerialization serializationType { get; }
}
// Creating extension methods for the interface to make the life easier
public static class IByteSerializableExtensions {
public static byte[] DeserializeObjectIntoBytes(this IByteSerializable objectToProcess) {
return objectToProcess.serializationType.Deserialize(objectToProcess);
}
public static void SerializeObjectFromBytes(this IByteSerializable objectToProcess, byte[] fromBytes) {
var someObjectData = objectToProcess.serializationType.Serialize(fromBytes);
}
}
// Abstract base class implementation with static readonly field.
// Only downside - there is no way to enforce the config of this field in the constructor from the interface.
// There also no way to make sure this field always gets set for other implementations of IByteSerializable
public abstract class SomeBaseClass : IByteSerializable
{
private static readonly ByteSerialization _serializationType = new ByteSerialization("FirstKind");
public ByteSerialization serializationType { get { return _serializationType; } }
}
public class SomeClass : SomeBaseClass {
}
// And here's how one would use it. You will need to create a new object of the class before serializing from bytes.
var someClass = new SomeClass();
var bytes = someClass.DeserializeObjectIntoBytes();
var someClass2 = new SomeClass();
var byteArray = new byte[1];
someClass2.SerializeObjectFromBytes(byteArray);
Upvotes: 4
Reputation: 27039
Use a generic interface and each implementation can close the generic and return the closed type. It is upto the implementation to decide what type to return.
public interface ICustomSerializable<T> where T : class
{
byte[] GetBytes();
T Deserialize(byte[]);
}
public class Foo : ICustomSerializable<Foo>
{
public byte[] GetBytes() {}
public Foo Deserialize(byte[]){}
}
public class Bar : ICustomSerializable<Bar>
{
public byte[] GetBytes() {}
public Bar Deserialize(byte[]){}
}
If you have classes which do serialization in a common way then:
public abstract class Something
{
public byte[] GetBytes() { //common code }
}
public class SomethingConcrete : Something, ICustomSerializable<SomethingConcrete>
{
public SomethingConcrete Deserialize(byte[]){}
}
Upvotes: 2