在 C# class 库中传递上下文,寻找不使用静态的 "easy" 方式
Passing the context around in a C# class library, looking for an "easy" way without using static
对于库 (.NET Standard 2.0),我设计了一些 classes,大致如下所示:
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
classes 因每个构造函数都需要的“MyContext”参数而受到批评。
有人说它使代码混乱。
我说过需要参数来传播上下文,例如当您调用 C3 的 DoSomething 方法时,包含的 C1 和 C2 实例都共享相同的上下文。
一个潜在的替代方案是将参数定义为 Base
而不是 MyContext
,例如:
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
现在必须传递的参数变短了,但仍然引起了一些关注。
(而且我不太喜欢它,因为逻辑上 C1/C2/C3/etc 对象不需要 Base
,它需要 MyContext
。更不用说那个构造函数了似乎是复制构造函数,但它不是!哦,现在我如何在 Base
派生的 class 之外初始化 C1、C2 或 C3,因为它们都需要 Base
, Base
是抽象的吗?我需要在某处“bootstrap”系统...我想所有 classes 可能有两个构造函数,一个带有 Base,一个带有 MyContext
...但是每个 class 都有两个构造函数可能是未来维护者的噩梦...)。
有人说“你为什么不把 MyContext
变成一个单例,就像在 Project X 中那样?”。
项目 X 具有类似的 class 结构,但是,“感谢”单例,classes 没有 MyContext
参数:它们“正常工作”。这是魔法!
我不想使用单例(或一般的静态数据),因为:
- 单例实际上是全局变量,全局变量是一种糟糕的编程习惯。我多次使用全局变量,也有很多次后悔我的决定,所以现在我更愿意尽可能避免使用全局变量。
- 对于单例,classes 共享一个上下文,但只有一个上下文:您不能有多个上下文。我发现它对于将由公司中未指定数量的人使用的库来说太局限了。
有人说我们不应该太拘泥于“学术理论”,“好的”和“坏的”编程实践的无用想法,我们不应该以抽象的名义过度设计 classes “原则”。
有人说我们不应该陷入“绕过巨大的上下文对象”的陷阱。
有人说 YAGNI:他们非常确定 99% 的使用将只涉及一个上下文。
我不想因为不允许他们的用例而激怒那 1%,但我不想让其他 99% 的 class 难以使用。
于是我想了个办法,有饼就有吃。
例如:MyContext
是具体的,但它也有一个自身的静态实例。所有 classes(Base、C1、C2、C3 等)都有两个构造函数:一个带有 MyContext
具体参数,另一个没有它(它从静态 MyContext.GlobalInstance
).
我喜欢它......同时我不喜欢它,因为它再次需要为每个 class 维护两个构造函数,而且我担心它太容易出错(使用不正确的重载只是一旦整个结构崩溃,你只能在运行时发现)。
所以暂时忘记“静态和非静态”的想法。
我试着想象这样的事情:
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
对 Make
的调用看起来更好,但它们更容易出错,因为 Make
的签名不是强类型的。我的目标是简化为用户做的事情。如果我打开一个括号,然后 Intellisense 向我推荐了一个 System.Object
数组,我会非常沮丧。我可能会传递不正确的参数,而且我只会在运行时发现(因为旧的 .NET Framework 2.0,我希望忘记 System.Object
... 的数组)
那又怎样?为每个 Base 派生的 class 提供专用的强类型方法的工厂将是类型安全的,但也许它看起来也很“丑陋”(并且违反了开闭原则的教科书。是的,“原则”,再次):
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(在评论之后):我忘了说是的,有人建议我使用 DI/IoC 框架,例如“Y 项目”。但显然“项目 Y”仅使用它来决定在需要 ISomething
时是实例化 MyConcreteSomething
还是 MyMockSomething
(请考虑模拟和单元测试超出此问题的范围,因为原因),它传递 DI/IOC 框架对象就像我传递我的上下文一样...
在 OP 中列出的备选方案中,原始版本是最好的。正如 Zen of Python 所述,显式优于隐式 。
使用单例会使显式依赖隐式化。那不会提高可用性;它使事情变得更加不清楚。它使 API 更难使用,因为 API 的用户在成功使用它之前必须 学习 的东西更多(相对于只需查看构造函数签名并提供使代码编译的参数。
使用 API 是否需要多敲几下键并不重要。在编程中,typing isn't a bottleneck.
通过构造函数传递 MyContext
只是普通的旧构造函数注入,即使它是 concrete dependency. Don't let the Dependency Injection (DI) jargon fool you, though. You don't need a DI Container to use those patterns.
您可能认为改进的是应用 Interface Segregation Principle (ISP)。基础 class 是否需要整个 MyContext
对象?它可以只用 MyContext
的 API 的一个子集来实现它的方法吗?
基础 class 根本需要 MyContext
,还是只是为了派生的 classes?
你还需要基地吗class?
根据我的经验,您总能想出不使用继承的更好设计。我不知道在这种情况下会是什么,但是像下面这样的东西怎么样?
public class C1
{
public C1(MyContext context, string x)
{
// Save context and x in class fields for later use
}
// methods
}
public class C2
{
public C2(MyContext context, long k, IEnumerable<C1> list)
{
// Save context, k, and the list of C1s in class fields for later use
}
// methods
}
public class C3
{
public C3(MyContext context, int y, double z)
{
Context = contex;
// do something with y and z.
}
public MyContext Context { get; }
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
}
在重用 MyContext
的单个实例时,无论您是否拥有基础 class 都没有区别。即使这三个 class 没有共同的超类型,您仍然可以这样做。
对于库 (.NET Standard 2.0),我设计了一些 classes,大致如下所示:
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
classes 因每个构造函数都需要的“MyContext”参数而受到批评。 有人说它使代码混乱。
我说过需要参数来传播上下文,例如当您调用 C3 的 DoSomething 方法时,包含的 C1 和 C2 实例都共享相同的上下文。
一个潜在的替代方案是将参数定义为 Base
而不是 MyContext
,例如:
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
现在必须传递的参数变短了,但仍然引起了一些关注。
(而且我不太喜欢它,因为逻辑上 C1/C2/C3/etc 对象不需要 Base
,它需要 MyContext
。更不用说那个构造函数了似乎是复制构造函数,但它不是!哦,现在我如何在 Base
派生的 class 之外初始化 C1、C2 或 C3,因为它们都需要 Base
, Base
是抽象的吗?我需要在某处“bootstrap”系统...我想所有 classes 可能有两个构造函数,一个带有 Base,一个带有 MyContext
...但是每个 class 都有两个构造函数可能是未来维护者的噩梦...)。
有人说“你为什么不把 MyContext
变成一个单例,就像在 Project X 中那样?”。
项目 X 具有类似的 class 结构,但是,“感谢”单例,classes 没有 MyContext
参数:它们“正常工作”。这是魔法!
我不想使用单例(或一般的静态数据),因为:
- 单例实际上是全局变量,全局变量是一种糟糕的编程习惯。我多次使用全局变量,也有很多次后悔我的决定,所以现在我更愿意尽可能避免使用全局变量。
- 对于单例,classes 共享一个上下文,但只有一个上下文:您不能有多个上下文。我发现它对于将由公司中未指定数量的人使用的库来说太局限了。
有人说我们不应该太拘泥于“学术理论”,“好的”和“坏的”编程实践的无用想法,我们不应该以抽象的名义过度设计 classes “原则”。 有人说我们不应该陷入“绕过巨大的上下文对象”的陷阱。 有人说 YAGNI:他们非常确定 99% 的使用将只涉及一个上下文。
我不想因为不允许他们的用例而激怒那 1%,但我不想让其他 99% 的 class 难以使用。
于是我想了个办法,有饼就有吃。
例如:MyContext
是具体的,但它也有一个自身的静态实例。所有 classes(Base、C1、C2、C3 等)都有两个构造函数:一个带有 MyContext
具体参数,另一个没有它(它从静态 MyContext.GlobalInstance
).
我喜欢它......同时我不喜欢它,因为它再次需要为每个 class 维护两个构造函数,而且我担心它太容易出错(使用不正确的重载只是一旦整个结构崩溃,你只能在运行时发现)。
所以暂时忘记“静态和非静态”的想法。
我试着想象这样的事情:
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
对 Make
的调用看起来更好,但它们更容易出错,因为 Make
的签名不是强类型的。我的目标是简化为用户做的事情。如果我打开一个括号,然后 Intellisense 向我推荐了一个 System.Object
数组,我会非常沮丧。我可能会传递不正确的参数,而且我只会在运行时发现(因为旧的 .NET Framework 2.0,我希望忘记 System.Object
... 的数组)
那又怎样?为每个 Base 派生的 class 提供专用的强类型方法的工厂将是类型安全的,但也许它看起来也很“丑陋”(并且违反了开闭原则的教科书。是的,“原则”,再次):
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(在评论之后):我忘了说是的,有人建议我使用 DI/IoC 框架,例如“Y 项目”。但显然“项目 Y”仅使用它来决定在需要 ISomething
时是实例化 MyConcreteSomething
还是 MyMockSomething
(请考虑模拟和单元测试超出此问题的范围,因为原因),它传递 DI/IOC 框架对象就像我传递我的上下文一样...
在 OP 中列出的备选方案中,原始版本是最好的。正如 Zen of Python 所述,显式优于隐式 。
使用单例会使显式依赖隐式化。那不会提高可用性;它使事情变得更加不清楚。它使 API 更难使用,因为 API 的用户在成功使用它之前必须 学习 的东西更多(相对于只需查看构造函数签名并提供使代码编译的参数。
使用 API 是否需要多敲几下键并不重要。在编程中,typing isn't a bottleneck.
通过构造函数传递 MyContext
只是普通的旧构造函数注入,即使它是 concrete dependency. Don't let the Dependency Injection (DI) jargon fool you, though. You don't need a DI Container to use those patterns.
您可能认为改进的是应用 Interface Segregation Principle (ISP)。基础 class 是否需要整个 MyContext
对象?它可以只用 MyContext
的 API 的一个子集来实现它的方法吗?
基础 class 根本需要 MyContext
,还是只是为了派生的 classes?
你还需要基地吗class?
根据我的经验,您总能想出不使用继承的更好设计。我不知道在这种情况下会是什么,但是像下面这样的东西怎么样?
public class C1
{
public C1(MyContext context, string x)
{
// Save context and x in class fields for later use
}
// methods
}
public class C2
{
public C2(MyContext context, long k, IEnumerable<C1> list)
{
// Save context, k, and the list of C1s in class fields for later use
}
// methods
}
public class C3
{
public C3(MyContext context, int y, double z)
{
Context = contex;
// do something with y and z.
}
public MyContext Context { get; }
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
}
在重用 MyContext
的单个实例时,无论您是否拥有基础 class 都没有区别。即使这三个 class 没有共同的超类型,您仍然可以这样做。