Удаление синглетонов из большой кодовой базы .NET - PullRequest
1 голос
/ 01 ноября 2011

Контекст:

(Примечание: в дальнейшем я использую «проект» для обозначения набора результатов поставки программного обеспечения, предназначенных для одного клиента или конкретного рынка. IЯ не имею в виду «проект», поскольку он используется в Visual Studio для обозначения конфигурации, которая создает один EXE или DLL в рамках решения.)

У нас есть значительная система, состоящая из трех уровней:

  1. Слой, содержащий код, который используется в разных проектах
  2. Слой, содержащий код, который используется в разных приложениях проекта
  3. Слой, содержащий код, относящийся к конкретномуконкретное приложение или веб-сайт в проекте.

Первые два слоя встроены в сборки DLL.Верхний слой представляет собой набор EXE-файлов и / или .aspx веб-приложений.

IIRC, у нас есть ряд различных проектов, использующих этот шаблон.Все четыре используют общий уровень 1 (хотя часто в слегка отличающихся версиях, управляемых VCS).Каждый из них имеет свой собственный уровень 2. Каждый из них имеет свой собственный набор результатов, которые могут варьироваться от веб-сайта или веб-сайта и справочной службы до наших самых больших и самых сложных (а также из нашего хлеба и масла).бизнес), которая состоит из пяти независимых веб-приложений, более 20 консольных приложений / фоновых служб, трех или четырех независимых веб-служб, полдюжины приложений с графическим интерфейсом для настольных компьютеров и т. д.

Мы стремились подтолкнутькак можно больше кода на уровни 1 и 2, чтобы избежать дублирования логики на верхних уровнях.Мы в значительной степени достигли этого.

Каждый из слоев 1 и 2 создает три результата: DLL, содержащая код, не связанный с сетью, DLL, содержащая код, связанный с сетью, и DLLсодержащий модульные тесты.

Проблема:

Нижние уровни были написаны для широкого использования синглетонов.

Не веб-DLL в слое1 содержит классы для обработки INI-файлов, ведения журналов, пользовательский объектно-реляционный картограф, который обрабатывает соединения с базой данных и т. Д. Все эти используемые синглтоны.

И когда мы начали создавать вещи в Интернете, все эти одиночные игры стали проблемой.Различные пользователи будут заходить на сайт, входить в систему и начинать делать разные вещи.Они делали что-то, что генерировало запрос, что приводило бы к вызову в одноэлементную ORM для получения нового соединения с базой данных, которая получала бы доступ к одноэлементному объекту конфигурации для получения строки соединения, а затем соединение запрашивалось выполнитьзапрос.И в запросе соединение получит доступ к одноэлементному регистратору для записи сгенерированного оператора SQL, и регистратор получит доступ к одноэлементному объекту конфигурации, чтобы получить текущее имя пользователя, чтобы включить его в журнал, и если кто-то еще зарегистрировалсяв то же время этот объект конфигурации синглтона будет иметь другого текущего пользователя.Это был беспорядок.

Итак, что мы сделали, когда начали писать веб-приложения с использованием этой базы кода, - создать класс фабрики синглтонов, который сам по себе был синглтоном.У каждого из других синглетонов был открытый статический метод instance (), который вызывал приватный конструктор.Вместо этого открытый статический метод instance () получил ссылку на объект-фабрику-одиночку, а затем вызвал метод для получения ссылки на единственный экземпляр рассматриваемого класса.

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

Теперь у нас был только один синглтон. И в его публичный статический метод instance () мы добавили некоторую логику, специфичную для веб-сайтов. Если бы у нас был HTTPContext и этот контекст имел экземпляр фабрики в своем сеансе, мы бы вернули экземпляр из сеанса. Если бы у нас был HTTPContext, а в его сеансе не было фабрики, мы бы создали новую фабрику и сохранили ее в сеансе, а затем вернули бы ее. Если бы у нас не было HTTPContext, мы бы просто создали новую фабрику и вернули ее.

Код для этого был помещен в классы, которые мы получили от Page, WebControl и MasterPage, а затем мы использовали наши классы в нашем коде более высокого уровня.

Это нормально работало для веб-приложений .aspx, где пользователи входили в систему и поддерживали сеанс. Он хорошо работал для веб-служб .asmx, работающих в этих веб-приложениях. Но у этого есть реальные ограничения.

