Приветствую Вас, Гость · RSS Понедельник, 07.10.2024, 21:24










Главная » 2013 » Июль » 16 » Множественные ветвления и шаблон «Правила»
21:08
 

Множественные ветвления и шаблон «Правила»

8 мая в 02:41

Множественные ветвления и шаблон «Правила» tutorial

Здравствуйте, уважаемые хабрачитатели. В этой статье я хотел бы поделиться знаниями об одном небольшом и простом, но полезном шаблоне, про который обычно не пишут в книжках (возможно, потому, что он является частным случаем шаблона «Команда»). Это шаблон «Правила» (Rules Pattern). Вероятно, для многих он будет очень знакомым, но кому-то будет интересно с ним познакомиться.




Суть вопроса

Очень часто при разработке сложной логики возникает дерево вложенных if-ов, которое может выглядеть, например, так:
Ужасный кодpublic double CalculateSomething(Condition condition) { //выполняется первое условие if(condition.First...) ... //выполняется второе условие if(condition.Second...) ... //специальное условие номер один if(condition.AnotherFirst...) { //но при этом выполняется первое условие if(condition.First) ... else... } //специальное условие номер два if(condition.AnotherSecond...) { //но при этом выполняется второе условие if(condition.Second) ... else... } //и еще одно добавим if(condition.YetAnotherFirst) { //... if(condition.AnotherFirst && condition.Second) ... else { ... } } // O_o }

Знакомо? Итак, какие тут встречаются проблемы?

Проблема 1: Растущая цикломатическая сложность. Если говорить просто, то цикломатическая сложность — это глубина вложенности if-ов и циклов c учетом логических операторов. Инструменты анализа кода позволяют оценить этот параметр для всех участков кода.
Считается, что параметр цикломатической сложности для отдельного участка кода не должен превышать 10. Из этой проблемы растет следующая.

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

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

Тут и приходит на помощь шаблон «Правила». Его структура очень проста:
Uml-диаграмма структуры

Здесь класс Evaluator содержит коллекцию реализаций интерфейса IRule. Evaluator выполняет правила и решает, какое правило надо использовать для получения результата. Чтобы понять, как это работает и выглядит в коде, рассмотрим небольшой пример на C#.

Пример. Игра в кости (наподобие «Тали»)

Правила игры:

Игрок кидает одновременно 5 кубиков, и в зависимости от их комбинации получает определенное количество очков.
Комбинации могут быть следующими:
1 X X X X — 100 очков
5 X X X X — 50 очков
1 1 1 X X — 1000 очков
2 2 2 X X — 200 очков
3 3 3 X X — 300 очков
4 4 4 X X — 400 очков
5 5 5 X X — 500 очков
6 6 6 X X — 600 очков

Примеры комбинаций:
[1,1,1,5,1] — 1150 очков
[2,3,4,6,2] — 0 очков
[3,4,5,3,3] — 350 очков

Конечно все они могут быть и другими, и их может быть гораздо больше, но об этом позже.

Делай раз! Без шаблонов.

Попробуем описать логику игры без применения шаблона «Правила», так, как бы мы писали на уроке информатики в 8-м классе (естественно, не снабдив наш плохой код комментариями — кому они нужны!)
Плохой, негодный класс Gamepublic class Game { public int Score(int[] roles) { int score = 0; for(int i=1; i<7; i++) { int count = CountDiceWithValue(roles, i); count = ScoreSetOfN(count, GetSetSize(i), SetSetScore(i), ref score); score += count * GetSingleDieScore(i); } return score } private int GetSingleDieScore(int val) { if(val==1) return 100; if(val==5) return 50; return 0; } private int GetSetScore(int val) { if(val==1) return 1000; return val*100; } private int GetSetSize(int val) { return 3; } private int ScoreSetOfN(int count, int setSize, int setScore, ref int score) { if(count>=setSize) { score += setScore; return count - 3; } return count; } private int CountDiceWithValue(int[] roles, int val) { int count = 0; foreach (int r in roles) { if (r == val) count++; } return count; } }

Делай два! Добавление правил? Модульные тесты.

Вроде бы 50 строк кода это очень мало. Но что будет, если правила игры будут изменяться и добавляться?
Например, мы добавим правила для различных комбинаций кубиков:

1 1 1 1 X — 2000
1 1 1 1 1 — 4000
1 2 3 4 5 — 8000
2 3 4 5 6 — 8000
A A B B X — 4000
и так далее.

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

Делай три! Применяем шаблон «Правила»

1. Определим интерфейс IRule с методом Eval, который нужен для оценки количества очков за определенный набор кубиков.
IRule.cspublic interface IRule { ScoreResult Eval(int[] dice); }

2. Создадим класс RuleSet, который будет определять набор правил, логику для добавления правила и логику выбора лучшего из правил, которое можно применить к данному набору кубиков:
RuleSet.cspublic class RuleSet { //коллекция правил private List<IRule> _rules = new List<IRule>(); //добавление правила public void Add(IRule rule) { _rules.Add(rule); } //оценка лучшего правила - того, которое возвращает максимальное количество очков public IRule BestRule(int[] dice) { ScoreResult bestResult = new ScoreResult(); foreach(var rule in _rules) { var result = rule.Eval(dice); if(result.Score > bestResult.Score) { bestResult = result; } return bestResult.RuleUsed; } } }

3. Конечно, небольшой класс-помощник
ScoreResult.cspublic class ScoreResult { //результат подсчета очков public int Score {get;set;} //какие кубики были использованы (чтобы кубик не участвовал в оценке другими правилами) public int[] DiceUsed {get;set;} //какое правило было использовано, чтобы определить, какое правило было лучшим (в методе BestRule) public IRule RuleUsed {get;set;} }

4. И определим сами правила.
ConcreteRules.cs//правило для одного кубика public class SingleDieRule : IRule { private readonly int _value; private readonly int _score; public SingleDieRule(int dieValue, int score) { _dieValue = dieValue, _score = score } //переопределенный метод интерфейса - оценка очков для набора кубиков public ScoreResult Eval(int[] dice) { //класс-помощник var result = new ScoreResult(); //использованные в оценке кубики (кубики с номерами очков) - для дальнейшего исключения result.DiceUsed = dice.Where(d=>d == dieValue).ToArray(); //логика подсчета очков result.Score = result.DiceUsed.Count() * _score; //использованное правило - для определения лучшего правила по очкам result.RuleUsed = this; return result; } } //другие правила в том же духе

5. В нашем случае классом Evaluator со схемы будет класс Game, он не будет содержать почти ничего, кроме логики добавления правил и логики подсчета очков
Game.cs - Evaluatorpublic class Game { private readonly RuleSet _ruleSet = new RuleSet(); public Game(bool useAllRules) { //старые правила _ruleSet.Add(new SingleDieRule(1,100)); _ruleSet.Add(new SingleDieRule(5,50)); _ruleSet.Add(new TripleDieRule(1,1000)); for(int i=2; i<7; i++) { _ruleSet.Add(new TripleDieRule(i, i*100)); } //дополнительные правила if(useAllRules) { _ruleSet.Add(new FourOfADieRule(1,2000)); _ruleSet.Add(new SetOfADieRule(5,1,4000)); _ruleSet.Add(new StraightRule(8000)); _ruleSet.Add(new TwoPairsRule(6000)); for(int i=2; i<7; i++) { _ruleSet.Add(new FourOfADieRule(i,i*200)); _ruleSet.Add(new SetOfADieRule(i,i*400)); //... } } } //Пользователь может добавлять к игре свои правила public void AddScoringRule(IRule rule) { _ruleSet.Add(rule); } //подсчет очков public int Score(int[] dice) { int score = 0; var remainingDice = new List<int>(dice); var bestRule = _ruleSet.BestRule(remainingDice.ToArray()); //проходим по правилам последовательно с выбором лучшего и удалением кубиков с подсчитанными очками while(bestRule!=null) { var result = bestRule.Eval(remainingDice.ToArray()); foreach(var die in result.DiceUsed) { remainingDice.Remove(die); } score+=result.Score; bestRule = _ruleSet.BestRule(remainingDice.ToArray()); } return score; } }

Ура! Задача решена! Теперь каждый класс занимается тем, что ему положено, цикломатическая сложность не растет,
а новые правила добавляются легко и просто. Выбор правила теперь осуществляется при помощи класса RuleSet, содержащего набор правил, а добавление правил и подсчет очков — классом Game.

О чем нужно помнить?

При проектировании программы, содержащей логику, основанную на правилах, полезно иметь ввиду следующие вопросы:
— Следует ли правилам быть read-only в отношении системы, чтобы не изменять ее состояние?
— Должны ли быть зависимости между правилами? Стоит ли уделить внимание порядку выполнения правил, в случае, когда одно правило может требовать результат работы другого правила для работы.
— Должны ли порядок выполнения правил быть строго определенным?
— Должны ли быть приоритеты в выполнении правил?
— Стоит ли позволять конечным пользователям редактировать правила?
и многие другие.

Пара слов о системах правил бизнес-логики (Business Rules Engines)

Концепция Business Rules Engines очень близка к идее шаблона «Правила» — это системы, которые позволяют
определять системы правил для бизнес-логики. Обычно они имеют некий графический интерфейс и позволяют пользователям определять правила и иерархии правил, которые могут храниться в базе данных или файловой системе. В частности, данный функционал имеет и Workflow Foundation от Microsoft.

Резюме

1) Используем шаблон «правила», когда надо избавиться от сложности условий и ветвлений
2) Помещаем логику каждого правила и его эффекты в свои классы
3) Отделяем выбор и обработку правил в отдельный класс — Evaluator
4) Знаем, что есть готовые «движковые» решения для бизнес-логики

Большое спасибо за внимание, надеюсь, моя творческая переработка данного учебного материала кому-нибудь поможет.
* Источником вдохновения для данной статьи послужил урок «Rules Pattern» из курса «Design Patterns» сайта pluralsight.com от Стива Смита

Developers, stick with Russians – work in London

Переводы без
открытия счета

Переводы
через QR-Код

Новая функция
«Мой контроль»

Извините, но это ужасно. Визуально, тот код, который назывался плохим для игры в кости, выглядит лучше. Он меньше.
Да, он непонятный, но только потому, что совершенно безобразно написаны имена, тройка прыгает по коду.
Если его причесать и если класс действительно должен считать кубики и там написан минимум на данный момент (YAGNI), то он будет лучше. Но он правда и ягни не отвечает. Просто странно ни в какие ворота написан.

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

Но я лично, паттерны и не люблю. Паттерны появляться должны самостоятельно, если надо. Обычно, они нужны для гибкости. Но программы не должны быть гибкими. Программы должны быть точными. А это буквально наоборот, чем то, что здесь написано. Т.е. вы с чего-то думаете о каком-то вымышленном завтра, когда вдруг внезапно появятся новые правила. И наворотили бог весть что. Вывернули логику, заставили страдать компилятор.

Не воспринимайте как оскорбление, просто давайте по другому взглянем на задачу. Я всегда говорил, что язык программирования — это язык, а программирование — это перевод требований. Как есть. Без никаких домыслов о завтра. Завтра будет завтра. Если записано как есть, то и менять не очень много надо будет. Также, в коде должно быть максимальное соотношение кода, которое прямо относится к задаче и к требованиям. Их должно быть видно.

Давайте и посмотрим:
1 X X X X — 100 очков
5 X X X X — 50 очков
1 1 1 X X — 1000 очков
2 2 2 X X — 200 очков
3 3 3 X X — 300 очков
4 4 4 X X — 400 очков
5 5 5 X X — 500 очков
6 6 6 X X — 600 очков
Это буквально юз-кейсы. Какие требования?
1. Порядок не важен.
2. Из всех комбинаций ищем наборы комбинаций (назову шаблонов), которые дают максимальное количество очков.
И вот код:
int GetScore(int[] roles) { List<int> rolesList = roles.ToList(); Func<int> getScoreFromBestTemplate = () => { var templatesWithScores = new [] { new { Template = new List<int> {1}, Score = 100 }, new { Template = new List<int> {5}, Score = 50 }, new { Template = new List<int> {1, 1, 1}, Score = 1000 }, new { Template = new List<int> {2, 2, 2}, Score = 200 }, new { Template = new List<int> {3, 3, 3}, Score = 300 }, new { Template = new List<int> {4, 4, 4}, Score = 400 }, new { Template = new List<int> {5, 5, 5}, Score = 500 }, new { Template = new List<int> {6, 6, 6}, Score = 500 }, }; Func<List<int>, List<int>, List<int>> deleteMatchElements = (source, template) => { var sourceCopy = source.ToList(); template.ForEach(item => sourceCopy.Remove(item)); return sourceCopy; }; Func<List<int>, List<int>, bool> isMatch = (source, template) => deleteMatchElements(source, template).Count == source.Count - template.Count; var templateWithMaxScore = (from tws in templatesWithScores where isMatch(rolesList, tws.Template) orderby tws.Score descending select tws ).FirstOrDefault() ?? new { Template = new List<int> {}, Score = 0 }; rolesList = deleteMatchElements(rolesList, templateWithMaxScore.Template); return templateWithMaxScore.Score; }; int sumScore = 0; int score = 0; do { score = getScoreFromBestTemplate(); sumScore += score; } while (score != 0); return sumScore; }

Согласитесь, так читаемость намного лучше. Требования видны невооруженным глазом. Не надо разбираться с конструкторами классов. Да и классы писать не надо. Буквально видно по шагам, как откуда всё получается. Не должно быть у простых задач много кода!
В нем да, жестко зашиты требования, как поняты мною на данный момент. Да, в этом коде заложено, что я выбираю всегда самый «дорогой» шаблон. Но ведь так и надо на данный момент. Именно это и требуется из задачи. Нет никакого будущего. Если меня попросили перевести на английский «собачка перебежала дорогу», то именно так и надо поступать, а не писать «ххх перебежало дорогу». Чтобы когда-то послезавтра кошку туда подставить. Когда требования выражены четко, когда кода минимум, тогда и переписать будет не проблема, если изменятся требования.

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

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

А вообще это холивар на тему того, нужны шаблоны или нет. На мой взгляд, нужны. Это язык, который понятен всем. А ввязываться в спор я не хочу, простите. Кому надо — тому пригодится.

Спорить не зачем и не о чем. Просто пример для этого паттерна неудачный.

Паттерны сами по себе в отрыве от задачи — зло. Я не знаю полезных паттернов. Каждый из них что-то плохое, да и делает.

Вы во вступлении написали, что бывает надо писать много ветвлений, а этот паттерн позволяет избавиться. Если в отрыве от задач, то стремиться избавиться от ифов — это просто объявить войну компилятору и языку программирования. Это добавление нового измерения, выворачивание условий наизнанку и создание своего «настраиваемого» языка. Т.е. есть в C# конструкция if, но кому-то не нравится этот язык и он решает создать свой. Более гибкое и настраиваемое решение — это всегда более ущербное и ненадежное.

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

Вот так я отношусь к паттернам. И еще, они и сами появляются, если заниматься постоянным рефакторингом.

Да, пожалуй, Вы в чем-то правы. Но что будет, если в Ваш код добавлять условия для пар? то есть 11 22 3, 22 33 4 и так далее? Можно ли там будет написать не Template, а именно логику для определения пары, и прочих правил которых может быть несколько?

Так получился у меня код, потому что я следую определенной стратегии. Представляю, что требования имеют смысл. И я этот смысл перевожу на язык программирования. Как можно точнее и как можно с меньшими своими домыслами. Я и не думал особо.

Посмотрев на
1 X X X X — 100 очков
5 X X X X — 50 очков
1 1 1 X X — 1000 очков
2 2 2 X X — 200 очков
3 3 3 X X — 300 очков
4 4 4 X X — 400 очков
5 5 5 X X — 500 очков
6 6 6 X X — 600 очков
я нигде не видел прямых указаний, что в правиле могут быть только по одной или только по три цифры. Что в правиле только одинаковые цифры. Я просто вижу набор цифр и соответствующее число очков. Глаза, конечно, замечают неявные закономерности, но смысл надо переводить как можно более прямо. А Вы, видимо, прочитали правила иначе. Вы сразу по умолчанию выписали неявные закономерности, как исходные требования. И у Вас возникло на ровном месте многообразие способов задания правил. И поэтому паттерн.
Поэтому пример для паттерна неудачный. Он лишний.

Замечу, что если бы требования были заданы в словесной форме, так:
«Правилом считаем выпадение некоторого числа одинаковых значений 1..6. За каждое правило начисляем очки.
Правила бывают с одним значением и с тремя.
Правила:
если выпадает 1-ца один раз, то начисляем 100;

если выпадает 4-ка три раза, то начисляем 400;
… „
То в таком виде стратегия перевода требования заставила бы создавать правила именно так. Но далее, в процессе рефакторинга, не применяя никаких паттернов, скорее всего алгоритм подсчета был бы вынес в один метод, данные, которые ему передаются, тоже сведены в список. Далее, по сути, создание правил — это всего лишь конструктор для этих списков. В конце концов, из-за рефакторинга снова пришли бы где-то к такому же коду.

И наоборот. Если я написал код вот так, как сейчас, при нынешних требованиях, но вдруг начали приходить “неудобные» требования. Например: «За 500 выпадений 6-рок, начисляем миллион». То теперь, становится ясно, что писать 500 раз 6 как минимум неудобно. Тогда добавляется конструктор. А именно: в коде анонимный тип заменяется на класс, в нем эти же два свойства, и добавляем конструктор, которому передаем (int diceRollNumber, int count). А сам алгоритм не меняется.

Как видим, снова пришли к тому же коду.
Просмотров: 544 | Добавил: hamaget | Рейтинг: 0.0/0
Всего комментариев: 0
Создать бесплатный сайт с uCoz
Copyright MyCorp © 2024