Проектируем рейтинговое оценивание

    Часто требуется реализовать возможность рейтингового оценивания того или иного объекта (заметки, комментария, цитаты, фотограммы, видеоролика и т. д.) посетителями сайта. Как это запрографировать?

    Прежде всего мы имеем объект оценивания и субъект оценивания. Последним могут быть, например, зарегистрированные пользователи, незарегистрированные пользователи (гости) и пр.

    Для того, чтобы обеспечить слабую связанность конкретных сущностей предметной области, к которым мы привязываем возможность рейтингового голосования, с модулем, реализующим нашу задачу, мы выделяем отдельные классы для объекта (Rating_Object) и субъекта (Rating_Subject). Оба эти класса — конкретные и реализованы как active record. Чтобы иметь возможность привязывать всяческие статьи и фотограммы к экземплярам Rating_Object, мы предусматриваем интерфейс Rating_Ratable:

    interface Rating_Ratable {
        /**
         * @return Rating_Object
         */
        public function asRatingObject();
    }


    Этот интерфейс теперь может быть реализован в классах Article и т. д., например, так:

    class Article extends ActiveRecord implements Rating_Ratable {
        public function setTableDefinition() {
            $this->hasReferenceColumn("rating_object_id");
        }
    
        public function setUp() {
            $this->hasOne("Rating_Object as rating_object", array("local" => "rating_object_id", "foreign" => "id"));
        }
    
        public function asRatingObject() {
            return $this->rating_object;
        }
    }


    Аналогично поступаем с субъектами оценивания. Необходимо лишь, чтобы мы хранили идентификатор субъекта для каждой конкретной сущности, которая может голосовать. Поскольку практически всегда у нас есть active record для текущего посетителя сайта (гостя можно и нужно идентифицировать по комплексу сведений и, соответственно, заводить для него запись в базе данных, — если вы это ещё не делаете, самое время начать), достаточно добавить в соответствующий класс поле rating_subject_id.

    Если в будущем у нас появится необходимость принимать голоса за рейтинг от сущностей, не являющихся посетителями сайта (например, брать данные из сторонних рейтингов), мы так же легко сможем привязать к каждому из экземпляров таких сущностей отдельный экземпляр Rating_Subject.

    Итак, у нас есть те, кто могут оценивать, и то, что можно оценивать. Нужно их связать. Для этого вводим класс Rating_Vote, — active record с полями object_id, subject_id, opinion. Последнее поле представляет собой конкретный выбор на шкале рейтинга — конкретную оценку, которую этот субъект поставил этому объекту.

    Здесь необходимо отметить, что спектры возможных оценок могут быть разными. Можно ограничиться двумя оценками «хорошо» и «плохо» (мой личный выбор), можно предлагать поставить от одной до пяти звёзд (а можно — от одной до десяти) и т. д. Естественно, конкретная шкала оценивания определяется для конкретного объекта: например, цитаты и комментарии оцениваем бинарно, а статьи и фотограммы — по шкале 1…5.

    Вместе с тем необходимо определить и способ вычисления итогового рейтинга. Скажем, для бинарного выбора «плюс-минус» можно брать за итоговую оценку разницу между количеством плюсов или количеством минусов. Но можно — отношение количества плюсов к общему количеству голосов (мой личный выбор). Для шкалы из нескольких вариантов можно брать среднее арифметическое, среднее гармоническое, моду или медиану. Опять же, для конкретного объекта задаём конкретный способ.

    Класс, определяющий, с одной стороны, шкалу оценок, а с другой стороны, метод вычисления итогового рейтинга, назовём стратегией оценивания. Введём интерфейс Rating_Strategy:

    interface Rating_Strategy {
        /**
         * @return array of float
         */
        public function getRatingOptions();
    
        /**
         * @param Rating_Vote_Collection $votes
         * @return float
         */
        public function getAggregatedOpinion(Rating_Vote_Collection $votes);
    }
    


    Почему для отражения конкретной возможной оценки выбран float? Это позволит нам отразить как дискретные шкалы оценивания (сопоставив варианты целым числам, каковые суть подмножество вещественных), так и плавающую шкалу, если нам такая понадобится. Итоговая же оценка может быть дробной даже для целочисленной шкалы (например, оценивать по шкале от 1 до 10 и брать среднее арифметическое оценок). Выбор float даёт нам возможность покрыть практически все варианты. Поле opinion в классе Rating_Vote, соответственно, тоже float.

    Класс Rating_Vote_Collection отражает, как нетрудно догадаться, коллекцию объектов Rating_Vote — то есть набор полученных голосов за конкретный объект. Зачем нужен этот класс, почему не обойтись просто массивом Rating_Vote? Голосов может быть очень много, несколько тысяч, — загружать их все из базы данных в оперативную память расходно, да и незачем. Почти всегда для получения итоговой оценки необходимо и достаточно иметь на руках сведения об агрегатном количестве голосов за каждый из вариантов. Поэтому в классе Rating_Vote_Collection мы сделаем соответствующий метод getOpinionCounts, который вернёт массив вида: [+1 → 100, −1 → 50] (100 «хорошо», 50 «плохо»). Но предусмотрим также lazy-загрузку на тот случай, если захотим реализовать нетривиальную стратегию (наделяющую, например, голоса весами в зависимости от кармы голосовавших).

    Несколько примеров стратегий оценивания:

    abstract class Rating_Strategy_Binary implements Rating_Strategy {
        const GOOD  = +1;
        const BAD   = -1;
    
        public function getRatingOptions() {
            return array(self::GOOD, self::BAD);
        }
    }
    
    class Rating_Strategy_Binary_Subtraction extends Rating_Strategy_Binary {
        public function getAggregatedOpinion(Rating_Vote_Collection $votes) {
            $counts = $votes->getOpinionCounts();
            $good   = isset($counts[self::GOOD]) ? $counts[self::GOOD] : 0;
            $bad    = isset($counts[self::BAD]) ? $counts[self::BAD] : 0;
            return $good - $bad;
        }
    }
    
    class Rating_Strategy_Binary_Rational extends Rating_Strategy_Binary {
        public function getAggregatedOpinion(Rating_Vote_Collection $votes) {
            $counts = $votes->getOpinionCounts();
            $good   = isset($counts[self::GOOD]) ? $counts[self::GOOD] : 0;
            $bad    = isset($counts[self::BAD]) ? $counts[self::BAD] : 0;
            $total  = $good + $bad;
            return ($total > 0) ? ($good / $total) : 0;
        }
    }
    
    abstract class Rating_Strategy_Range implements Rating_Strategy {
        private $min, $max;
    
        public function __construct($min, $max) {
            $this->min = $min;
            $this->max = $max;
        }
    
        public function getRatingOptions() {
            return range($this->min, $this->max);
        }
    }
    
    class Rating_Strategy_Range_Arithmetic extends Rating_Strategy_Range {
        public function getAggregatedOpinion(Rating_Vote_Collection $votes) {
            $counts = $votes->getOpinionCounts();
            $sum = 0;
            $total_count = 0;
            foreach ($counts as $value => $count) {
                $sum += $value * $count;
                $total_count += $count;
            }
            return ($total_count > 0) ? ($sum / $total_count) : 0;
        }
    }


    Осталось добавить в интерфейс Rating_Ratable метод getRatingStrategy:

    interface Rating_Ratable {
        /**
         * @return Rating_Object
         */
        public function asRatingObject();
    
        /**
         * @return Rating_Strategy
         */
        public function getRatingStrategy();
    }
    
    class Article extends ActiveRecord implements Rating_Ratable {
        public function setTableDefinition() {
            $this->hasReferenceColumn("rating_object_id");
        }
    
        public function setUp() {
            $this->hasOne("Rating_Object as rating_object", array("local" => "rating_object_id", "foreign" => "id"));
        }
    
        public function asRatingObject() {
            return $this->rating_object;
        }
    
        public function getRatingStrategy() {
            return new Rating_Strategy_Range_Arithmetic(1, 5);
        }
    }


    А в классе Rating_Object предусмотреть методы для оперирования связанными Rating_Vote.

    Теперь мы легко можем в нужных местах произвести добавление голоса или узнать текущий рейтинг:

    $article->asRatingObject()->addVote($user->asRatingSubject(), 5);
    
    echo $article->getRatingStrategy()->getAggregatedOpinion($article->asRatingObject()->getVotes());
    
    Поделиться публикацией
    Комментарии 60
      –10
      Уберите текст под хабракат плиз.
        +2
        Напишите об этом автору в личку, зачем это всем читать?
          +8
          Но ведь это же хабро-традиция.
        +2
        Какой интересный акцент — «запрографировать», «фотограммы» =)
          –6
          -графия — деятельность (полиграфия, порнография, электрокардиография, прография)
          -грамма — результат (полиграмма, порнограмма, электрокардиограмма, программа)
          -граф — инструмент (полиграф, порнограф, электрокардиограф, програф)
          -графист — деятель (полиграфист, порнографист, электрокардиографист, прографист)
            +2
            Угу.

            Но почему-то фотография (объект и деятельность), фотограф (деятель, а не инструмент), фотоаппарат (инструмент). Ну и с полиграфией (как печатным процессом) все тоже не так прямолинейно.

            Вы смешиваете слова из разных парадигм (и с разным происхождением).
              –2
              Фотография — деятельность, фотограмма (также: фотокарточка; неправ.: фотография) — результат; фотогр́аф (также: фотоаппарат) — инструмент; фотография (неправ.: фот́ограф) — деятель.

              Я не смешиваю, а навожу порядок в мешанине.
                0
                Не надо наводить порядок в том, в чем вы не разбираетесь.

                Язык — это устоявшаяся система, которая не любит насилия. Чтобы навести в нем порядок, надо начать с самого начала, устраняя «мешанину», которая возникает благодаря заимствованиям и естественной эволюции языка. По сути — надо создать искусственный язык.

                Вперед. Только естественный язык не трогайте.
                  –2
                  Это мой язык, я его использую в повседневном общении, я его носитель, я имею на него ровно такие же права, как и любой другой, и делаю с ним, что хочу. Другие могут следовать моему примеру или отвергать его.
                    –1
                    Расскажите это редактору/корректору, которые «ваш язык» безжалостно поправят и будут правы.

                    Вы можете «делать, что хотите», но только учитывайте эффект, который это производит. В частности то, что вас будут неправильно понимать.

                    Понятие языковой нормы взято, честное слово, не с неба.
                      0
                      Я не сотрудничаю с редакторами и корректорами, которые правят мой язык в тех местах, где я намеренно отклоняюсь от общепринятой практики.
                      +3
                      Это не только ваш язык, а наш общий «протокол обмена информацией».

                      Если каждый клиент будет видоизменять протокол по своему усмотрению, рандому или велению левой пятки, то начнутся серьёзные проблемы с коммуникацией.
                        –3
                        Я не заметил проблем с коммуникацией, возникающих из-за систематизированного использования слов на -граф- и -грамм- в противовес исторически сложившейся каше. Кроме разве того, что иногда кто-то интересуется причиной, и тогда я поясняю, но это не проблема с протоколом обмена информацией, а обычные человеческие любопытство и тяга к знаниям.
                +7
                Блин, думал вы чисто приколоться, а вы на полном серьёзе проповедуете.
                  0
                  Если с фотограммами всё понятно, то слово «запрографировать» я и гугл встречаем впервые. Конечно, из контекста понятно, о чём речь, но лучше пояснять это необыкновенное использование языка.
                +1
                Вот смотрите.

                Почему для добавления голоса мы приводим объект к одному интерфейсу, а для получения списка голосов — к другому?

                Ведь очевидно, что и «добавить голос» и «получить список голосов» — это методы одного и того же семантического уровня.

                При этом мы держим в голове, что addVote должен знать о стратегии — чтобы не позволить добавить голос, который не попадает под стратегию, поэтому никакое сокрытие информации тут не при чем.

                  –2
                  И добавление голоса (addVote), и получение голосов (getVotes) — это методы Rating_Object, к которому мы получаем доступ через один и тот же интерфейс Rating_Ratable. Получение итоговой оценки на основании списка голосов — это уже задача не Rating_Object, а Rating_Strategy.

                  Мы можем, например, в какой-то момент заменить Rating_Strategy_Binary_Arithmetic на Rating_Strategy_Binary_Rational, не затрагивая совершенно Rating_Object.

                  Метод addVote и вообще Rating_Object ничего не знает о стратегии; это не даёт нам возможность контролировать попадание добавляемой оценки в выбранную шкалу. Грубо говоря, Rating_Object занимается только сбором и хранением голосов.

                  На основании стратегии мы выводим для пользователя набор кнопок (форму), валидируем полученные данные — и затем вызывает методы объекта, чтобы этот голос записать.

                  На основании стратегии же мы выводим для пользователя текущий рейтинг.
                    +1
                    «это методы Rating_Object, к которому мы получаем доступ через один и тот же интерфейс Rating_Ratable. Получение итоговой оценки на основании списка голосов — это уже задача не Rating_Object, а Rating_Strategy.»
                    Это излишнее усложнение. Когда у меня есть объект рейтинга, я хочу иметь возможность узнать его рейтинг, не задумываясь о том, как он его считает. Иными словами, стратегия должна быть от меня спрятана.

                    «Метод addVote и вообще Rating_Object ничего не знает о стратегии; это не даёт нам возможность контролировать попадание добавляемой оценки в выбранную шкалу. Грубо говоря, Rating_Object занимается только сбором и хранением голосов.»
                    Вот это и есть ошибка дизайна. Зачем регистрировать некорректные голоса?

                    «На основании стратегии мы выводим для пользователя набор кнопок (форму), валидируем полученные данные — и затем вызывает методы объекта, чтобы этот голос записать.»
                    Таким образом вы легко можете получить несогласованные данные, выведя для отрисовки одну форму, а для отображения результата (спустя неделю) — другую.
                      –2
                      Предложите исправления.
                        +1
                        (1) Делаем публичным только интерфейс RatingObject, с методами добавить/получить список/получить статистику/получить рейтинг. Все внешние взаимодействия ведутся через него.

                        (2) Привязываем конкретный RatingObject к конкретной стратегии рейтингования. В этом случае описанные выше методы RatingObject начинают работать по приблизительно одному шаблону: получить чистые данные (от пользователя или из базы) — валидировать/обработать стратегией — передать дальше (в базу или пользователю).

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

                        (3) Все вышенаписанно исходит из того, что стратегия рейтингования атомарна, то есть нет смысла делить ее на стратегию сбора (бинарные голоса/голоса по шкале, например) и стратегию показа (усреднение шкалы одним типом/усреднение шкалы другим типом). Если было бы нужно, то надо стратегию, хранимую в объекте, считать стратегией сбора, а для отображения использовать _совместимую_ (это важно) стратегию показа (например, выбирая ее из ratingStrategy->getDisplayStrategies).
                          –1
                          Каким образом мы привяжем Rating_Object к Rating_Strategy?
                            +1
                            Зависит от деталей реализации и отображения на хранилище.

                            Если мы говорим о некоей абстрактной объектной системе, то я бы делал RatingObject where T: RatingStrategy.

                            Если бы я отображал это на базу, то у меня в базе в таблице RatingObject лежало бы поле RatingStrategyId, по которому бы доставалась конкретная стратегия для этого объекта. Причем это поле, очевидно, было бы immutable.

                            (собственно, первый пункт даже не противоречит второму)
                              0
                              Каждый экземпляр Rating_Object соответствует строке в базе данных; а Rating_Strategy не хранятся в базе данных вовсе. Стратегии — классы, а не экземпляры классов, их не реализуешь как active record. Что будет записано в RatingStrategyId?
                                0
                                Идентификатор, по которому мы определим, какой именно класс надо подгрузить.
                                  –1
                                  И как вы предлагаете это реализовать? Хранить имя класса? Определять к каждом классе числовую константу? Вести где-то пронумерованный реестр классов? Вы правда думаете, что это будет лучше?
                                    +1
                                    А это уже будет зависеть от конкретной реализации маппера, СУБД и фабрики стратегий. (По большому счету, мы здесь имеем дело с частным и очень специфическим случаем ORM-сценария, предполагающего хранение в одной таблице различных классов, и этот сценарий решается.)

                                    Да, я правда думаю, что это будет лучше.
                                      +1
                                      … собственно, фишка в том, что мы опустились с уровня «как это реализовать в ООП» на уровень «как отобразить конкретную модель объектов на базу». И вы пытаетесь спорить с дизайном объектной модели, аргументируя это тем, что ее неудобно отображать на базу.

                                      Фаулер пишет, что чем сложнее объектная модель, тем более вероятно, что ее придется реализовывать автономно от базы, а потом делать маппер.
                    +11
                    Это что, наглядная иллюстрация к статье «Как не надо считать факториалы»? :)
                      0
                      Та статья забавная, она заставила меня вспомнить об этой моей разработке. Если хотите, здесь я предлагаю в противовес пример разумного, хотя и неочевидного, усложнения. Мне не хотелось бы, чтобы после прочтения статьи о факториале люди бросались в крайности и начинали считать, что чем проще и прямолинейнее, тем лучше, а где появляется больше одного (двух, трёх) классов — всё плохо и нет пути.
                        +5
                        Помоему без бутылки и без указания фреймворка, для которого написан этот код, здесь не разобраться. Что находится в $this->rating_object? Где объявлены addVote и getVotes? Сколько запросов будет к базе данных, если у нас список из 20 объектов и для каждого мы вызовем $article->asRatingObject()->getVotes()?
                          0
                          В $this->rating_object находится объект класса Rating_Object, связанный по rating_object_id с данной записью. Методы addVote и getVotes определены в Rating_Object («А в классе Rating_Object предусмотреть методы для оперирования связанными Rating_Vote»). Статья предназначена для людей, знакомых с active record и способных по вышеприведённой строке немедленно представить в голове код этих двух методов.

                          Я намеренно привожу минимальное количество псевдокода и опускаю конкретную реализацию многих методов, стремясь говорить об общей архитектуре, а не деталях кодирования.
                      +3
                      А вообще, конечно, имеем наглядный пример того, что в любой книжке называется «заложились на будущее».

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

                      Это такое классическое «программист решил, что в будущем может понадобиться». МакКоннел хорошо пишет об этом: «программисты обычно плохо предсказывают будущее». Вкупе с тем, что это усложняет разработку сейчас (и усложняет понимание кода и его структуру), это очевидный bad bet.

                      (ну либо _сначала_ пишите весь набор требований, а потом будем думать, как это реализовать)

                      И самое смешное, что я пишу не просто умозрительно, а вспоминая опыт мучительного рефакторинга системы, в которой очень много где заложились на расширение, но за два года ее использования так нигде и не расширили. А вот поддерживать существующий функционал — именно из-за точек расширения — было очень тяжело.
                        0
                        Я понимаю, о чём вы говорите. Но описываемая концепция была придумана мной давно и реализована несколько раз; за это время у меня не появилось повода от неё отказаться, хотя я внёс несколько усовершенствований.
                          0
                          Проблема в том, что ваша концепция применяется в _конкретных_ случаях, детали которых вы не раскрываете.

                          А пост вы пишете о некоем обобщенном случае, и на основании этого обобщения читатель начинает думать, что _все_ голосования надо делать так (даже те, в которых в ближайшие десять лет не будет двух стратегий оценки).
                            0
                            Действительно, все оценивания можно (нужно ли, пусть читатель сам решит) делать так, и это не создат проблем. Точно так же, как можно делать сайт при помощи фреймворка, используя только два десятка из сотни включённых в него классов.
                              0
                              «это не создат проблем»
                              Вот это — ошибочно.

                              Ваш код сложнее понять, чем простое голосование без объектной модели (точнее, с вырожденной объектной моделью) и без стратегий. Его сложнее разработать, его сложнее поддерживать.

                              Разница с примером с фреймворком в том, что фреймворк нам дают готовым. Впрочем, даже фреймворки я предпочитаю выбирать, в частности, по тому, сколько дополнительного кода надо написать для того, чтобы заработал интересующий меня _простой_ функционал.

                              Например, если мне нужен логгер, я не хочу инициализировать десяток других компонентов — DI, exception handling strategy, авторизацию, DAL и так далее. Логгер должен запускаться (в самом простом сценарии) двумя строчками в коде, где первая — его инициализация, а вторая — запись. Все остальное должно быть от меня спрятано.
                                –2
                                Не следует использовать что-то, не поняв его сперва.
                                  0
                                  Не стоит писать вещи, которые требуют чрезмерных усилий для понимания. Код, который легко поддерживать, должен быть в первую очередь легко читаем и прозрачен.
                                    –1
                                    Всякий код сложен в той или иной степени, требует той или иной подготовки для чтения и понимания.
                                      +1
                                      Именно поэтому я и написал «чрезмерных» усилий.
                        +1
                        Как замутно, а чем плохо иметь свойство raiting у объекта. И иметь где-нибудь функцию
                        countRaiting(obj):int?
                          0
                          Что будет делать эта функция?

                          Вы говорите о случае, когда у Article есть rating: integer и метода increaseRating, decreaseRating, getRating?
                          +3
                          Чрезмерно сложное «идеальное» решение. Я считаю, что код по большей части должен писаться под конкретные задачи, а у вас тут больше половины «на всякий случай».
                            –1
                            Давайте рассмотрим конкретную, реальную практическую задачу.

                            У нас есть сайт наподобие bash.org.ru — публикуются цитаты, и посетители голосуют: хорошая или плохая. Регистрации пользователей нет.

                            В таблице есть поле rating: integer, при голосовании оно увеличивается или уменьшается на единицу.

                            В один прекрасный день мы наконец осознаём всю порочность такого способа подсчёта рейтинга и хотим сделать рейтинг как pos / (pos + neg).
                              +1
                              а если в итоге, осознав порочность и нового метода, мы решим делать рейтинг на основе более сложной формулы? Например, когда каждый голос с увеличением его возраста на день увеличивает вес на 1%? Я думаю и эту возможность нужно заложить изначально.

                              Не хочу повторяться, но не усложняйте ваш код. Пишите ровно столько кода, сколько необходимо сейчас, а не через год, два или пять. И пребудет с вами сила.
                                –1
                                Вовсе не обязательно эту возможность закладывать изначально. Мы вполне можем в тот момент, когда решим её реализовать:
                                1) добавить поле timestamp к Rating_Vote и заполнить его для существующих голосов текущим моментом (а почему нет?);
                                2) в методе addVote добавить заполнение поля timestamp;
                                3) написать стратегию, учитывающую timestamp при подсчёте.

                                Пишите ровно столько кода, сколько позволит вам сделать что угодно и сейчас, и через год, и через пять. Повторю: не предусматривайте все функциональные возможности, но предусматривайте возможность добавления любых новых возможностей.
                                  0
                                  Да хватит уже отвечать короткими набросками реализаций на конструктивные замечания и советы. То что у подхода существует реализация не доказывает что подход удобен, верен и оптимален.
                                  Веб-разработка — это динамичный процесс с высокими требованиями к скорости внедрения. Надо выпускать работающий продукт с разумным «запасом прочности» в кротчайшие сроки, а не продумывать все возможные «заначки на чёрный день», так как это грозит прогрессирующим нарастанием бесполезного кода, а при плохом раскладе и полным крахом проекта.
                                  За 5 лет слишком многое меняется, чтобы прогнозировать функционал «про запас»… Если бы все разработчики работали по таким принципам — мы бы все до сих пор сидели в громоздкой консоли с миллиардом команд, но огромным запасом потенциально необходимых функций.
                            0
                            Полтора года назад уже популярно высказался по поводу этой схемы. Сейчас же добавлю только то, что нужно быть проще и не предусматривать заранее все возможные варианты использования…
                              +4
                              После прочтения топика KISS вместе с YAGNI покончили с собой.
                                0
                                а велосипедные фабрики решили выпускать тостеры.
                                +2
                                Вот поэтоу мне стыдно признаваться на собеседовании, что знаю ООП и паттерны, сразу приходиться оправдываться что я не совсем болен оопизмом головного мозга.

                                ужасное желание хранить каждый голос как запись в отдельной таблице, когда можно обойтись парой полей непосредственно айтема. а ещё получается, что перед апдейтом вы делаете ненужный толстый селект и можете внести ошибку при одновременных голосованиях, не говоря уже о дополнительной нагрузке.
                                  0
                                  А как вы предлагаете проконтролировать, чтобы один субъект мог голосовать за один объект только один раз? (Пускай даже менять в будущем голос можно, но нельзя два раза поставить плюсик статье.)
                                    0
                                    в том то и дело, что нигде не увидел чтобы вы решали данную проблему.
                                      0
                                      Rating_Vote: object_id, subject_id. На уровне таблицы БД по этой паре — unique key. Метод addVote, естественно, проверяет, есть ли уже такой голос, или же обходится средствами SQL вроде INSERT IGNORE или REPLACE.
                                  +2
                                  Я кое-что знаю о рейтингах и голосованиях, и могу с уверенностью сказать, что вы чрезмерно усложнили довольно простую задачу
                                    0
                                    Просим-просим! Не в смысле «а слабо?», а в практическом — я как раз разрабатываю свой велосипед в этом направлении, было бы полезно познакомиться с практическим опытом людей, уже преуспевших на этой стезе
                                      0
                                      Да, конечно, чем смогу помогу. Если есть сформировавшиеся вопросы — спрашивайте
                                        +1
                                        Мм, ну для начала хотелось бы увидеть ваш, более простой вариант построения задачи, по сравнению с топикстартером — что лишнее, на что обратить внимание, подводные камни тсзать…
                                    0
                                    Как вы решили проблему вхождение новичка? Вес голоса человека накопившего себе карму равен голосу только что зарегистрировавшегося?
                                      0
                                      Я не занимался реализацией взвешенных голосов, но ничто не мешает это сделать. Как именно — вопрос задачи, а не реализации.

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

                                      Самое читаемое