В частности, он не будет работать в ситуациях, когда нет сессии. Мы чувствуем давление с целью предоставления веб-сайтов, которые обслуживают большую пользовательскую базу - это может иметь десятки или сотни тысяч пользователей, которые динамически обращаются к ним. До сих пор наши пользователи были довольно типичными настольными бизнес-пользователями. Они заходят на наши веб-сайты и остаются в них большую часть дня, используя наши веб-приложения в качестве альтернативы настольному приложению. У данного клиента может быть до шести пользователей, которые могут использовать наши веб-сайты, и, хотя у нас есть тысяча или более клиентов, в совокупности они не создают такой большой нагрузки. Но наша нынешняя архитектура не будет масштабироваться до этого.

Мы также сталкиваемся с ситуациями, когда ASP.NET MVC лучше подходит для создания веб-интерфейса, чем веб-формы .aspx. И мы исследуем создание мобильных приложений, которые будут взаимодействовать с автономными веб-сервисами WFC. И хотя в обоих случаях создается впечатление, что их можно запускать в среде, в которой есть сеанс, они довольно жестко ограничивают их гибкость и производительность.

Итак, мы действительно ищем способы устранения этих синглетонов.

Что бы я действительно хотел:

Я пытаюсь представить серию рефакторингов, которые в конечном итоге приведут к более структурированной, более гибкой архитектуре. Я легко мог видеть преимущества инфраструктуры IoC в нашей ситуации.

Но вот в чем дело - из того, что я видел в инфраструктурах IoC, им нужны их зависимости, предоставляемые им извне через параметры конструктора. Моему классу регистратора, например, нужен экземпляр моего класса конфигурации, из которого можно получить текущего пользователя. В настоящее время он использует открытый статический метод instance () класса config для его получения. Чтобы использовать IoC-фреймворк, мне нужно передать его как конструктор.

Другими словами, с того места, где я сижу, первая и неизбежная задача состоит в том, чтобы изменить каждый класс, использующий любой из этих синглетонов, чтобы принимать фабрику синглетонов в качестве параметра конструктора. И это огромный объем работы.

В качестве примера, я просто провел день, занимаясь именно этим, в библиотеках уровня 1, чтобы увидеть, насколько это много работы. Я закончил тем, что изменил более 1300 строк кода. Библиотеки уровня 2 будут хуже.

Итак, есть ли альтернативы?

Ответы [ 2 ]

1 голос
/ 01 ноября 2011

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

Вы должны попытаться разработать нечто подобное, чтобы вместо возврата одноэлементного экземпляра вы возвращали экземпляр из текущего контекста. Таким образом, вам не нужно менять свой потребительский код, который ссылается на эти статические методы (например, Logger.Instance()).

Я обычно свертываю информацию, такую ​​как регистратор, текущий пользователь, конфигурация, разрешения безопасности в контексте приложения (при необходимости может быть несколько классов). Статический метод AppContext.Current возвращает текущий контекст. Реализация метода выглядит примерно так:

public interface IContextStorage
{
        // Gets the stored context
        AppContext Get();

        // Stores the context, context can be null
        void Set(AppContext context);
}

public class AppContext
{
    private static IContextStorage _storageProvider, _defaultStorageProvider;

    public static AppContext Current
    {
    get
    {
       var value = _storageProvider.Get();
       // If context is not available in storage then lookup
       // using default provider for worker (threadpool) therads.
       if (null == value && _storageProvider != _defaultStorageProvider
        && Thread.CurrentThread.IsThreadPoolThread)
       {
        value = _defaultStorageProvider.Get();
       }
       return value;
    }
    }

  ...
}

IContextStorage Реализации зависят от приложения. Статические переменные _storageProvider вводятся во время запуска приложения, а _defaultStorageProvider - простая реализация, которая просматривает текущий контекст вызова.

Создание контекста приложения происходит в несколько этапов - например, глобальная информация, такая как конфигурация, читается и кэшируется при запуске приложения, в то время как конкретная информация, такая как пользователь и безопасность, формируется на этапе аутентификации. Как только вся информация становится доступной, фактический экземпляр создается и сохраняется в определенном месте хранения приложения. Например, настольное приложение будет использовать одноэлементный экземпляр, а веб-приложение, вероятно, может сохранить экземпляр в состоянии сеанса. Для веб-приложения у вас может быть логика в начале каждого запроса, чтобы обеспечить инициализацию контекста.

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

1 голос
/ 01 ноября 2011

Я бы порекомендовал начать с реализации паттерна "Бедняжка". Здесь вы определяете два конструктора в ваших классах, один из которых принимает экземпляр зависимостей (IoC), а другой - конструктор по умолчанию, который их создает (или вызывает одиночный код).

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

public class Foo {
    public Foo(ILogger log, IConfig config) {
        _logger = log;
        _config = config;
    }

    public Foo() : this(Logger.Instance(), Config.Instance()) {}
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...