пятница, 4 мая 2012 г.

"Лишние" ссылки в проектах


Известно что «лишняя» ссылка из проекта А в проект В в результате компиляции не попадет в A.dll – об этом позаботится компилятор. Казалось бы ну и нет проблемы. Однако с точки зрения билд-процесса эта «лишняя» ссылка существует и поэтому если у вас изменились исходные файлы в проекте В, то будет перестроен и он и проект А, даже если последний не менялся. Так что для ускорения билдов неплохо было бы подчистить эти «лишние» ссылки. Для этого можно воспользоваться R#-ом и его «Optimize references».

Все сказанное помогает «нормальным» проектам. Нормальные проекты - это проекты, чье дерево зависимостей между проектами выглядит «широким», то есть имеет много «листовых», ни от кого независящих проектов. Если же у вас есть одна сборка Помойка.dll, от которой зависит большинство остальных сборок (таким образом ваше дерево проектов выглядит как Александрийский столп), то вы сами себе буратины – можете ходить и пить кофе на каждый билд.

P.S. Дерево проектов и его ширину-глубину можно посмотреть в билд-логе под расширенным логгированием.

воскресенье, 26 февраля 2012 г.

О валидации доменных объектов

Читаю эту старую, но не потерявшую популярность, книжку и не перестаю удивляться как давно люди пишут про "качество кода": ортогональность, high cohesion/low coupling и т.д. Все эти разговоры какие-то "неземные" - в реальной жизни приходится постоянно сталкиваться с трудностями в базовых вещах вокруг ООП...

Частая ситуация, с которой приходится сталкиваться, это невалидные данные при сохранении сущностей: это может быть сработавший constraint в базе, а может вы это обнаружите уже при вычитывании сущностей... Для примера рассмотрим такую "доменную" модель:

class Rectangle
{
    public int Height { get; set; }
    public int Width { get; set; }

    public int Area()
    {
        return Height * Width;
    }
}

class Square : Rectangle
{
}


В нашем примере проблема состоит в том, что в базе оказываются "квадраты" с разными сторонами. Проблему можно "решить" разными способами, например так:

class Rectangle
{
    public virtual int Height { get; set; }
    public virtual int Width { get; set; }

    public int Area()
    {
        return Height * Width;
    }
}

class Square : Rectangle
{
    private int _lengthOfSide;

    public override int Height
    {
        get
        {
            return _lengthOfSide;
        }
        set
        {
            _lengthOfSide = value;
        }
    }

    public override int Width
    {
        get
        {
            return _lengthOfSide;
        }
        set
        {
            _lengthOfSide = value;
        }
    }
}

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

class Rectangle
{
    public int Height { get; set; }
    public int Width { get; set; }

    public int Area()
    {
        return Height * Width;
    }

    public virtual void Validate()
    {
    }
}

class Square : Rectangle
{
    public override void Validate()
    {
        base.Validate();

        if (Height != Width)
        {
            throw new InvalidProgramException("Height and Width must be equal!");
        }
    }
}

Проблема вроде бы решена, остается выбрать бросать исключение или, например, получать от метода коллекцию ошибок валидации для более удобного отображения информации пользователю. Однако есть несколько моментов:

  1. Наличие метода Validate "подрывает" наше доверие к объектам, подталкивает к тому, чтобы вызывать его постоянно, "на всякий случай". В случае больших сущностей и большого набора правил валидации это может быть накладно. После этого мы конечно же придумаем механизм оптимизации...
  2. Результат валидации бесполезен! Как так? Ну а что толку в том, что перед сохранением объекта мы узнали что он невалиден? Сохранить об этом запись в логе и бросить исключение - вот все что мы можем. Мы не знаем когда объект стал невалидным, а если это "развесистый" объект с десятком свойств, который по пути к нам пересек несколько границ процессов, то шансов найти то самое место в коде, где на самом деле произошла ошибка, становится практически нереальным. Получив подобную ошибку с "живой" площадки мы с большой вероятностью либо закроем ее как unable to reproduce либо она вечно будет висеть в состоянии open.
Что если бы мы падали с исключением и записью в логе в тот самый момент, когда пытались привести объект в невалидное состояние? Хотя бы так:

