Передача контекста в библиотеке классов C #, поиск «простого» способа без использования статического

#c# #design-patterns #.net-standard #class-design #class-hierarchy

#c# #шаблоны проектирования #.net-стандартный #дизайн класса #иерархия классов

Вопрос:

Для библиотеки (.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
  

Классы подверглись критике из-за этого параметра «MyContext», который требуется каждому конструктору.
Некоторые люди говорят, что это загромождает код.

Я сказал, что параметр необходим для распространения контекста, так что, например, когда вы вызываете метод doSomething C3, содержащиеся экземпляры 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 не требует a Base , он требует a MyContext . Не говоря уже о том конструкторе, который кажется конструктором копирования, но это не так! О, и теперь, как я могу инициализировать C1, C2 или C3 вне Base производного от a класса, поскольку все они хотят a Base и Base являются абстрактными? Мне нужно где-то «загрузить» систему… Я предполагаю, что все классы могут иметь два конструктора, один с Base и один с MyContext … но наличие двух конструкторов для каждого класса может стать кошмаром для будущих сопровождающих …).

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
  

Вызовы Make ВЫГЛЯДЯТ лучше, но они еще более подвержены ошибкам, потому что подпись Make не является строго типизированной. Моя цель — упростить выполнение задач для пользователей. Если бы я открыл скобку, и Intellisense предложил мне… массив System.Object , я был бы ОЧЕНЬ разочарован. Я мог бы передать неверный параметр, и я узнал бы об этом только во время выполнения (начиная со старой доброй .NET Framework 2.0, я надеялся забыть о массивах System.Object …)

И что? Фабрика с выделенным строго типизированным методом для каждого базового производного класса была бы типобезопасной, но, возможно, это также было бы «некрасиво» видеть (и нарушение принципа открытия-закрытия учебника. Да, «принципы», опять же):

 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
  

РЕДАКТИРОВАТЬ (после комментариев): Я забыл упомянуть, что да, кто-то предложил мне использовать фреймворк DI / IoC, например, в «Project Y». Но, по-видимому, «Project Y» использует его только для того, чтобы решить, нужно ли создавать экземпляр MyConcreteSomething или MyMockSomething когда ISomething это необходимо (пожалуйста, рассмотрите насмешливые и модульные тесты вне рамок этого вопроса по причинам), и он проходит вокруг объектов DI / IOC framework точно так же, как я прохожу вокруг своего контекста…

Комментарии:

1. На мой взгляд, нет ничего плохого в вашем самом первом фрагменте кода (с контекстом в конструкторах). Это делает код намного понятнее и удобен в обслуживании. Это называется инверсией управления. Ваш проект может использовать среду внедрения зависимостей для автоматизации процесса внедрения экземпляра в каждый конструктор. Но очень хорошо, что ваши классы явно указывают, что им нужно в своих конструкторах.

2. Я бы остался с первым подходом к фрагментам. Вы не можете сделать всех счастливыми, и вас это не должно сильно волновать. Они предоставили некоторые «аргументы», вы сочли их неконструктивными, так что просто двигайтесь дальше.

3. Первый фрагмент — это стандартная инъекция конструктора. Я тоже не вижу проблемы. Другим подходом было бы использование DbContextScope . У него есть свои плюсы и минусы, как и у вашего шаблона. У вас есть большой контроль над тем, как и когда вы создаете контекст. У вас все равно будут параметры в конструкторах репозиториев, т.Е. public MyRepository(IAmbientDbContextLocator contextLocator) Вам не нужно создавать его в каждом методе обслуживания, но вы можете, если это необходимо.

4. Мой плохой, приведенный выше не является стандартом .net, хотя есть вилка

5. спасибо всем вам, люди! Да, я забыл упомянуть, кто-то предложил мне использовать фреймворк DI / IOC, например, в «Project Y», но, по-видимому, «Project Y» использует его только для того, чтобы решить, создавать ли экземпляр MyConcreteSomething или MyMockSomething, когда требуется ISomething (пожалуйста, рассмотрите насмешливые и модульные тесты вне рамок этого вопроса, потому что причины), и он обходит объекты DI / IOC framework точно так же, как я обхожу свой контекст…

Ответ №1:

Из альтернатив, перечисленных в OP, оригинальная является лучшей. Как гласит Дзен Python, явное лучше, чем неявное.

Использование синглтона сделало бы явную зависимость неявной. Это не улучшает удобство использования; это делает вещи более неясными. Это усложняет использование API, потому что пользователь API должен будет изучить больше вещей, прежде чем он или она сможет успешно его использовать (в отличие от простого просмотра подписи конструктора и предоставления аргументов, которые заставляют код компилироваться).

На самом деле не имеет значения, требуется ли для использования API еще несколько нажатий клавиш. В программировании ввод текста не является узким местом.

Передача MyContext через конструктор — это просто старая инъекция конструктора, даже если это конкретная зависимость. Однако не позволяйте жаргону Dependency Injection (DI) обмануть вас. Вам не нужен контейнер DI для использования этих шаблонов.

Что вы могли бы рассмотреть в качестве улучшения, так это применение принципа разделения интерфейса (ISP). Нужен ли базовому классу целый MyContext объект? Может ли он реализовать свои методы только с подмножеством MyContext API?

Нужен ли 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
}
  

Есть ли у вас базовый класс или нет, не имеет значения, когда дело доходит до повторного использования одного экземпляра MyContext . Вы все равно можете это сделать, даже если у этих трех классов нет общего супертипа.

Комментарии:

1. Спасибо за ваш ответ (я подожду несколько дней, а затем отмечу его зеленым, если других ответов нет). Очень интересные идеи, которые стоит изучить и поэкспериментировать. Но у меня есть подозрение, что это более вероятно для определенных людей (er… включая меня ???) принять отказ от глобальных переменных… чем признать, что библиотека классов может существовать без наследования : D Обвинять некоторых университетских преподавателей 😉