For a library (.NET Standard 2.0), I designed some classes that look roughly like this:
public class MyContext
{
// wraps something important.
// It can be a native resource, a connection to an external system, you name it.
}
public abstract class Base
{
public Base(MyContext context)
{
Context = context;
}
protected MyContext Context { get; private set; }
// methods
}
public class C1 : Base
{
public C1(MyContext context, string x)
: base(context)
{
// do something with x
}
// methods
}
public class C2 : Base
{
public C2(MyContext context, long k, IEnumerable<C1> list)
: base(context)
{
// do something with the list of C1s.
}
// methods
}
public class C3 : Base
{
public C3(MyContext context, int y, double z)
: base(context)
{
// do something with y and z.
}
public void DoSomething()
{
var something = new C2(this.Context, 2036854775807, new[]
{
new C1(this.Context, "blah"),
new C1(this.Context, "blahblah"),
new C1(this.Context, "blahblahblah"),
}
);
}
// other methods
}
// other similar classes
The classes were criticized because of that "MyContext" parameter that every constructor requires. Some people say that it clutters the code.
I said that the parameter is needed to propagate the context, so that e.g. when you call the DoSomething method of C3, the contained instances of C1 and C2 all share the same context.
A potential alternative is defining the parameter as Base
instead of MyContext
, e.g.:
public abstract class Base
{
public Base(Base b) // LOOKS like a copy constructor, but it isn't!
{
Context = b.Context;
}
protected MyContext Context { get; private set; }
// methods
}
public class C1 : Base
{
public C1(Base b, string x)
: base(b)
{
// do something with x
}
// methods
}
public class C2 : Base
{
public C2(Base b, long k, IEnumerable<C1> list)
: base(b)
{
// do something with the list of C1s.
}
// methods
}
public class C3 : Base
{
public C3(Base b, int y, double z)
: base(b)
{
}
public void DoSomething()
{
var something = new C2(this, 2036854775807, new[]
{
new C1(this, "blah"),
new C1(this, "blahblah"),
new C1(this, "blahblahblah"),
}
);
}
// other methods
}
// other similar classes
Now the parameter you must pass is shorter, but it still raised some eyebrows.
(And I don't like it very much, because logically a C1/C2/C3/etc object does not require a Base
, it requires a MyContext
. Not to mention that constructor that seems a copy constructor, but it is not! Oh, and now how can I initialize C1, C2 or C3 outside a Base
-derived class, since all of them want a Base
, and Base
is abstract? I need to "bootstrap" the system somewhere... I guess all classes may have two constructors, one with Base and one with MyContext
... but having two constructors for each class may be a nightmare for future maintainers...).
Some people said "Why don't you turn MyContext
into a singleton, like in Project X?".
Project X has a similar class structure but, "thanks" to the singleton, classes don't have a MyContext
parameter: they "just work". It's magic!
I don't want to use a singleton (or static data in general), because:
- singletons are actually global variables, global variables are a bad programming practice. I used global variables many times, and many times I regretted my decision, so now I prefer to avoid global variables if possible.
- with a singleton, classes share a context, but only ONE context: you cannot have MULTIPLE contexts. I find it too limiting for a library that will be used by an unspecified number of people in the company.
Some people said that we should not be too anchored to "academic theory", useless ideas of "good" and "bad" programming practices, that we should not overengineer classes in the name of abstract "principles". Some people said that we should not fall into the trap of "passing around ginormous context objects". Some people said YAGNI: they're pretty sure that 99% of the usage will involve exactly one context.
I don't want to anger that 1% by disallowing their use case, but I don't want to frustrate the other 99% with classes that are difficult to use.
So I thought about a way to have the cake and eat it.
For example: MyContext
is concrete but it ALSO has a static instance of itself. All classes (Base, C1, C2, C3, etc) have two constructors: one with a MyContext
concrete parameter, the other one without it (and it reads from the static MyContext.GlobalInstance
).
I like it... and at the same time I don't, because again it requires to maintain two constructors for each class, and I'm afraid it can be too error-prone (use the incorrect overload just once and the entire structure collapses, and you find out only at runtime).
So forget for the moment the "static AND non static" idea.
I tried to imagine something like this:
public abstract class Base
{
public Base(MyContext context)
{
Context = context;
}
protected MyContext Context { get; private set; }
public T Make<T>(params object[] args) where T : Base
{
object[] arrayWithTheContext = new[] { this.Context };
T instance = (T)Activator.CreateInstance(typeof(T),
arrayWithTheContext.Concat(args).ToArray());
return instance;
}
// other methods
}
public class C1 : Base
{
public C1(MyContext context, string x)
: base(context)
{
// do something with x
}
// methods
}
public class C2 : Base
{
public C2(MyContext context, long k, IEnumerable<C1> list)
: base(context)
{
// do something with the list of C1s.
}
// methods
}
public class C3 : Base
{
public C3(MyContext context, int y, double z)
: base(context)
{
// do something with y and z.
}
public void DoSomething()
{
var something = Make<C2>(2036854775807, new[]
{
Make<C1>("blah"),
Make<C1>("blahblah"),
Make<C1>("blahblahblah"),
}
);
}
// other methods
}
// other similar classes
The calls to Make
LOOK better, but they are even more error-prone, because the signature of Make
is not strongly-typed. My goal is to simplify doing stuff for the users. If I opened a parenthesis and Intellisense proposed me ... an array of System.Object
, I'd be VERY frustrated. I could pass incorrect parameter and I would find out only at runtime (Since good old .NET Framework 2.0, I hoped to forget about arrays of System.Object
...)
So what? A factory with a dedicated, strongly-typed method for each Base-derived class would be type-safe, but maybe it would also be "ugly" to see (and a textbook violation of the open-close principle. Yes, "principles", again):
public class KnowItAllFactory
{
private MyContext _context;
public KnowItAllFactory(MyContext context) { _context = context; }
public C1 MakeC1(string x) { return new C1(_context, x); }
public C2 MakeC2(long k, IEnumerable<C1> list) { return new C2(_context, k, list); }
public C3 MakeC3(int y, double z) { return new C3(_context, y, z); }
// and so on
}
public abstract class Base
{
public Base(MyContext context)
{
Factory = new KnowItAllFactory(context);
}
protected MyContext Context { get; private set; }
protected KnowItAllFactory Factory { get; private set; }
// other methods
}
public class C1 : Base
{
public C1(MyContext context, string x)
: base(context)
{
// do something with x
}
// methods
}
public class C2 : Base
{
public C2(MyContext context, long k, IEnumerable<C1> list)
: base(context)
{
// do something with the list of C1s.
}
// methods
}
public class C3 : Base
{
public C3(MyContext context, int y, double z)
: base(context)
{
// do something with y and z.
}
public void DoSomething()
{
var something = Factory.MakeC2(2036854775807, new[]
{
Factory.MakeC1("blah"),
Factory.MakeC1("blahblah"),
Factory.MakeC1("blahblahblah"),
}
);
}
// other methods
}
// other similar classes
EDIT (after the comments): I forgot to mention that yes, someone suggested me about using a DI/IoC framework like in "Project Y". But apparently "Project Y" uses it only to decide if instantiate MyConcreteSomething
or MyMockSomething
when ISomething
is needed (please consider mocking and unit tests out of scope for this question, because reasons), and it passes around the DI/IOC framework objects exactly like I pass around my context...
public MyRepository(IAmbientDbContextLocator contextLocator)
- you don't have to create it in every service method, but you could, if needed. – Selfreliance