class Rectangle
{
    public int Height { get; private set; }
    public int Width { get; private set; }

    public int Area()
    {
        return Height * Width;
    }

    public virtual void Resize(int height, int width)
    {
        Height = height;
        Width = width;
    }
}

class Square : Rectangle
{
    public override void Resize(int height, int width)
    {
        if (height != width)
        {
            throw new ArgumentException("Height and Width must be equal!");
        }

        base.Resize(height, width);
    }
}


Идея проста: не надо валидировать доменные объекты, надо недопускать их невалидного состояния! Причем здесь ООП? При том что мы не дали внешнему коду менять состояние нашего объекта, а спрятали это действие и связанные с ним правила внутрь метода объекта. Не это ли называется инкапсуляцией?

P.S. Предвижу вопроcы типа "наши пользователи редактируют объекты как хотят, а мы должны подсвечивать неправильные данные". Ответ прост - то что пользователи видят и редактируют это совсем не доменные объекты...

пятница, 9 сентября 2011 г.

О доменной модели...

Допустим в вашем приложении есть три модуля: модуль управления профилем пользователей, модуль формирования-оплаты заказов и модуль доставки заказов. Выглядела бы так в вашей доменной модели сущность "Покупатель"?

Customer
{
    FirstName
    LastName
    DateOfBirth
    ShippingAddress
    CreditCardNo
    BillingAddress
    WishList
}

вторник, 30 ноября 2010 г.

Эволюция кода

Как получает так, что в целом неплохой код со временем становится помойкой, клубком проблем, Авгиевыми конюшнями и ящиком Пандоры all-in-one?

The Broken Window Theory

A window gets broken at an apartment building, but no one fixes it. It's left broken. Then something else gets broken. Maybe it's an accident, maybe not, but it isn't fixed either. Graffiti starts to appear. More and more damage accumulates. Very quickly you get an exponential ramp. The whole building decays. Tenants move out. Crime moves in. And you've lost the game. It's all over.

Аналогично и с программированием. Вы написали код, хороший код, почти идеальный. От "почти" до идеала вас отделила какая то мелочь. То ли не вовремя закончился рабочий день, а с ним и вдохновение, то ли не кстати появилась критическая проблема, которую срочно нужно было решать. В конце концов мы не идеалисты, мы реалисты! И, черт побери, этот код работает, что еще нужно?!

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

Поздравляю! Вы очередной автор очередного г..нокода!

Что такое "чистый код"?
Как улучшить плохой код?
Почему чистый код часто "портится"?
Почему в написании кода так важны мелочи?

Читаем Роберт Мартин "Чистый Код"!

понедельник, 1 февраля 2010 г.

Заменяем механизм событий в CAB

Стандартный механизм событий в CompositeUI Application Block'е (CAB) не устраивает меня по нескольким причинам:
  1. Сложно и не удобно использовать. Нужно объявить событие, пометить его аттрибутом, потом сделать метод, который будет проверять на непустой список подписчиков и уже этот метод вызывать для генерации события.
  2. Отсутствие строгой типизации. Никто не мешает объявить подписку на событие с одной сигнатурой, а сгенерировать событие с другой.
  3. Неудобно отлаживать и разбираться в коде. Для того чтобы найти "подписчиков" и "публикаторов" события, приходится искать по строковому литералу.
  4. Неявная регистрация через рефлексию. А значит это не фатально, но медленнее чем могло бы быть.
Почитав Jeremy Miller's "Build Your Own CAB" и посмотрев на исходники StoryTeller, не мог удержаться от того, чтобы сделать что то подобное:

    public interface IListener<T> : IListener
    {
        void Handle(T message);
    }

    public interface IEventAggregator
    {
        void AddListener(IListener listener);
        void RemoveListener(IListener listener);
 
        void SendMessage<T>(T message);
        void SendMessage<T>() where T : new();
    }

После чего всё становится до неприличия простым:

    class MultiListener : IListener<Message1>, IListener<Message2>
    {
        public MultiListener(IEventAggregator eventAggregator)
        {
            // This is sample code!
            // Don't forget to unsubscribe in production code!
            eventAggregator.AddListener(this);
        }
 
        public void Handle(Message1 message)
        {
            Console.WriteLine("Message1");
        }
 
        public void Handle(Message2 message)
        {
            Console.WriteLine("Message2");
        }
    }

