ANTICHAT.XYZ    VIDEO.ANTICHAT.XYZ    НОВЫЕ СООБЩЕНИЯ    ФОРУМ  
Баннер 1   Баннер 2
Antichat снова доступен.
Форум Antichat (Античат) возвращается и снова открыт для пользователей. Здесь обсуждаются безопасность, программирование, технологии и многое другое. Сообщество снова собирается вместе.
Новый адрес: forum.antichat.xyz
Вернуться   Форум АНТИЧАТ > Программирование > С/С++, C#, Delphi, .NET, Asm
   
 
 
Опции темы Поиск в этой теме Опции просмотра

Как не нужно писать программы
  #1  
Старый 17.12.2009, 21:33
Аватар для Algol
Algol
Регистрация: 29.05.2002
Сообщений: 1,793
Провел на форуме:
2050916

Репутация: 0


По умолчанию Как не нужно писать программы

На написание этой мини-статьи меня натолкнул топик http://forum.antichat.ru/thread162874.html в котором автор реализует подсчет числа различных символов в тексте. При этом нарушаются все мыслимые и немыслимые правила построения архитектуры приложения, а также не выполняются никакие требования к качеству кода, его производительности, масштабируемости и т.п.

Рассмотрим основные недостатки решения, приведенного в http://forum.antichat.ru/thread162874.html :
1) Не разделены интерфейс пользователя и функциональная часть программы. Функционал содержится непосредственно в коде главной формы. Он никак не отделен ни синтаксически ни семантически, что приведет в последствии к тому, что код невозможно будет повторно использовать, будет затруднено его понимание и модификация, будет невозможно масштабирование приложения.
2) Интерфейсные элементы испульзуются как хранилище информации (так ListView >хранит< найденные символы, хотя его основная задача - отображать найденные элементы). В результате - невозможность масштабирования, даже минимального.

Например, если автор вдруг захочет паралельно обрабатывать два текста - он просто физически не сможет этго сделать, поскольку ListView - только один.
3) Алгоритм, как и его реализация, крайне неэффективен. Во-первых используется попросту полный перебор (автор ищет символ в массиве уже найденых символов, что приводит к числу операций O(n^2)). Во-вторых поиск ведется в структурах, не предназначенных для поиска(и вообще хранения информации) - в ListView. Я уж не говорю про такие "мелочи", как нетипизированный ArrayList, поскольку потери на боксинг/анбоксинг выглядят невинно на фоне остального "алгоритма".
4) Приложение имеет недружественный интерфейс. Во время выполнения операции рассчета статистики оно попросту зависает. В паре с неэффективным и долгим алгоритмом это вызовет "недоумение" пользователя.

Теперь рассмотрим реализацию приложения, в которм я попытался устранить все вышеописанные недостатки. Оно не претендует на абсолютную правильность, но по крайней мере оно соответствует тому минимальному уровню, который должен присутствовать в любом, даже в самом простом приложении.

Начнем с общей архитектуры. Во-первых я разделил интерфейс пользователя и собственно "вычислитель" статистики.

Теперь вычисления живут в отдельном классе и в отдельном файле StatCalculator.cs. Можно было бы и вынести этот класс в отдельный проект, но в данном, простейшем случае это уже перебор . Разделив интерфейс пользователя и класс-вычислитель я убиваю кучу зайцев и Мазая заодно: Во-первых теперь, у меня есть два класса, у каждого из которых есть свой простой и понятный функционал (в литературе это называется принцип единственной ответственности) . Один класс (MainForm) занимается диалогом с пользователем, другой (StatCalculator) - занимается подсчетом статистик в тексте. При этом калькулятор ничего не знает о MainForm, что снижает связность архитектуры, и позволяет использовать калькулятор в других местах программы, либо вообще в других приложениях. Во-вторых, вынеся функционал в отдельный класс я создаю возможность маштабирования и расширения приложения. Так, например, я могу вычислять статистику сразу нескольких текстов - просто создав необходимое число калькуляторов и запустив их в разных потоках. Либо, я могу вообще вынести класс StatCalculator в сервис, и запускать на удаленном сервере - просто передавая ему исходный текст и принимая рассчетную статистику в ответ. В общем, одно только разделение функционала уже решает часть проблем, описанных выше.

Далее, я выделил основные функциональные части программы: ввод текста - вычисление статистик - вывод результата.
Ввод текста автоматически реализуются компонентами формы (TextBox и т.п.), вычисление статистик берет на себя StatCalculator, а выводом результатов занимается метод UpdateInterface() (который при усложнении приложения, можно вынести и в отдельный класс, но здесь я не стал заморачиваться).
Явное разделение функционала по частям дает возможность создания более сложных связей между частями программы.

Например, если мы в дальнейшем захотим реализовать сохранение/чтение результатов вычислений, то мы можем воспользоваться уже готовым методом для отображения загружаемой статистики UpdateInterface().

