Feed Rss



Окт 20 2009

Системы рейтингования, попытка сделать лучше

Преамбула

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

Правила игры

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

Думаю все согласятся, что основными критериями качественного ПО являются:

  • расширяемость
  • удобочитаемость (удобство поддержки и эксплуатации)
  • отказоустойчивость

Значит и оценивать будем по этим критериям, сведя все на уровень кода и выставляя баллы в зависимости от сложности достижения данных целей предложенными решениями.

Так разобьём архитектуру на составляющие:

  • компоненты (классы, интерфейсы)
  • связи
  • взаимодействие (порядок вызовов для достижения цели)

В каждой группе возможны разные варианты решения — от простых в поддержке до сложных, что мы и будем отмечать. Так, например, компоненты могут быть:

  • интерфейсы (самые простые в реализации, сопровождении и наименее подвержены ошибкам. Значит у них сложность будет 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, который сможет возвращать например целочисленный уникальный идентификаторы типа. Но это уже изыски :)

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

Понравился пост? Подпишись на RSS!

Метки: , , , , ,