Исходный код можно взять здесь.

P.S. А мне начинает нравиться coding style, когда приватные методы начинаются с lower case.

P.P.S. До конца не уверен, что это окончательный вариант с "синхронизированными" "подписчиками". Что если потоков много и я хочу получать свои сообщения в том же потоке, в котором я подписывался? Надо обдумать...

среда, 20 января 2010 г.

Разделяй и тестируй!

Программирование это процесс написания кода. Причем большую часть этого времени мы изменяем существующий код. "Чужой" код. Чертыхаясь на того Васю Пупкина (оригинальное слово заменено, вдруг это прочтут дети...), который всё это написал, мы мучаемся, ставим "заплатки" и думаем как бы было здорово "всё переписать". Почему так? Почему так сложно разбираться в чужом коде, расширять его функциональность, не боясь ничего сломать? Уверены ли вы, что ваш код лучше, что вы не очередной "Вася Пупкин"? Говорите, что ваш код работает? Ну это еще не повод для самоуспокоения...

Возьмем для примера кусок кода, который отсылает клиенту счет за какие то товары с учетом доставки:

    public class OrderProcessingModule
    {
        public void SendInvoice(Guid orderId)
        {
            // get order from database
            Order order = GetOrder(orderId);
 
            // calculate total price
            double qtyPrice = 0;
            if (order.Qty < 10)
            {
                qtyPrice += order.Qty * order.UnitPrice;
            }
            else
            {
                // apply discount for large quantities
                qtyPrice += 0.9 * order.Qty * order.UnitPrice;
            }
 
            double shippingPrice = order.Shipping.IsInternational ? 10 : 5;
            if (order.Shipping.IsExpress)
                shippingPrice *= 2;
 
            var totalPrice = qtyPrice + shippingPrice;
 
            // prepare and send html-formatted message to customer
            var message = CreateMessageForOrder(order, totalPrice);
            var messagingGateway = new MessagingGateway();
            messagingGateway.Send(message);
        }
    }

Вроде бы неплохой код, структурирован, часть функциональности вынесена в отдельные методы:

        private Order GetOrder(Guid orderId)
        {
            string connectionString = ConfigurationManager.AppSettings["ConnectionString"];
            using (var connection = new SqlConnection(connectionString))
            {
                // 'SELECT * FROM Order' lives here
 
                var order = new Order();
                return order;
            }
        }
 
        private MailMessage CreateMessageForOrder(Order order, double totalPrice)
        {
            const string format = @"Define HTML template here";
            var sb = new StringBuilder();
            sb.AppendLine("" + order.UnitPrice + "");
            sb.AppendLine("" + order.Qty + "");
            sb.AppendLine("" + (order.Shipping.IsInternational ? "y/" : "n/") + (order.Shipping.IsExpress ? "y" : "n") + "");
            sb.AppendLine("" + totalPrice + "");
 
            var html = string.Format(format, order.OrderId, sb);
 
            var mailMessage = new MailMessage();
            mailMessage.Body = html;
 
            // ...
 
            return mailMessage;
        }

Но как это тестировать?! Как проверить что в зависимости от заказа правильно расчитывается его стоимость, что клиенту отправляется правильно отформатированное письмо и что оно отправляется правильному клиенту? Никак!!! Вариант с тем, чтобы написать тест, который будет ходить по SMTP или IMAP к почтовику, забирать письмо и проверять что оно существует и правильно сформированно я, по определенным причинам, не рассматриваю.

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

Single Responsibility PrincipleA class should have one, and only one, reason to change.

