Как избежать болтливых интерфейсов

10

Справочная информация: я проектирую серверное приложение и создаю отдельные библиотеки DLL для разных подсистем. Для упрощения скажем у меня есть две подсистемы: 1) Users2)Projects

Публичный интерфейс пользователя имеет такой метод:

IEnumerable<User> GetUser(int id);

И открытый интерфейс Projects имеет такой метод:

IEnumerable<User> GetProjectUsers(int projectId);

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

Проблема: в идеале, Projectsподсистема не должна также хранить информацию о пользователях и должна просто хранить идентификаторы пользователей, участвующих в проекте. Для того , чтобы служить GetProjectUsers, он должен позвонить GetUserв Usersсистеме для каждого идентификатора пользователя , хранящегося в собственной базе данных. Тем не менее, это требует много отдельных GetUserвызовов, вызывая много отдельных запросов SQL внутри Userподсистемы. Я на самом деле не проверял это, но этот болтливый дизайн повлияет на масштабируемость системы.

Если бы я оставил в стороне разделение подсистем, я мог бы хранить всю информацию в одной схеме, доступной для обеих систем, и Projectsмог просто сделать так, JOINчтобы все пользователи проекта были в одном запросе. ProjectsТакже необходимо знать, как генерировать Userобъекты из результатов запроса. Но это нарушает разделение, которое имеет много преимуществ.

Вопрос: Может ли кто-нибудь предложить способ сохранить разделение, избегая при этом всех этих отдельных GetUserвызовов во время GetProjectUsers?


Например, одна из моих идей заключалась в том, чтобы пользователи могли предоставлять внешним системам возможность «помечать» пользователей парой метка-значение и запрашивать пользователей с определенным значением, например:

void AddUserTag(int userId, string tag, string value);
IEnumerable<User> GetUsersByTag(string tag, string value);

Затем система Projects может пометить каждого пользователя по мере его добавления в проект:

AddUserTag(userId,"project id", myProjectId.ToString());

и во время GetProjectUsers он может запросить всех пользователей проекта за один вызов:

var projectUsers = usersService.GetUsersByTag("project id", myProjectId.ToString());

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

Эрен Эрсонмез
источник

Ответы:

10

Чего не хватает в вашей системе, так это кеша.

Ты говоришь:

Тем не менее, это требует много отдельных GetUserвызовов, вызывая много отдельных запросов SQL внутри Userподсистемы.

Количество вызовов метода не должно совпадать с количеством запросов SQL. Вы получаете информацию о пользователе один раз, зачем вам снова запрашивать ту же информацию, если она не изменилась? Очень вероятно, что вы можете даже кэшировать всех пользователей в памяти, что приведет к нулевым запросам SQL (если пользователь не изменится).

С другой стороны, когда Projectsподсистема запрашивает и проекты, и пользователей с помощью INNER JOIN, вы вводите дополнительную проблему: вы запрашиваете один и тот же фрагмент информации в двух разных местах в вашем коде, что чрезвычайно затрудняет аннулирование кэша. Как следствие:

  • Либо вы не будете вводить кеш в любое время позже,

  • Или вы потратите недели или месяцы на изучение того, что должно быть признано недействительным при изменении части информации,

  • Или вы добавите кеширование недействительным в простых местах, забывая другие и приводя к трудностям в обнаружении ошибок.


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

  1. Спросите себя, работает ли система медленно (т.е. она нарушает нефункциональное требование производительности или просто кошмар в использовании).

    Если система не медленная, не беспокойтесь о производительности. Беспокойство о чистом коде, удобочитаемости, удобстве сопровождения, тестировании, освещении веток, чистом дизайне, подробной и легкой для понимания документации, хороших комментариях к коду

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

  3. Как только узкое место найдено, ищите решения. Вы делаете это путем гадания, тестирования, профилирования, написания альтернатив, понимания оптимизации компилятора, понимания оптимизации, которая зависит от вас, задавания вопросов о переполнении стека и перехода на языки низкого уровня (включая Assembler, когда это необходимо).

Какова реальная проблема с Projectsподсистемой, запрашивающей информацию для Usersподсистемы?

Возможная проблема масштабируемости в будущем? Это не проблема. Масштабируемость может стать кошмаром, если вы начнете объединять все в одно монолитное решение или запрашивать одни и те же данные из разных мест (как объяснено ниже, из-за сложности введения кэша).

Если уже есть заметная проблема с производительностью, то на шаге 2 найдите узкое место.

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

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

Арсений Мурзенко
источник
Если я вас неправильно понимаю, вы говорите: «Держите отдельные вызовы GetUser, но используйте кэширование, чтобы избежать обходов БД».
Эрен Эрсонмез
@ ErenErsönmez: GetUserвместо запросов к базе данных, будет выглядеть в кеше. Это означает, что на самом деле не имеет значения, сколько раз вы будете звонить GetUser, поскольку он будет загружать данные из памяти, а не из базы данных (если только кэш не был аннулирован).
Арсений Мурзенко
это хорошее предложение, учитывая, что я не сделал хорошую работу, выдвинув на первый план основную проблему, которая заключается в том, чтобы «избавиться от болтливости, не объединяя системы в одну систему». Мой пример «Пользователи и проекты», естественно, привел бы вас к мысли, что существует относительно небольшое количество редко меняющихся пользователей. Возможно, лучшим примером были бы Документы и Проекты. Представьте, что у вас есть пара миллионов документов, тысячи добавляются каждый день, и система Project использует систему Document для хранения своих документов. Вы все еще порекомендуете кеширование? Наверное, нет, верно?
Эрен Эрсонмез
@ ErenErsönmez: чем больше у вас данных, тем критичнее кэширование. Как правило, сравнивайте количество чтений с количеством записей. Если в день добавляются «тысячи» документов и миллионы selectзапросов в день, лучше использовать кэширование. С другой стороны, если вы добавляете миллиарды объектов в базу данных, но получаете только несколько тысяч selectс очень избирательными where, кэширование может быть не таким уж полезным.
Арсений Мурзенко
Вы, вероятно, правы - я, вероятно, пытаюсь решить проблему, которой у меня еще нет. Я, вероятно, буду реализовывать как есть и постараюсь улучшить позже, если это необходимо. Если кэширование не подходит, потому что, например, сущности, вероятно, будут прочитаны только 1-2 раза после добавления, как вы думаете, может ли сработать возможное решение, которое я добавил к вопросу? Вы видите огромную проблему с этим?
Эрен Эрсонмез