C#

Определение проверки алгоритма

Тип Node — это самосодержащий тип, следовательно, алгоритм не нуждается в соз¬дании экземпляра древовидной структуры. Это пример хорошего проекта, поскольку, при необходимости добавить больше городов, изменения нужно внести только в тип S'ode. Алгоритм поиска, использующий тип Node, изменяться не должен. На заметку. Когда вам удается создать код, локализующий влияние изменений, чтобы они не распространялись на другие части кода, это называется разъединением (decoupling) кода. Желательно создавать код так, чтобы он был отделен от другого кода, чтобы после изменений в одном фрагменте кода, остальные части продолжали работать. По мере приобретения опыта в разработке кода разъединение кода станет вашей ежедневной практикой. Для иллюстрации давайте нанесем удар по алгоритму поиска и посмотрим, что бу-zer. Мы можем начать с определения класса поиска или проверки для класса поиска. Гначала определим предварительную проверку, поскольку это позволит выяснить, ка¬кую форму должен принять класс поиска. public static void TestS.earch () SearchSolution.SearchAlgorithm.DepthFirstFindRoute("Montreal", "Seattle"); При проверке происходит непосредственный вызов алгоритма поиска learchAlgorithm. DepthFirstFindRoute (). Здесь SearchAlgorithm — это имя класса, а leochFirstFindRoute () — имя метода. Имя означает, что данный класс будет содер¬жать все реализации алгоритма поиска. Это неправильно, поскольку весь алгоритм по¬иска не будет содержаться внутри одного метода. Для этого, вероятнее всего, потребу¬йся несколько методов. И если каждый алгоритм поиска потребует несколько методов, те поддержка класса SearchAlgorithm превратится в настоящий кошмар. Лучшее решение заключалось бы в идентификации одного класса для единой реа¬лизации алгоритма поиска. Затем, для каждого класса, мы можем определить общий ссентификатор метода, используемый для поиска маршрута между двумя пунктами. 7-т:го приведет к следующей модификации проверки: public static void TestSearch() SearchSolution.DepthFirstSearch.FindRoute("Montreal", "Seattle"); Теперь проверка подразумевает, что класс DepthFirstSearch имеет статический метод FindRoute (). Это было бы вполне приемлемо, и, если бы вы должны были рев¬изовать класс BreadthFirstSearch, то именование было бы таким SearchSolution. BreadthFirstSearch. FindRoute. Но есть еще одна проблема, относящаяся к способно¬сти нескольких пользователей использовать алгоритм во время выполнения програм¬мы. Метод FindRoute () статический, а следовательно, это общий ресурс. (Вспомните аналогию с конференц-связью.) Если несколько пользователей применят этот алгоритм, то они используют ресурс совместно. Но это бывает проблематично, если вы храните временные данные в переменных-членах класса DepthFirstSearch. Использование ста¬тического метода могло бы повредить уже найденный путь. Более подходящее решение заключается в том, чтобы определить метод FindRoute () как нестатический метод. Это подразумевает, что экземпляр DepthFirsf Search следует создать прежде, чем вызвать метод FindRoute () . Мы снова должны изменить проверку следующим образом: public static void TestSearchO { SearchSolution.DepthFirstSearch els = new SearchSolution.DepthFirstSearch (); els.FindRoute("Montreal", "Seattle"); } Для работы метода FindRoute () необходимо создание экземпляра класса DepthFirstSearch, что позволяет нескольким пользователям осуществлять поиск, не мешая друг другу. Теперь мы можем поздравить себя и считать, что имеем хорошую проверку, которая требует реализации класса. Проблема магических данных Наша проверка еще не закончена, ведь мы не имеем доступа к маршруту, найденно¬му алгоритмом, но это будет вскоре объяснено. В реализации класса DepthFirstSearch необходима ссылка на структуру данных. Алгоритм поиска должен знать, по какому дереву он перемещается. Один из способов реализации ссылки на дерево — это непосредственная ссылка на статические данные Node .RootNodes. Реализация метода DepthFirstSearch () имела бы следующий вид: public class DepthFirstSearch { public DepthFirstSearch ( ) { } public void FindRoute(string start, string end) { Node[] startNodes = Node.RootNodes; } } В этом примере объявлена переменная по имени startNodes, представляющая от¬правную точку или корень дерева, как показано на рис. 4.2. Корень дерева создан на базе переменной-члена Node .RootNodes, и ее присвоение называется присвоением вол¬шебного типа (magic type assignment). Волшебный тип получается при вызове метода, а волшебство заключается в знании способа обращения к данным, даже при том, что вы не указываете тип. В случае метода DepthFirstSearch () волшебство — в его способ¬ности узнать, как правильно обратиться к переменной-члену RootNodes. Данное предположение плохо, поскольку оно привязывает переменную-член RootNodes к методу FindRoute () . А если разработчик класса Node впоследствии решит добавить функции загрузки дерева из файла на жестком диске? В этом случае, чтобы не нарушить метод FindRoute (), разработчик вынужден явно скопировать дерево, за¬груженное с жесткого диска, в переменную-член RootNodes. А что, если два разных пользователя захотят создать два разных дерева полета? Ресурс Nodes .RootNodes общий, а следовательно, может содержать только одно дерево полета. Разработчик класса Node мог бы изменить переменную-член RootNodes, и метод FindRoute () повел бы себя непредсказуемо. В случае магических данных волшебными должны быть любые данные, передавае¬мые типу. Проверка для маршрута полета изменилась бы так. public static void TestSearchO { SearchSolution.DepthFirstSearch els = new SearchSolution.DepthFirs tSearch(SearchSolution.Node.RootNodes); els.FindRoute("Montreal", "Seattle"); } Поскольку корневой узел дерева необходим, мы изменим конструктор так, чтобы вызывающая сторона передавала узел корня дерева. Проверочный код все еще ис¬пользует статическую переменную-член RootNodes. но метод DepthFirstSearch() не должен знать, где искать дерево. Если разработчик класса Node изменит поведе¬ние переменной-члена RootNodes, то изменять понадобится только код конструктора :epthFirstSearch () , а не метод DepthFirstSearch (). Таким образом, это правильно, что классы Node и DepthFirstSearch отделены друг от друга. Получение найденного маршрута После вызова метода FindRoute () вы ожидаете ответ. Поскольку маршрут может : одержать несколько городов, найденный маршрут сохраняется в массиве элементов :::ое. С точки зрения программирования, существует два способа получения массива Sodes. Первый подразумевает использование значения параметра возврата, как здесь: public static void TestSearchO { SearchSolution.DepthFirstSearch els = new SearchSolution. DepthFirstSearch(SearchSolution.Node.RootNodes); Node[] foundRoute = els . FindRoute("Montreal", "Seattle"); Код, выделенный полужирным шрифтом, демонстрирует присвоение возвращае¬мого значения переменной foundRoute. Второй подход подразумевает использование леременной-члена следующим образом: public static void TestSearchO SearchSolution.DepthFirstSearch els = new SearchSolution.DepthFirstSearch(SearchSolution.Node.RootNodes); els.FindRoute("Montreal", "Seattle"); Node[] foundRoute = els.FoundRoute; При втором подходе маршрут сохраняется в переменной-члене FoundRoute. Каждый подход кажется прекрасным и трудно определить, какой из них использовать. Самый надежный способ принятия решения — это написание проверки, позволяющей выяс¬нить, нет ли каких-то проблем у каждого из подходов. При вычислении одного маршрута любой подход прекрасен. Но давайте рассмотрим код. допускающий поиск нескольких маршрутов. Сначала рассмотрим код, где найден-вый путь — значение параметра возврата. public static void TestSearchO SearchSolution.DepthFirstSearch els = new SearchSolution. ZepthFirstSearch (SearchSolution.Node.RootNodes); Node[] foundRoutel = els.FindRoute("Montreal", "Seattle"); NodeM foundRoute2 = els . FindRoute ("New York", "Seattle"); Теперь обратите внимание на код, который использует переменную-член. public static void TestSearchO { SearchSolution.DepthFirstSearch els = new SearchSolution. DepthFirstSearch(SearchSolution.Node.RootNodes); els.FindRoute("Montreal", "Seattle"); Node[] foundRoutel = els.FoundRoute; els.FindRoute("New York", "Seattle"); Node[] foundRoute2 = els.FoundRoute; } И снова кажется, что оба варианта адекватны. Но есть небольшое различие, впол¬не достаточное, чтобы иметь значение. В реализации проверки, где найденный марш¬рут — это возвращаемое значение, переменные foundRoutel и foundRoute2 представ¬ляют маршруты, относящиеся непосредственно к искомому маршруту. Не имеется никакой вероятности, что переменная foundRoutel сможет представить маршрут Нью-Йорк-Сиэтл. У кода с использованием переменной-члена это вполне допустимо, пере¬менная foundRoutel вполне способна представлять маршрут Нью-Йорк-Сиэтл, как по¬казано в следующем коде: public static void TestSearchO { SearchSolution. DepthFirstSearch els = new SearchSolution. DepthFirstSearch(SearchSolution.Node.RootNodes); els.FindRoute("Montreal", "Seattle"); els.FindRoute("New York", "Seattle"); Node[] foundRoutel = els.FoundRoute; Node[] foundRoute2 = els.FoundRoute; } Меняя порядок вызова метода FindRoute ()' и ссылки на переменные-члены FoundRoute, переменные foundRoutel и foundRoute2 сошлются на тот же найденный маршрут, а именно маршрут Нью-Йорк-Сиэтл. Но это не лучшая идея. Следующий при¬мер демонстрирует, что переменные-члены не имеют никакого непосредственного от¬ношения к методам и могут изменяться независимо. g Поэтому возврат найденного маршрута из метода — лучший и более надежный подход. На заметку. Переменные-члены полезны, когда вы хотите сохранять или возвращать данные, кото¬рые охватывают несколько вызовов метода или не зависят от порядка вызова методов. Когда у вас данные, зависимые от порядка вызываемых методов, лучше использовать ключевое слово return или параметр out. Ниже приведен полный случай проверки, включающий код верификации перелета из Монреаля в Сиэтл. public static void TestSearchO { SearchSolution.DepthFirstSearch els = new SearchSolution.DepthFirstSearch(SearchSolution.Node.RootNodes); SearchSolution.Node[] foundRoute = els.FindRoute("Montreal", "Seattle"); if (foundRoute.Length != 2) { Console.WriteLine("Incorrect route as route has two legs"); } if (foundRoute[0] .CityName.CompareTo ("Los Angeles") != 0) { Console.WriteLine("Incorrect as first leg is Los Angeles"); } На заметку. Мы уже использовали конструкцию if в предыдущих главах. Она проверяет условие и зыполняет код, содержащийся в ее фигурных скобках, если это условие истинно. Знак ! = озна¬чает неравенство. Более подробная информация по этой теме приведена ниже в этой главе.