I have two objects that are similar but not quite the same:
public class ObjectA
{
public string PropertyA { get; set; }
}
public class ObjectB
{
public string PropertyA { get; set; }
public string PropertyB { get; set; }
}
In both objects PropertyA
is used for database lookup via a stored procedure, however, the stored procedure used differs between the objects. Assuming the stored procedure returns successfully, the response is used to create the relevant object.
Currently, I have two methods that perform the same logic of calling the relevant stored procedure and then creating the object:
public ObjectA QueryObjectA(string propertyA)
{
// shared code
var result = CallToStoredProcedureA(...);
var obj = new ObjectA
{
// use result here
};
return obj;
}
public ObjectB QueryObjectB(string propertyA)
{
// shared code
var result = CallToStoredProcedureB(...);
var obj = new ObjectB
{
// use result here
};
return obj;
}
This works, however, I am maintaining some shared code in multiple places and if I were to add another object in the future I would potentially have a third method (QueryObjectC
).
Can this, or should this, be improved through the use of generics, and if so what might that look like? Perhaps something along the lines of the following?
public T QueryObject<T>(string propertyA) where T : new()
{
// shared code
// get the type of T
// switch statement that calls the relevant stored procedure and creates the object
// return object
}
CodePudding user response:
So either way, you're probably going to want to create a common interface between A and B. We'll call it IBase
Now the simplest solution would be to try move this procedure to inside IBase, and let polymorphism do the hard work for you
public T QueryObject<T>() where T: IBase, new()
{
var obj = new T();
var result = obj.DoProcedure(...);
obj.AssignResults(result); // could be a part of DoProcedure
return obj;
}
with
interface IBase
{
ResultType DoProcedure(...);
}
and each A or B would impliment the correct call (possibly using a visitor pattern if needed)
If however this procedure really isn't the responsibility of A or B (from the looks of it it could be, but if it's not) then you can upgrade to the slightly more heavy duty solution of a Factory of some kind.
You'll want a similar interface as before, but this one you can leave empty
interface IBase
{ }
And a IProcedureExecutor interface (name can be improved
interface IProcedureExecutor
{
ResultType DoProcedure(...);
}
The implimemtors would each impliment the function, ATypeProcExecutor : IProcedureExexutor
, BTypeProcExecutor : IProcedureExexutor
Then a Factory
class ProcedureExecutorFactory
{
private static Dictionary<Type, IProcedureExecutor> executors = new
{
{typeof(A), new ATypeProcExecutor},
{typeof(B), new BTypePrpcExecutor}
};
public static IProcedureExecutor GetExecutor(Type t)
{
return executors[t];
}
}
Then your QueryObject function would be
public T QueryObject<T>() where T: IBase, new()
{
var executor = ProcedureExecutorFactory.GetExecutor(typeof(T));
var result = executor.DoProcedure(...);
var obj = new T();
obj.AssignResults(result);// could be part of constructor
return obj;
}
The second is probably more SOLID but might well be overkill for your problem.
You mentioned a switch
statement in your question's sudocode, this is a bad idea. A generic function that switches based on the type of T
is really loosing all the flexibility benifits gained by using a generic at all.
Wanting to do something different based on the type of an object is really the heart of OOP, so making good use of polymorphism is really the way to go here.
(on my phone so code untested)
CodePudding user response:
By using a common abstract base class ObjectBase
having the property PropertyA
you could write
public T QueryObject<T>(string propertyA) where T : ObjectBase, new()
{
var result = typeof(T).Name switch {
nameof(ObjectA) => CallToStoredProcedureA(...),
nameof(ObjectB) => CallToStoredProcedureB(...),
_ => throw new NotImplementedException()
};
var obj = new T {
PropertyA = result
};
return obj;
}
If the initialization is different for the different object types, then you could consider adding a static factory method to these types.
Objects with factory methods:
public class ObjectA : ObjectBase
{
public static ObjectA Create(string storedProcResult) =>
new() { PropertyA = storedProcResult };
}
public class ObjectB : ObjectBase
{
public string PropertyB { get; set; }
public static ObjectB Create(string storedProcResult) =>
new() {
PropertyA = storedProcResult.Substring(0, 3),
PropertyB = storedProcResult.Substring(3)
};
}
Generic method using the factory methods:
public T QueryObject<T>(string propertyA) where T : ObjectBase, new()
{
ObjectBase obj = typeof(T).Name switch {
nameof(ObjectA) => ObjectA.Create(CallToStoredProcedureA(...)),
nameof(ObjectB) => ObjectB.Create(CallToStoredProcedureB(...)),
_ => throw new NotImplementedException()
};
return (T)obj;
}