Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Alternative to using static factory (as cannot have abstract static methods)

Tags:

c#

inheritance

I'm trying to build a functional package parser. I have a base class Datagram, now I was naively imagining I'd have it defined like this:

(Note on edit, this class is not abstract!)

public class Datagram
{
    public abstract static Datagram CreateFromDatagram(Datagram datagram);
}

And then for specfic datagrams, say Ethernet and Tcp as an exampe:

public class EthernetDatagram : Datagram, IPayloadDatagram
{
    public override static Datagram CreateFromDatagram(Datagram datagram)
    {
        return new EthernetDatagram();
    }

    public Datagram Payload { get; }
}

public class TcpDatagram : Datagram, IPayloadDatagram
{
    public overrides static Datagram CreateFromDatagram(Datagram datagram)
    {
        return new TcpDatagram();
    }

    public Datagram Payload { get; }
}

The reason for this (impossible) abstract static method is I want to have an extension method which allows me to "chain" all these packets together:

public static class DatagramExtensions
{
    public static T As<T>(this IPayloadDatagram datagram) where T : Datagram
    {
        return (T)T.CreateFromDatagram(datagram.Payload);
    }
}

So, all I'd need to do to have a completely new datagram type ANewDatagram is have it define it's factory method CreateFromDatagram, and I'll be able to then merrily use my functional extension:

SomeDatagram.As<EthernetDatagram>().As<TcpDatagram>().As<ANewDatagram>()... 

and it'll all be extensible.

Given this isn't going to work as I can't inherit abstract classes, what would be a good alternative to instantiating a generic class like this?

I could use reflection, but then that's hiding it from the user. When I try create ANewDatagram I have to remember that I'm reflecting a CreateFromDatagram method later on.

I am currently using reflection to get the constructor - but there's no way for me to enforce there's a specific constructor that takes the payload. If someone creates a new Datagram there's no guarantee they add the right constructor, I'd have to inform them this in comments, which will very likely be missed, and the point of failure is at run-time at the latest possible point.

Is there a better alternative, architecturally, or with some form of interface/inheritence that could get around this issue?

(If anyone wants to see the full source code I'm working with, I'm trying to add these extensions to the packet interpretation library as part of https://github.com/PcapDotNet/Pcap.Net, with as little modification as possible)

like image 818
Joe Avatar asked Nov 28 '25 20:11

Joe


2 Answers

I would suggest a different solution. I would apply the Depdency Inversion Principle

Update with new Solution:

public class Datagram
{
    public byte[] Data { get; set; }
}


public interface IPayload
{
    Datagram Payload { get; }

}    

public interface IConvertible
{
    IPayload Convert(IPayload load);
}

public class EthernetDatagram : IPayload , IConvertible
{
    public Datagram Payload
    {
        get
        {
            return null;
        }
    }


    public IPayload Convert(IPayload load)
    {
        return new EthernetDatagram();
    }
}

public class TcpDatagram : IConvertible, IPayload
{
    public Datagram Payload
    {
        get
        {
            return null;
        }
    }

    public IPayload Convert(IPayload load)
    {
        return null;
    }
}

public static  class Extension
{
    public static IPayload As<T>(this IPayload load) where T : class, IConvertible, new()
    {
        IConvertible conv = new T();
        return conv.Convert(load);
    }
}

class Program
{
    static void Main(string[] args)
    {
        IPayload load = new TcpDatagram();

        var result = load.As<EthernetDatagram>();
    }
}

With this solution you go the other way around. You leave the path of hard reflection and move it to different layer of abstraction by passing the concrete type you want to 'convert' to and have full control. All you have to do for new datagrams is to implement both interfaces as long as you wish a converting between them. Is this more suitable for your problem?

Update to the comments:

The new solution will overcome the disadvantages of the former solution. No reflection and specification of the 'converted' type by a generic parameter. You can chain calls to 'As' together to reach exactly what you want. The only thing you now have to provide is the interface implementation, a default ctor and thats it.

like image 124
ckruczek Avatar answered Dec 01 '25 10:12

ckruczek


... but there's no way for me to enforce there's a specific constructor that takes the payload.

You could enforce it by declaring appropriate constructor in your base abstract class.

I also propose several modifications to your code. Since all of your derived classes are supposed to have the same Payload property, declare it in base Datagram class. Also consider declaring Datagram class as implementing IPayloadDatagram interface. It will make unnecessary to mark each derived class as implementing this interface.

Here is sample code that hopefully does what you need:

public interface IPayloadDatagram
{
    Datagram Payload { get; }
}

public abstract class Datagram : IPayloadDatagram
{
    public Datagram Payload { get; }

    protected Datagram(Datagram datagram)
    {
        Payload = datagram;
    }
}

public class EthernetDatagram : Datagram
{
    public EthernetDatagram(Datagram datagram) : base(datagram)
    {
    }
}

public static class DatagramExtensions
{
    public static T As<T>(this IPayloadDatagram datagram) where T : Datagram
    {
        return (T)Activator.CreateInstance(typeof(T), datagram.Payload);
    }
}
like image 37
CodeFuller Avatar answered Dec 01 '25 10:12

CodeFuller