Search code examples
c#ooprefactoring

How to refactor this code with two same class name and same helper methods with different return types?


I have two classes like below:

public async A GernerateStuff(int expireDays = 15)
{
    using var randomNumberGenerator = RandomNumberGenerator.Create();
    var randomBytes = new byte[64];
    var now = DateTime.UtcNow;
    randomNumberGenerator.GetBytes(randomBytes);
    return new A
    {
        Stuff = Convert.ToBase64String(randomBytes),
        Created = now,
        Expires = now.AddDays(expireDays)
    };
}

public async B GernerateStuff(int expireDays = 10)
{
    using var randomNumberGenerator = RandomNumberGenerator.Create();
    var randomBytes = new byte[64];
    var now = DateTime.UtcNow;
    randomNumberGenerator.GetBytes(randomBytes);

    return new B
    {
        Stuff = Convert.ToBase64String(randomBytes),
        Created = now,
        Expires = now.AddDays(expireDays)
    };
}

public class A
{
    public string Stuff{ get; set; }
    public DateTime Created { get; set; }
    public DateTime Expires { get; set; }
}

public class B
{
    public string Stuff{ get; set; }
    public DateTime Created { get; set; }
    public DateTime Expires { get; set; }
}

The constraint is: I can not create just one class instead of two separate classes A and B as they have significant differences in usage.

Now, my question is: how can I clean this code up having both classes A and B but a single method for GernerateStuff?

I can create an interface like this:

public class A : IInterface
{
}

public class B : IInterface
{

}

public interface IInterface
{
    public string Stuff{ get; set; }
    public DateTime Created { get; set; }
    public DateTime Expires { get; set; }
}

Then, the problem is how public async IInterface GernerateStuff(int expireDays = 15) signature would handle both class A and B?


Solution

  • There are a couple of ways tot achieve this. One has already been mentioned in other answeres, which is about using generics. However this assumes your types A and B do even have anything in common that you could use as common generic constraint - e.g a common base-interface. Then you could do this:

    public async T GernerateStuff<T>(int expireDays = 15) where T: new(), MyInterface
    {
        using var randomNumberGenerator = RandomNumberGenerator.Create();
        var randomBytes = new byte[64];
        var now = DateTime.UtcNow;
        randomNumberGenerator.GetBytes(randomBytes);
        return new T
        {
            Stuff = Convert.ToBase64String(randomBytes),
            Created = now,
            Expires = now.AddDays(expireDays)
        };
    }
    

    You could also create a factory that creates the instances of A or B depending on some condition - e.g. some configuration. However you'd also need a common base-interface for that:

    public async MyInterface GernerateStuff(int expireDays = 15)
    {
        using var randomNumberGenerator = RandomNumberGenerator.Create();
        var randomBytes = new byte[64];
        var now = DateTime.UtcNow;
        randomNumberGenerator.GetBytes(randomBytes);
        return CreateTheThing(Convert.ToBase64String(randomBytes), now, now.AddDays(expireDays));
    }
    
    MyInterface CreateTheThing(string stuff, DateTime created, DateTime expires)
    {
        if(...)
            return new A { ... }
        else if(...)
            return new B { ... }
        return new C { ... }
    }
    

    This solution has the advantage, that you don't need to change the client-logic when you add a new type to the factory. All you need to change is the factory itself by introducing new C { ... }. Furthermor a client cannot provide any types that actually don't work, as they don't provide any type-information at all.

    By the way your method does not await anything, so there's no reason to make it async.