在 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,因为它们都需要 BaseBase 是抽象的吗?我需要在某处“bootstrap”系统...我想所有 classes 可能有两个构造函数,一个带有 Base,一个带有 MyContext ...但是每个 class 都有两个构造函数可能是未来维护者的噩梦...)。

有人说“你为什么不把 MyContext 变成一个单例,就像在 Project X 中那样?”。 项目 X 具有类似的 class 结构,但是,“感谢”单例,classes 没有 MyContext 参数:它们“正常工作”。这是魔法! 我不想使用单例(或一般的静态数据),因为:

有人说我们不应该太拘泥于“学术理论”,“好的”和“坏的”编程实践的无用想法,我们不应该以抽象的名义过度设计 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 没有共同的超类型,您仍然可以这样做。