Теперь об алгоритме. Начинающие программисты часто не умеют пользоваться(или даже не знают о них) такими абстрактными типами данных как списки, хеши(HashSet), словари(Dictionary), остортированные списки (SortedList, SortedDictionary), деревья. Часто пользуются исключительно массивами. Не буду останавливаться на целесообразности применения того или иного типа данных - это тема для отдельной статьи. Скажу лишь, что в задаче которую мы решаем, нужно применять Dictionary. При чем хотелось бы особо отметить слово "нужно" именно нужно, а не "можно". В каждой задаче, как правило, есть единственная верная структура даных, которая нужна для правильного и быстрого решения. Программист должен спиным мозгом, на автомате, чувствовать какой именно тип даных нужно использовать в данном конкретном случае. Применение неудачного типа данных влечет потенциальные ошибки(например неуникальность ключей) и гарантированно снижает производительность (например, выполняется полный перебор при поиске).

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

Ниже приведен метод, рассчитыающий необходимые нам статистики. Он так прост, что коментарии излишни:

Код:
        public void CalcCharStat()
        {
            statistics = new Dictionary<char, CharStat>();
            //считаем частоты
            foreach (char c in text)
            {
                if (!statistics.ContainsKey(c))
                    statistics[c] = new CharStat();
                statistics[c].frequency++;
            }
            //считаем сумму всех частот 
            totalChars = 0;
            foreach (CharStat stat in statistics.Values)
                totalChars += stat.frequency;
            //считаем процент
            if (totalChars > 0)
                foreach (CharStat stat in statistics.Values)
                    stat.percent = (float)stat.frequency / totalChars;
        }
Обратим лишь внимание на то, что в процессе рассчетов не используются нетипизированные коллекции, сведено к минимуму преобразование типов.
Алгоритм можно было бы еще ускорить на пару процентов, но из-за потери наглядности я не стал этого делать - как правило оно того не стоит.

Быстродействие приведенного алгоритма: O(n) (что является теоретическим пределом для данной задачи).

Теперь остановимся еще на двух моментах - запуск рассчетов в отдельном потоке и отображении результатов.

Запуск рассчета сделан в отдельном потоке. Он создается в главном потоке приложения, запускается и далее приложение ждет пока он завершится, обрабатыая попутно события главной формы, так что приложение в целом - не виснет. При этом достигаются несколько целей:
1) Приложение реагирует на действия пользователя и не "висит"
2) Есть возможность сделать прогресс-бар, для отображения хода рассчета статистик
3) Есть возможность сделать кнопку Stop для останова рассчетов, не дожидаясь конца алгоритма.

Отображение результатов тоже очень простое:

Код:
private void UpdateInterface()
        {
            //очищаем listView
            lvStat.Items.Clear();
            //заносим результаты рассчетов в listView
            foreach (KeyValuePair<char, CharStat> pair in calculator.statistics)
            {
                //приводим char к презентабельному виду
                string presentableChar;
                if (pair.Key <= ' ')
                    presentableChar = "#" + Convert.ToByte(pair.Key);
                else
                    presentableChar = pair.Key.ToString();
                //создаем элемент listView
                ListViewItem item = new ListViewItem(
                    new string[]{
                    presentableChar,
                    pair.Value.frequency.ToString(),
                    string.Format("{0:0.00}", 100*pair.Value.percent)});//здесь округляем процент до двух знаков после запятой
                lvStat.Items.Add(item);
            }
            //обновляем статус бар
            lbTotal.Text = string.Format("Total chars: {0}", calculator.totalChars);
            lbTotalUnique.Text = string.Format("Total unique chars: {0}", calculator.statistics.Count);
        }
Тут можно обратить внимание на двух моментах - во-первых здесь мы преобразуем символы в читабельный вид (в dictionary они хранятся в естественном, "сыром" виде) - ведь не все символы можно отобразить на экране (например, возврат каретки, пробел или табуляцию). Во-вторых здесь мы производим округление рассчитанных процентов. Округление в пользовательском интерфейсе дает два важных преимущества:
1) Сохраняется целостность данных, поскольку сумма процентов внутри dictionary - ровно 100%. После округления это было бы уже не так.
2) Сохраняется возможность указания необходимой точности процентов пользователем. Причем точность процентов можно менять уже после рассчетов.

Вот в общем-то и все что хотелось сказать
Полная реализация - здесь http://rapidshare.com/files/322173460/WindowsFormsApplication5.zip.html .
 
Ответить с цитированием
 



Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
FAQ по выделенным серверам †Romi4† Авторские статьи 4 31.08.2009 16:19
Сказ про настоящего Хакера tclover Болталка 3 14.05.2009 23:44
WebScarab - профессиональный инструмент для анализа защищённости веб-приложений Kuzya Авторские статьи 5 09.04.2009 21:54
КАК ПИСАТЬ ПИСЬМА> В ПОМОЩЬ СОЦИАЛЬНОМУ ИНЖЕНЕРУ infothief Авторские статьи 14 21.05.2007 03:42



Здесь присутствуют: 1 (пользователей: 0 , гостей: 1)
 


Быстрый переход




ANTICHAT.XYZ