Tee*_*ary 6 c# design-patterns class-design class-hierarchy .net-standard
对于库(.NET Standard 2.0),我设计了一些大致如下所示的类:
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
Run Code Online (Sandbox Code Playgroud)
这些类因为每个构造函数都需要“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
Run Code Online (Sandbox Code Playgroud)
现在你必须传递的参数更短了,但它仍然引起了一些人的注意。
(而且我不太喜欢它,因为从逻辑上讲,C1/C2/C3/etc 对象不需要Base,它需要MyContext。更不用说看起来像复制构造函数的构造函数,但它不是!哦,还有现在如何在Base派生类之外初始化 C1、C2 或 C3 ,因为它们都需要一个Base,并且Base是抽象的?我需要在某处“引导”系统......我猜所有类可能有两个构造函数,一个带有Base 和一个 with MyContext... 但是每个类有两个构造函数对于未来的维护者来说可能是一场噩梦......)。
有人说“你为什么不MyContext变成一个单身人士,就像在 X 项目中一样?”。Project X 具有类似的类结构,但是,“感谢”单例,类没有MyContext参数:它们“只是工作”。这是魔法!我不想使用单例(或一般的静态数据),因为:
有人说,我们不应该过于锚定“学术理论”,“好的”和“坏的”编程实践的无用想法,我们不应该以抽象的“原则”的名义过度设计类。有人说我们不应该陷入“传递大量上下文对象”的陷阱。有些人说 YAGNI:他们很确定 99% 的用法将只涉及一种上下文。
我不想通过禁止他们的用例来激怒那 1% 的人,但我不想用难以使用的类来挫败其他 99% 的人。
所以我想了一个方法来吃蛋糕。例如:MyContext是具体的,但它也有自己的静态实例。所有类(Base、C1、C2、C3 等)都有两个构造函数:一个带有MyContext具体参数,另一个没有它(它从 static 中读取MyContext.GlobalInstance)。我喜欢它......同时我不喜欢它,因为它再次需要为每个类维护两个构造函数,而且我担心它太容易出错(只使用一次错误的重载和整个结构崩溃,您只能在运行时发现)。所以暂时忘记“静态和非静态”的想法。
我试着想象这样的事情:
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
Run Code Online (Sandbox Code Playgroud)
对MakeLOOK的调用更好,但它们更容易出错,因为 的签名Make不是强类型的。我的目标是简化为用户做事。如果我打开一个括号并且 Intellisense 向我提出了 ... 的数组System.Object,我会非常沮丧。我可以传递不正确的参数,我只会在运行时发现(因为旧的 .NET Framework 2.0,我希望忘记System.Object......的数组)
所以呢?为每个 Base 派生类提供专用的强类型方法的工厂将是类型安全的,但也许它看起来也很“丑陋”(并且违反了开闭原则。是的,“原则” , 再次):
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
Run Code Online (Sandbox Code Playgroud)
编辑(在评论之后):我忘了提到是的,有人建议我使用“项目 Y”中的 DI/IoC 框架。但显然“项目 Y”仅使用它来决定是否实例化MyConcreteSomething或MyMockSomething何时ISomething需要(请考虑模拟和单元测试超出此问题的范围,因为原因),并且它传递 DI/IOC 框架对象就像我传递的一样我的上下文...
在 OP 列出的替代方案中,原始方案是最好的。正如Python 禅宗所说,显式优于隐式。
使用单例会使显式依赖变得隐式。这并不能提高可用性;这让事情变得更加不清楚。它使 API 更难使用,因为 API 用户在成功使用它之前必须学习更多的东西(而不是仅仅查看构造函数签名并提供使代码编译的参数)。
API 是否需要更多的击键来使用并不重要。在编程中,输入并不是瓶颈。
通过构造函数传递MyContext只是普通的旧构造函数注入,即使它是一个具体的依赖项。不过,不要让依赖注入 (DI) 术语欺骗了您。您不需要 DI 容器来使用这些模式。
您可能认为的改进是应用接口隔离原则(ISP)。基类是否需要整个MyContext对象?它可以仅使用MyContextAPI 的一个子集来实现其方法吗?
基类是否需要MyContext,或者只是为了派生类的利益而存在?
您甚至需要基类吗?
根据我的经验,您总是可以想出一个不使用继承的更好的设计。我不知道在这种情况下会发生什么,但是像下面这样怎么样?
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
}
Run Code Online (Sandbox Code Playgroud)
在重用 的单个实例时,无论是否有基类都没有区别MyContext。即使这三个类没有共同的超类型,您仍然可以这样做。