Преамбула
Несколько дней назад мой коллега и блоговод бесайланд, выложил в посте свою реализацию подсистемы рейтингования на сайте. Это решение он использовал в одном из проектов, и, не смотря на критику, не удосужился доработать и по сей день, на что я в комментариях и обратил его внимание, из чего в итоге вырос этот пост — я, как и обещал, представляю собственное видение решения проблемы.
Правила игры
Что бы было что и как сравнивать, прежде всего необходимо определиться с критериями оценки решения и досконально разобрать каждую из представленных архитектур.
Думаю все согласятся, что основными критериями качественного ПО являются:
- расширяемость
- удобочитаемость (удобство поддержки и эксплуатации)
- отказоустойчивость
Значит и оценивать будем по этим критериям, сведя все на уровень кода и выставляя баллы в зависимости от сложности достижения данных целей предложенными решениями.
Так разобьём архитектуру на составляющие:
- компоненты (классы, интерфейсы)
- связи
- взаимодействие (порядок вызовов для достижения цели)
В каждой группе возможны разные варианты решения — от простых в поддержке до сложных, что мы и будем отмечать. Так, например, компоненты могут быть:
- интерфейсы (самые простые в реализации, сопровождении и наименее подвержены ошибкам. Значит у них сложность будет 1)
- абстрактные классы (сложнее интерфейсов за счет того что уже могут содержать реализацию, а значит и ошибок при эксплуатации может быть больше и разбираться в них сложнее. Оценка сложности — 2)
- классы (обычные классы. Наибольшее кол-во ошибок связано с ними, так как они представляют полностью и интерфейс и реализацию. Будет за каждый присваивать 3 балла сложности)
Связей бывает чуть больше. Так два компонента можно связать посредством:
- реализации интерфейсов (самая простая связь, практически не влияет на связанность и посему получает сложность 1 балл)
- делегирования (передав в метод объекта другой объекта, даже посредством раскрытия только интерфейса, мы их уже связали. Хоть и не достаточно тесно. 2 балла за сложность)
- уточнения/наследования (связь подразумевает наследование одного класса от другого, а значит поведение будет зависеть не от одного, а сразу от двух классов. Конечно такое решение поддерживать еще сложнее и его сложность оценим в 3 балла)
- инстанциирования (порождение объекта внутри другого объекта. Операция скрытая и полностью подавляет инвариантность системы. Очень сложно изменить поведение, если код скомпилирован. Наивысшая сложность в нашем случае и обладает оценкой 4)
Так же необходимо учитывать что мы разрабатывает универсальную подсистему, а значит ее связность должна быть максимальной, поэтому каждая связь между подсистемами, нарушающая связность, будет увеличивать сложность этой связи в два раза.
Ну что ж, разобравшись с правилами, можно переходить к анализу решений.
Архитектура бесайланда
Не смотря на то, что коллега не удосужился предоставить диаграмм своего решения, а ограничился лишь текстовым описанием да несколькими примерами на не стыкующемся псевдокоде, попробуем провести комплексный анализ. И начнем с превращения текста в одну простую UML-картинку:
Из приведенных примеров некоторые связи остались не ясными, идимо из-за того, что статья писалась на коленке и не было времени проверить отношения между объектами. Например не понятны зависимости между Vote_Voter, Vote_Voter_User и Vote_Voter_Guest. С одной стороны в примере использования видно, что Vote_Voter_User является Vote_Voter, так как метод .addOpinion(+1, voter) принимает вторым параметром Vote_Voter, а не Vote_Voter_User, а значит последний должен либо наследовать Vote_Voter, либо Vote_Voter должен быть интерфейсом. Но ни одно из этих предположений не верно, так как Vote_Voter_User как и Vote_Voter наследуются в примере от ActiveRecord, а значит они являются параллельными в иерархии и не могут взаимозаменяться.
С другой стороны не описаны некоторые участники модуля, а именно: Collection, Variant и Array. Что это за структуры приходится только догадываться.
Так же у внимательных читателей могут возникнуть вопросы о природе происхождения методов asVoter и asVoteObject у классов User и Article, так как они не навязываются ни одном интерфейсом, но это мы оставим на совести автора. Будем считать что он стоит с палкой над программистами, когда им нужно получать соответствующие объекты в коде.
Для рассмотрения архитектуры упростим решение до двух субъектов голосования (User и Guest), одного объекта (Article) и одной сложной стратегии (Vote_Strategy_Binary_Rational). Основываясь на этих упрощениях, подведем итоговую статистику этого решения:
- 1 абстрактный класс (2 балла)
- 2 интерфейса (2*1, 2 балла)
- 8 классов (8*3, 24 балла)
между этими компонентами существует:
- 4 зависимости уровня делегирования (одна выходит за рамки подсистемы. 1*2*2+3*2, 10 баллов)
- 1 зависимость уровня инстанциирования (выходит за рамки подсистемы, 1*4*2, 8 баллов)
- 3 зависимости уровня уточнения (2 выходят за рамки подсистемы, 2*2*3 + 1*3, 15 баллов)
- 2 зависимости уровня интерфейсов (одна выходит за рамки подсистемы, 1*2*1 + 1*1, 3 балла)
Далее необходимо описать присущий этому решению порядок развертывания для конечного программиста (цель: проголосовать за объект и получить его итоговое кол-во голосов):
- создать необходимые стратегии
- описать связи между субъектами голосования и объектами типа
Vote_Voter_* - реализовать интерфейс
Vote_Votableу всех объектов голосования - при голосовании принять решение какой объект из
Vote_Voter_*должен быть инстанциирован или получен из других субъектов голосования - через отношение объекта голосования с его
Vote_Objectдобавить голос, делегируя субъект голосования и оценки (кстати тут стоит заметить, что если объект может быть оценен только по бинарной стратегии (+1 или -1), то на этом этапе проверка на допустимость не проводиться. таким образом мы можем передать +5 и все будет отлично — это еще один минус, но учитывать его не будем, так как объективно ему балл не выставить) - через стратегию объекта голосования получить результирующее значение, методом получения и делегирования всех переданных голосов за объект голосования
Подсчитаем кол-во действий необходимых для достижения основной цели на примере авторского кода:
Vote_Voter voter = Auth.getUser() ? Auth.getUser().asVoter() : Vote_Voter_Guest::getForIpAddress( Request.getIpAddress() ); article.asVoteObject().addOpinion( +1, voter ); print article.getVoteStrategy().getAggregatedOpinion( article.asVoteObject().getGivenOpinions() );
Нас интересуют только последние два строки (первые три вообще сложно оценить из-за выпадания вне контекста). В них можно насчитать 6 вызовов. Это еще +6 баллов.
Ну что ж, инвентаризация окончена, данный подход набрал 70 баллов сложности, и теперь изучим предложенное мной решение и сравним их.
Архитектура предложенная мной
Для удобства сравнения я старался минимально отклоняться от представленного ранее решения, но внося кардинальные изменения для целостности и модульности.
Сразу представляю UML схему, по которой буду вести разъяснения.
Как видно — структуру стратегий я оставил совершенно не измененную. И хотя внутреннее Я сопротивлялось, так как нужда в разных принципах подсчета рейтинга в рамках одной системы довольно не очевидна, но для универсальности решения я стратегии оставил.
Первое что притерпело изменений — это наличие интерфейсов Vote_Object (ранее представленный классом + интерфейсом для оболочки) и Vote_Subject, который ранее был группой классов типа Vote_Voter*.
Vote_Opinion у меня упростился до имени Vote, что полностью описывает свое назначение.
Ну а последним добавился всего один класс Vote_Manager. Он же Фасад, он же Одиночка, он же основное звено подсистемы.
Что бы понять значение тех 5 методов новосозданного менеджера, рассмотрим несколько примеров использования.
Изначально подсистема требует небольшого конфигурирования, что бы в последствии более изящно можно было с ней обращаться и что бы она была более защищенной.
Vote_Manager vm = Vote_Manager.getInstance(); // далее в примерах будем использовать переменную vm, а не получать ее из Одиночки vm.setDefaultStrategy( new Vote_Strategy_Binary_Rational() ) .configure( typeof( Article ), new Vote_Strategy_Binary_Rational() );
Что мы тут сделали?
В первой строке получили объект-одиночку менеджера, во второй установили стратегию по-умолчанию для всех объектов голосования (а объекты голосования — любые объекты, которые реализуют интерфейс Vote_Object), а в третьей указали что для объекта Article будет использоваться специфичная стратегия, а не указанная по-умолчанию (хотя в данном случае они и одинаковы).
Далее нам нужно для любых бизнес-объектов системы, которые должны стать объектами голосования, реализовать интерфейс Vote_Object, я для любых субъектов (для тех, кто будет за что-то голосовать) — реализовать интерфейс Vote_Subject. Интерфейсы примитивны и требуют реализации всего одного метода getId, возвращающего уникальный идентификатор для каждого объекта этих классов.
А дальше нужно использовать систему.
Проголосовать:
vm.vote( new Article(), new User(), +5 );
Хочу сразу заметить, что внутри метод vote утилизирует переданную при конфигурации стратегию для переданного объекта и если выставленный голос выходит за рамки возвращенного значения Vote_Strategy.getPossibleValues(), то генерируется исключение и голос не учитывается.
Далее получим рейтинг для определенной статьи:
Article article = ArticlesStorage.get( someId ); float rate = vm.countFor( article ); // получаем рейтинг, высчитаный на основе переданной в конфигурации стратегии
И конечно же мы очень просто можем получить голосовал ли конкретный участник за объект и сколько выставил баллов:
User user = UserStorage.get( someId ); Vote vote = vm.getVoteFor( article, user ); vote.getValue(); // получаем поставленную оценку или NULL, если не голосовал
Отдельно хотел бы рассмотреть собственную реализацию зарегистрированых и не зарегистрированых пользователей. Я исходил из того, что если не зарегистрированному пользователю можно голосовать в системе, то ему, вероятно, можно совершать и другие действия. Именно поэтому для него был создан класс UnregisteredUser, который как и RegisteredUser реализует интерфейс User (который в свою очередь расширяет интерфейс Vote_Subject) и может предоставлять свой идентификатор, например на основании собственного IP адреса или чем-то еще — это остается на усмотрении разработчика. А можно было вообще унаследовать зарегистрированного пользователя от не зарегистрированного и удалить интерфейс для простоты, но не будем на этом зацикливаться.
Вот собственно и все — осталось все тщательно подсчитать и выделить плюсы, которых мы смогли добиться.
Статистика и бухгалтерия:
- 1 абстрактный класс (2 балла)
- 4 интерфейса (4*1, 4 балла)
- 6 классов (6*3, 18 баллов)
между этими компонентами существует:
- 3 зависимости уровня делегирования (3*2, 6 баллов)
- 1 зависимость уровня инстанциирования (1*4, 4 балла)
- 1 зависимости уровня уточнения (1*3, 3 балла)
- 5 зависимости уровня интерфейсов (два из них выходят за рамки подсистемы, 2*2*1+3*1, 7 баллов)
Как видно связанность стала намного слабее, хотя кол-во классов и связей в общем не сократилось. Использование подсистемы стало гараздо прозрачнее — для достижения цели всего 2 вызова (за которых еще +2 балла) вместо 6, которые были ранее. Система стала более защищенная, благодаря контролю переданных баллов голосования непосредственно при голосовании. У бизнес-объектов теперь нет ненужной ответственности за предоставление стратегий для подсистемы, так как теперь стратегии — непосредственная часть подсистемы, а значит ее связность возросла.
Итого решение набрало 46 баллов сложности, что значительно меньше, а значит решение гараздо проще в обслуживании, использовании и расширении, что и требовалось реализовать.
Выводы
В заключение ничего не буду говорить — пусть все останется на суд читателя. Оговорюсь только о принятом решении идентификации типов объектов и субъектов через вызов конструкции typeof( Object ). Думаю что такого решения вполне достаточно, но тут можно развернуться для последующей оптимизации и в интерфейсах Vote_Object и Vote_Subject добавить метод getType, который сможет возвращать например целочисленный уникальный идентификаторы типа. Но это уже изыски :)
П.С. Отдельно хочу упомянуть что для обоих подходов не учитывался вопрос производительности, так ка каждое из них требует молотка и лобзика перед деплойментом, а посему баллы за производительность не начислялись и она вовсе не анализировалась.

