在 C# 类库中传递上下文,寻找一种不使用静态的“简单”方法

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”仅使用它来决定是否实例化MyConcreteSomethingMyMockSomething何时ISomething需要(请考虑模拟和单元测试超出此问题的范围,因为原因),并且它传递 DI/IOC 框架对象就像我传递的一样我的上下文...

Mar*_*ann 3

在 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。即使这三个类没有共同的超类型,您仍然可以这样做。