Pull to refresh

Кодоребус или паттерн «стратегия» на .Net 4.0

Reading time 3 min
Views 2.5K
Недавно при работе над одним проектом у нас родился интересный код. Мы сразу же принялись тестировать наших коллег на смекалку, с просьбой объяснить что это, как работает и что делает. Даже опытных разработчиков этот код вгоняет в ступор (после пары минут истерического смеха). Итак, встречаем:

    Action<Action> action = (Action action) => { action(); };

Прежде чем заглянуть под хабракат, попробуйте ответить на несколько вопросов (сделаем вид, что заголовок поста Вы не видели):
  • На каком языке написан этот кусок кода?
  • Верен ли он синтаксически? Скомпилируется ли он?
  • Имеет ли данный код смысл? Что он делает?
  • Зачем такой код мог быть написан?
  • Как можно улучшить этот код? (Как бы его написали Вы?)
  • Приведите реальные варианты использования этого кода.
  • Какие потенциальные проблемы могут возникнуть при его применении?

Ответили? Тогда ныряем под кат за предысторией и разъяснениями.


Сразу скажу что это вполне работающий код, написанный на С# под .Net Framework 4.0. Конечно же мы умышленно превратили его в веселый ребус, в продакшн коде он выглядит так:

    private Action<Action> doWork = work => { work(); };

Говоря по-русски, мы создаем делегат, который в качестве параметра принимает другой делегат. Теперь расскажу как он родился, максимально упростив существующий код, для простоты понимания. Был у нас такой код (все названия вымышлены, а совпадения случайны):

    public class MyWorker {
        private void DoWorkInternal() {
            Thread.Sleep(1000);//simulate work
            IsWorkDone = true;
        }
        public void DoWork() {
            DoWorkInternal();
        }
        public bool IsWorkDone { get; private set; }
    }

И такой тест, который проверяет результат работы:

        [TestMethod]
        public void TestDoWork() {
            MyWorker worker = new MyWorker();
            Assert.IsFalse(worker.IsWorkDone);
            worker.DoWork();
            Assert.IsTrue(worker.IsWorkDone);
        }

Со временем, в боевых условиях выяснилось, что метод DoWorkInternal выполняется достаточно долго, а результат его выполнения не влияет на дальнейшую работу приложения (здесь можно поразмышлять, чем же он мог заниматься). Естественно захотелось сделать его выполнение асинхронным:

        public void DoWork() {
            Task.Factory.StartNew(DoWorkInternal);
        }

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

    public class MyWorker {
        private Action<Action> doWork = work => { work(); };
        private void DoWorkInternal() {
            Thread.Sleep(1000);//simulate work
            IsWorkDone = true;
        }
        public void DoWork() {
            doWork(DoWorkInternal);
        }
        public bool IsWorkDone { get; private set; }
    }

Т.е. стратегия для синхронного выполнения операции выглядит так:

    (Action work) => { work(); }

Стратегия для асинхронного выполнения:

    (Action work) => { Task.Factory.StartNew(work); }

Добавим в наш класс метод, который позволит сменить дефолтную (синхронную) стратегию на другую:

        public void SetDoWorkStrategy(Action<Action> doWork) {
            this.doWork = doWork;
        }

Готово. Тест в качестве доказательства:

        [TestMethod]
        public void TestDoWork() {
            MyWorker worker = new MyWorker();
            Assert.IsFalse(worker.IsWorkDone);
            worker.DoWork();
            Assert.IsTrue(worker.IsWorkDone);

            MyWorker worker2 = new MyWorker();
            Action<Action> asyncWorkStrategy = (Action work) => { Task.Factory.StartNew(work); };
            worker2.SetDoWorkStrategy(asyncWorkStrategy);
            Assert.IsFalse(worker2.IsWorkDone);
            worker2.DoWork();
            Assert.IsFalse(worker2.IsWorkDone);//work is in progress
        }

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

P.S. Обсуждение приведенного кодоребуса может быть хорошей темой для беседы с кандидатом на должность .Net разработчика
Tags:
Hubs:
+18
Comments 48
Comments Comments 48

Articles