Звучит красиво, но что это на самом деле? На самом деле это означает, что код, который отвечает за расчет стоимости должен быть выделен в отдельный класс. И не только этот код...
Попробуем выделить в отдельные классы код, ответственный за чтение из базы, расчет стоимости и форматирование:

    class DataProvider
    {
        private readonly string _connectionString;
 
        public DataProvider(string connectionString)
        {
            _connectionString = connectionString;
        }
 
        public Order GetOrder(Guid orderId)
        {
            using (var connection = new SqlConnection(_connectionString))
            {
                // 'SELECT * FROM Order' lives here
 
                var order = new Order();
                return order;
            }
        }
    }

    class CostCalculator
    {
        public double CalculateOrderCost(Order order)
        {
            double qtyPrice = 0;
            if (order.Qty < 10)
            {
                qtyPrice += order.Qty * order.UnitPrice;
            }
            else
            {
                // apply discount for large quantities
                qtyPrice += 0.9 * order.Qty * order.UnitPrice;
            }
 
            double shippingPrice = order.Shipping.IsInternational ? 10 : 5;
            if (order.Shipping.IsExpress)
                shippingPrice *= 2;
 
            var totalPrice = qtyPrice + shippingPrice;
            return totalPrice;
        }
    }

    class MailFormatter
    {
        public MailMessage CreateMessageForOrder(Order order, double totalPrice)
        {
            const string format = @"Define HTML template here";
            var sb = new StringBuilder();
            sb.AppendLine("" + order.UnitPrice + "");
            sb.AppendLine("" + order.Qty + "");
            sb.AppendLine("" + (order.Shipping.IsInternational ? "y/" : "n/") + (order.Shipping.IsExpress ? "y" : "n") + "");
            sb.AppendLine("" + totalPrice + "");
 
            var html = string.Format(format, order.OrderId, sb);
 
            var mailMessage = new MailMessage();
            mailMessage.Body = html;
 
            // ...
 
            return mailMessage;
        }
    }

В результате наш OrderProcessingModule заметно упрощается:

    public class OrderProcessingModule
    {
        public void SendInvoice(Guid orderId)
        {
            // get order from database
            string connectionString = ConfigurationManager.AppSettings["ConnectionString"];
            Order order = new DataProvider(connectionString).GetOrder(orderId);
 
            // calculate total price
            var totalPrice = new CostCalculator().CalculateOrderCost(order);
 
            // prepare and send html-formatted message to customer
            var message = new MailFormatter().CreateMessageForOrder(order, totalPrice);
 
            var messagingGateway = new MessagingGateway();
            messagingGateway.Send(message);
        }
    }

В принципе мы добились чего хотели - мы можем написать простые юнит-тесты на каждый кусок функциональности, изменить только один класс с правилами расчета стоимости, адаптировать тесты, еще раз убедиться что всё работает правильно и идти спать спокойно :-)

Однако неплохо бы иметь возможность оттестировать и сам контроллер в лице OrderProcessingModule, но сейчас это невозможно, так как для этого нам потребуются конфигурационный файл с параметрами соединения, развернутая БД и сервис для отправки сообщений. Что ж, попробуем решить и эту проблему:

    public class OrderProcessingModule
    {
        private readonly IDataProvider _dataProvider;
        private readonly ICostCalculator _costCalculator;
        private readonly IMailFormatter _mailFormatter;
        private readonly IMessagingGateway _messagingGateway;
 
        public OrderProcessingModule(IDataProvider dataProvider, ICostCalculator costCalculator, IMailFormatter mailFormatter, IMessagingGateway messagingGateway)
        {
            if (dataProvider == null)
                throw new ArgumentNullException("dataProvider");
 
            // TODO: check all arguments
 
            _dataProvider = dataProvider;
            _costCalculator = costCalculator;
            _mailFormatter = mailFormatter;
            _messagingGateway = messagingGateway;
        }
 
        public void SendInvoice(Guid orderId)
        {
            // get order from database
            Order order = _dataProvider.GetOrder(orderId);
 
            // calculate total price
            var totalPrice = _costCalculator.CalculateOrderCost(order);
 
            // prepare and send html-formatted message to customer
            var message = _mailFormatter.CreateMessageForOrder(order, totalPrice);
            _messagingGateway.Send(message);
        }
    }

Теперь, вооружившись каким-нибудь Mock Framework'ом, можно приступать к написанию простых, понятных, быстрых юнит-тестов. Happy coding! :-)
Wider Two Column Modification courtesy of The Blogger Guide