В последняя статья Я рассказал, как мы пришли к DDD и о его очень важной особенности — едином языке, на котором проще и дешевле общаться с бизнесом.
Мы также рассмотрели разработку на основе моделей.
Когда вначале имеется не фича, сделанная по требованиям, а абстрактная модель, созданная по требованиям и имеющая отражения в различных видах.
Все эти области работают на одном языке и реализованы максимально схоже, чтобы каждый, кто будет работать с проектом, мог понять любую из них.
Сегодня мы поговорим о том, как приручить непосредственно исходные коды программ, как они представлены архитектурно.
Расскажу об идеях, которые мы используем для построения прозрачной и понятной модели, чтобы ее можно было легко разрабатывать вместе с заказчиком.
Эти подходы относятся к архитектуре, хранению исходного кода и вопросам разработки в целом.
Я также расскажу вам о практических трудностях.
Формат статьи не позволяет включить огромное количество случаев, поэтому приведу только два примера.
DDD и микросервисы: взаимодополняющие подходы
Самое неприятное в микросервисной архитектуре — это когда вы понимаете, что разделили микросервисы неправильно и нужно разделить их еще раз.Это очень дорого и очень больно.
DDD позволяет предотвратить это.
Таким образом, микросервисная архитектура и DDD дополняют друг друга.
Потому что, с одной стороны, есть идея высшего уровня, о которой сейчас много говорят в индустрии.
Что если вы начнете использовать предметно-ориентированное проектирование и действительно задумаетесь о разделении на ограниченные контексты с правильным формированием агрегатов и прорисовкой границ между контекстами и агрегатами, то вам будет легко разбить систему на микросервисы.
С другой стороны, если вы хотите создавать микросервисы, вы обязательно услышите о доменно-ориентированном дизайне.
Потому что это один из лучших способов разделить систему на микросервисы.
Это вертикальное деление, дальнейшее фрагментирование и деление на признаки, которыми, в том числе, мы пользуемся сами.
Можно сказать, что DDD открывает возможность качественного разделения на микросервисы.
Я расскажу о DDD применительно к микросервисам.
Потому что о монолитах сказано много, а о микросервисах материала меньше.
Именно из-за популярности микросервисной архитектуры DDD переживает такой рост. Фактически, без терминов DDD сейчас невозможно объяснить, как разделить код на вертикальные сервисы.
Мы сами используем микросервисную архитектуру, в том числе и на вновь запускаемых проектах.
Мы можем себе это позволить, потому что мы отработали инфраструктуру, накопили предметные знания, определили контексты и агрегаты.
Совместное использование кода
Когда мы начали работать над нашим приложением, у нас был один большой репозиторий.В нем хранились коды, связанные с пользовательским интерфейсом приложения, бизнес-логикой и уровнем обслуживания, а также функциональностью, связанной с уровнем инфраструктуры.
Возможно, нам в чем-то повезло, потому что наша инфраструктура, благодаря коллегам, которые были до нас, уже была совершенно независима от бизнес-кода.
Однако работа с одним большим монолитным репозиторием помешала нам осуществить переход от монолита к микросервисам.
Была очень высокая вероятность того, что вновь созданный микросервис быстро привязывается общей бизнес-логикой к монолиту и утрачивает функцию отдельного независимого приложения.
Поэтому мы хотели провести некоторые границы и готовились разделить некоторые части решения на отдельные репозитории.
А потом у нас появилось отдельное приложение от внешнего вендора, на которое мы решили сопоставить нашу инфраструктуру.
Но потом это продолжалось и продолжалось.
В результате получилась такая ситуация:
- Исходные коды новых микросервисов стали выносить в отдельные репозитории,
- Старый репозиторий содержал основные исходные коды приложения,
- Во избежание копирования инфраструктуру разобрали в пакеты и поместили на наше хранилище в Артефабрике внутри банка.
И на наш взгляд, это правильно – у них несколько разные цели и задачи.
Я опишу, какие подходы мы используем в разных частях решения.
БЛ (бизнес-логика)
Именно здесь тактические шаблоны DDD являются для нас основным местом их использования, потому что мы не хотим использовать их везде, а только там, где действительно необходима максимальная гибкость.Мы уделяем особое внимание таким качествам, как модифицируемость или гибкость, чтобы поддерживать историю постоянных новых требований, которые мы вынуждены вводить в наш продукт. Код бизнес-логики представляет собой модель предметной области и уровень приложения сценариев использования.
При написании бизнес-логики мы избегаем наследования, стараясь максимально заменить его композицией.
Мы стараемся избегать любых подходов, которые вносят дополнительную жесткость (шаблоны проектирования GoF, наследование, автоматически генерируемый код).
Мы разбиваем уровень приложения на небольшие функции.
Это позволяет работать с функциями самостоятельно: заказчик захотел фичу — включили в поставку, не захотел — не включили.
Любой компонент очень легко удалить без изменения исходного кода.
В целом это выглядит так, как будто подход с микросервисами переносится непосредственно на уровень внутри сервиса: есть вертикальное деление по бизнес-логике на уровне микросервиса и есть вертикальное деление внутри микросервисов.
Это характерно для многих отраслей и областей знаний, когда подходы с высокого уровня передаются на более низкий.
ИЛ (инфраструктура)
Здесь у нас все наоборот — разработка DDD не затрагивает вопросы инфраструктуры, мы хотим сделать этот слой более жестким, так как причин его менять не так уж и много.Логика инфраструктуры позволяет программам запускаться и выполнять свои обязанности.
Поэтому фокус инфраструктурной части смещается от модифицируемости к производительности, возможности повторного использования и безопасности.
Инфраструктура как бы защищает гибкую, красивую модель домена от внешнего мира.
А поскольку он должен быть максимально многоразовым, мы используем в нем все лучшие практики проектирования, придуманные отраслью.
Это паттерны Gang of Four (GoF), SOLID, DRY, тонкое разделение классов, различные принципы типа dom3.
Микросервисная архитектура
Нет ничего удивительного в том, как выглядит наш микросервис, написанный с использованием DDD. Мы используем луковую архитектуру, стараясь следовать доменно-ориентированному принципу, когда центром программы является не уровень доступа к данным, а модель предметной области:Луковая микросервисная архитектура.
Внутренние уровни относятся к бизнес-уровню, внешний уровень — к инфраструктуре.
То есть, как я уже говорил, ситуация внутри нашего микросервиса чем-то похожа на то, что происходит на уровне инфраструктуры, когда вы делаете микросервис:
Логика приложения разбита на отдельные небольшие обработчики, и для перенаправления к ним команд и запросов мы используем медиатор.
Все эти обработчики, описывающие варианты использования, полагаются на одну и ту же модель предметной области, чтобы иметь возможность полностью поддерживать согласованное состояние и инварианты в модели предметной области.
Инфраструктура имеет сервисную функцию.
Он связывает модель предметной области как основную ценность с различными компонентами инфраструктуры.
Общий код
У нас есть микросервисы в разных репозиториях, и нам очень хотелось сделать их похожими друг на друга.Вполне логично, что, поскольку появились пакеты, связанные с инфраструктурой, а мы переиспользуем инфраструктуру от одного сервиса к другому, у нас возникла идея перенести общий код дальше.
Этот общий код охватывает все уровни нашего приложения.
У нас есть сборки в инфраструктуре, которые помогают нам работать на уровне домена.
Есть сборки, работающие на уровне обработчиков приложений или других сервисных вариантов.
И конечно — сборки, напрямую тиражирующие инфраструктуру.
В результате мы сначала создали специальную платформу DDD для быстрого запуска новых микросервисов внутри организации, а затем выпустили ее с открытым исходным кодом.
Фактически у нас получилось микросервисное шасси ВенаNET :
Я думаю, если вы захотите написать микросервисное приложение с точки зрения DDD, у вас может получиться нечто очень похожее.
Основной язык, на котором мы разрабатываем — C#, работаем в инфраструктуре .
net. К сожалению, у нас нет такого инструмента, как, например, в мире Java, который позволил бы нам не думать о том, какой фреймворк мы будем использовать для запуска наших микросервисов.
Поэтому такие микросервисные шасси часто можно найти на GitHub. Они не очень популярны, но мы решили расширить список этих библиотек, опубликовав нашу версию.
Слой домена
При работе со слоем предметной области у нас есть некоторый код, который поддерживает работу с событиями предметной области — сущностями, спецификациями и проверкой.Мы создали отдельную среду проверки на основе одной из популярных платформ, существующих в мире .
net.
Сущность
Пример кода сущности:Код представляет собой сущность, которая содержит:public class Card : IEntity<int>, IEventProvider { private IEventCollector _eventCollector; .
public virtual void AddBlock([NotNull] BlockDetails blockDetails) { .
_eventCollector.CollectEvent(new CardBlocked(Id, blockDetails)); } public virtual void SetCollector(IEventCollector sender) { _eventCollector = sender; } }
- Интерфейсы, определяющие его как сущность;
- Интерфейсы, указывающие, что этот объект может обрабатывать события;
- Дополнительные методы, позволяющие инфраструктуре наделить сущность инструментами для работы с событиями инфраструктуры.
Прикладной уровень
Здесь мы в основном фокусируемся на интерфейсах, которые позволяют нам работать с различными аспектами уровня инфраструктуры: шиной сообщений, посредником, интерфейсом обмена сообщениями и вещами, связанными с кэшированием.
Типичный пример того, как выглядит обработчик на уровне приложения: Спойлер public async Task<BlockResult> HandleAsync(BlockCardRequest dataRequest,
CancellationToken token)
{
using (var uow = _entityFactoryService.Create())
{
var card = await _entityFactoryService.Create<Card>()
.
GetAsync(dataRequest.Id, token);
var result = await card.BlockCardAsync(request.Reason, token);
await uow.CommitAsync(token);
return result;
}
}
Он содержит единицу работы для поддержки транзакций и работает напрямую с объектами уровня домена.
В этом примере, например, карта заблокирована.
Уровень инфраструктуры
Здесь у нас основная реализация, основной контент библиотек и различный функционал для работы с внешней инфраструктурой.В том числе реализация интерфейса, который используется на уровне приложения.
Если вам нужна поддержка DDD в вашем проекте, а этот проект состоит из большего количества сервисов, чем один, то, думаю, у вас возникнет аналогичная идея — повторить инфраструктуру, разделить ее и попытаться заставить эту инфраструктуру подтолкнуть пользователя к программированию, к проектированию.
конкретно в рамках тактических паттернов DDD. Таким образом, сервисы можно будет писать быстрее и сделать их единообразными.
Более того, если этот фреймворк будет выпущен хотя бы как Inner Source, не говоря уже об Open Source, то сервисы будут едины на уровне разных команд. Инфраструктура становится единой, и непосредственно на этой основе можно построить внутреннее сообщество.
Например, у нас есть много вопросов от внутреннего сообщества разработчиков, связанных с тем, как мы доработаем этот фреймворк и как он будет выглядеть.
Все, кто делает микросервисы и использует фреймворк, тяготеют к использованию DDD. И все они хотят избежать необходимости заново писать всю инфраструктуру.
Теперь давайте посмотрим, какие трудности могут вас ожидать.
Сложность: связь между контекстами
Первая трудность — какое соединение между контекстами выбрать при использовании микросервисов.При этом, если вы посмотрите на приведенную выше архитектуру предприятия, окажется, что взаимосвязь между контекстами имеет смысл.
И так же, как имеет смысл между микросервисами внутри одного приложения, это имеет смысл, когда есть несколько команд, несколько приложений — уже на уровне предприятия.
Можно сказать, что DDD хорош тем, что очень хорошо реагирует на такие новые вызовы.
Реализация общего функционала
Эта задача может быть для двух сервисов (например, внутри вашего проекта) или для двух команд внутри вашего предприятия.Стандартный подход к интеграции, который предлагает DDD, — это подход с общим ядром, когда вы хотите поддерживать некоторую часть кода вместе.
Наиболее типичный подход к этому — разработка универсального микросервиса.
Именно в микросервисной архитектуре это будет выглядеть наиболее логично и позволит в будущем вносить в нее последовательные изменения и предоставлять функционал через API:
Решение: общее ядро.
Общего обслуживания Более классический подход — создать отдельную библиотеку и использовать ее в двух разных приложениях или в двух разных микросервисах:
Решение: общее ядро.
Общий пакет nuget У нас был опыт такого использования, и он был крайне неудачным, когда дело касалось бизнес-логики.
Если вы добавляете сущности, агрегаты и даже объекты значений с каким-либо поведением, то считайте, что вы находитесь на хорошем и стабильном пути к распределенному монолиту.
Поэтому я бы не советовал этого делать.
Если вам действительно нужно реализовать какой-то общий функционал, то делайте это в микросервисе.
Но если вам нужно реализовать что-то, не имеющее поведения — например, стандартные типы внутри приложения, микротипы, в которых есть небольшая валидация, но это явно не претендует на отдельный сервис — то вы легко можете создать разделяемую библиотеку.
.
В отличие от серьезной большой библиотеки с бизнес-логикой, это не доставит вам головной боли.
В целом Shared Kernel в микросервисной среде в его классическом виде — это, конечно, использование отдельного сервиса, который, например, поддерживают две команды вместе.
Получить доступ к функционалу другой команды
Это еще одна задача, с которой разработчик также сталкивается очень часто.
DDD предлагает использовать один из восходящих/нисходящих подходов:
Решение: восходящий/нисходящий поток.
Внешний сервис Конечно, когда у вас есть заказчик-поставщик или конформист, вы можете использовать любой из этих подходов (или вам навяжут один из них).
Но обычно такая интеграция между двумя командами внутри микросервисной среды означает, что приходится думать о версионировании внутреннего API, что может оказаться довольно неприятной идеей.
Поэтому чаще при интеграции систем встречается более распространенный подход, использующий некие слои МЕЖДУ.
Это уже соответствует шаблону уровня борьбы с коррупцией, и распространенным вариантом такого уровня является корпоративная шина данных:
Решение: Антикоррупционный уровень.
Посредник Думаю, коллеги, которые работают в крупных компаниях, об этом знают. Такие корпоративные шины данных есть у многих людей, и они содержат канонические модели и преобразователи.
Все это позволяет отвязать ваше приложение, но, конечно, посредники берут за это определенную цену.
Другой распространенный шаблон — не использовать одну шину данных или один проект, а просто сделать адаптер внутри каждой системы:
Решение: Антикоррупционный уровень.
Адаптеры В среде микросервисов эти адаптеры часто представлены одними и теми же микросервисами.
Их API уже версионирован, и они защищают систему от взаимодействия с внешним миром.
Мы тоже активно используем этот подход. Это оказывается эффективным, хотя, конечно, есть накладные расходы на написание дополнительного кода.
Доступ к данным другой команды
Это третья задача, которая возникает при интеграции микросервисов или приложений при взаимодействии разных команд. Здесь вам уже не нужна бизнес-логика и функционал, а просто нужно сделать так, чтобы данные из какого-то источника попадали к вам и вы могли их использовать.Самый распространенный подход — тот же Раздельные способы, когда две команды, два микросервиса начинают жить полностью параллельно и вообще не пересекаются ни в плане зависимостей от других сервисов, ни в плане общих библиотек.
Они просто получают подписку, например, на какую-то тему, добавляют данные себе и живут самостоятельной жизнью:
Решение: Разные пути.
Подписка на данные Этот подход не очень распространен внутри микросервисного приложения, но на уровне архитектуры предприятия, наоборот, это очень распространенная практика, и она позволяет командам по-настоящему развиваться самостоятельно.
Если посмотреть на взаимодействие контекстов, то DDD предлагает для этого четкие шаблоны, а в микросервисной среде также есть несколько стандартных вариантов проектирования, которые я описал здесь.
Если у вашей компании есть другие варианты, было бы интересно их обсудить.
Сложность: логика между слоями
Эта проблема заключается в том, как разделить логику между уровнями, и касается организации сервиса внутри.Доменно-ориентированное проектирование здесь предоставляет набор тактических паттернов, которые позволяют написать красивый, гибкий, модифицируемый сервис — и в то же время получить максимальную возможность для его развития в будущем.
Логическое распределение
Вопрос, который часто волнует людей, непосредственно участвующих в разработке: какую логику заложить в сервис приложения, а какую логику заложить в сервис домена или вообще на уровень домена? DDD рассказывает нам об этом в довольно абстрактных вещах высокого уровня.Например, поместите вариант использования в сервис приложения, а код, связанный с принятием решений, важных для вашего поддомена, вашего ограниченного контекста, — в сервис домена.
Что это значит, каждый решает по-своему.
Расскажу, какие подходы мы разработали.
Сначала мы, конечно, так не делали.
Это свод правил, возникший методом проб и ошибок.
Меня очень порадовало, что это практически полностью совпало с рекомендациями Microsoft в 2018 году по созданию микросервисов.
Наши разработчики если нет принятия решения, кладут код в сервис приложения .
Пример кода, в котором нет принятия решения: Спойлер public BlockResult BlockCard(int cardId, BlockDto blockDto)
{
if (cardId <= 0)
return new BlockResult("Card id must be greater than zero");
if (blockDto == null);
return new BlockResult("Block details must not be null");
var repository = _entityRepositoryFactory.Create<Card>();
var card = repository.Get(cardId);
if (card == null)
return new BlockResult("Card does not exist");
_accountsService.LockAccount(card);
card.BlockCard(blockDto.ToDetails());
.
}
Мы просто выполняем небольшую проверку зависимостей внешнего входящего метода, извлекаем объект из репозитория и вызываем метод службы домена и метод объекта.
Мы никоим образом не зависим от того, что эти методы, например, нужно вернуть.
Таким образом, можно сказать, что здесь не происходит принятия деловых решений.
Если есть принятие решения, то разработчики помещают этот код в доменный слой , то есть делают отдельный доменный сервис: Спойлер public class BlockingService : IBlockingService
{
.
public BlockResult BlockCard(Card card, BlockDetails blockDetails)
{
var result = _accountsService.LockAccount(card);
if(result.HasErrors())
{
return new BlockResult(result.GetErrorMessage());
}
card.BlockCard(blockDetails);
}
}
В отличие от предыдущего примера, здесь блокируется счет, и от его результата зависит, будет ли карта заблокирована в дальнейшем.
Здесь уже используется бизнес-логика, поэтому мне бы хотелось, чтобы она не попадала в сервис приложений, потому что, как минимум, ее можно повторно использовать от варианта использования к варианту использования.
Третий вариант, когда код настолько тесно привязан к инфраструктуре, что скрыть техническую сторону просто невозможно - тоже встречается довольно часто.
Тогда как в рамках доменной модели мы вообще не хотим думать об инфраструктуре.
И вообще желательно, чтобы пользователь, читая код предметной модели, не до конца понимал, что происходит на уровне инфраструктуры.
Типичный пример, который можно привести в этом случае: Спойлер using(var session = _externalCardSessionFactory.CreateSession(card))
{
var context = _cardContextFactory.Create(card);
card.DoSomeStaff(context);
if (card.HasSomeConditions())
{
var batchResult = session.Commit(card);
if(batchResult.HasErrors())
{
card.DoErrorStaff(batchResult);
return;
}
card.DoFinalStaff(batchResult);
}
}
Это ситуация, когда у вас есть объект типа сеанса и он требует выполнения последовательных действий в зависимости от того, какие изменения происходят в бизнес-логике.
То есть, по сути, вы поддерживаете внешнюю систему, синхронизированную со своей системой по определенному бизнес-шаблону.
Поскольку скрыть этот сеанс очень сложно, я бы разместил этот код на уровне приложения.
Хоть здесь и происходит принятие решений на основе состояния сущностей, этот код очень редко используется повторно — поэтому, скорее всего, он будет только в одном месте вашей программы.
Более того, вы можете скрыть инфраструктуру за отдельным интерфейсом, реализовав принцип инверсии зависимостей, о котором говорил Роберт Мартин: Спойлер public class BlockingService : IBlockingService
{
.
public BlockResult BlockCard(Card card, BlockDetails blockDetails)
{
var result = _accountsService.LockAccount(card);
if(result.HasErrors())
{
return new BlockResult(result);
}
card.BlockCard(blockDetails);
}
}
Тогда вы легко сможете реализовать взаимодействие с инфраструктурой, например в доменном сервисе, закрыв таким образом блок аккаунтов за интерфейсом.
Или это может быть поход в отдельный сервис или в очередь, в зависимости от технической реализации.
Техническая оптимизация
Мы также включаем техническую оптимизацию в сервисы приложений.DDD часто критикуют за то, что он очень медленный.
Но это не будет медленно, если вы, например, получите все данные до того, как они попадут в код домена.
Конечно, если к таблице сделать подряд сотню одинаковых запросов, она будет работать медленнее.
Но если перед выполнением операции выделить все записи и потом просто работать с ними, скорость будет вполне приемлемой.
При переходе на DDD мы не заметили какого-либо глобального снижения производительности.
Наоборот, мы победили.
Потому что мы реализовали в решении такой замечательный паттерн, как периодический пошаговый поиск сокровищ: обращения к базе данных шли один за другим вместо того, чтобы совершать, например, объединение.
Любые транзакции
В нашем случае за транзакционную активность на уровне одного сервиса отвечает единица работы.
Поэтому единица работы размещается на прикладном уровне: Спойлер using (var collector = _eventCollectorFactory.BeginCollection())
{
using (var uow = _entityRepositoryFactory.Create())
{
var parser = _factory.Create(file);
var parseResult = parser.Parse();
uow.Commit();
eventCollector.Send();
}
}
Проверка на основе объектов предметной области и внешних данных
Понятно, что вы будете проверять команду, скорее всего, на уровне приложения.Но часто бывает, что нужно валидировать не просто команду, а непосредственное изменение предметной области вместе с данными команды.
И в то же время во время этой проверки вы также можете влиять на данные из внешних систем.
У нас такие случаи происходят регулярно.
В этом случае лучше использовать доменный слой, предварительно подготовив для него данные, чтобы он не работал с тамошней инфраструктурой, а валидация происходила в одном месте.
Такая валидация зачастую очень важна для бизнеса, поскольку она также влияет на данные текущего состояния, текущего инварианта — поэтому лучше поместить ее в модель предметной области.
Общая схема решения трудностей следующая:
У нас такие правила, у нас такие проблемы.
Я думаю, что ваши подходы разные, и было бы интересно это обсудить.
Краткое содержание
- DDD позволяет определить разделение системы на микросервисы, и в этом контексте об этом стали говорить постоянно.
Это дало дополнительный стимул людям к его изучению.
В монолитных системах, на мой взгляд, доменно-ориентированное проектирование не было столь популярно.
- Тактические паттерны подходят для использования в бизнес-логике и позволяют добиться там максимальной продуктивности.
В инфраструктуре они избыточны, потому что там не нужна высокая гибкость, нет быстро меняющихся требований.
- Полезно разделить зависимости инфраструктуры для повторного использования.
Даже если вы сейчас работаете над монолитным проектом, вполне вероятно, что в какой-то момент его жизненного цикла вам захочется разделить некоторые его части на отдельные сервисы.
А для повторного использования понадобится собственная инфраструктура.
Если вы поделитесь им со своим предприятием, то сможете получить неожиданные бонусы в виде создания, например, сообщества разработчиков определенной технологической платформы.
- DDD предлагает подходы для взаимодействия как внутри приложения, так и с внешними системами.
У нас есть стратегические вопросы, например, вопрос о том, как взаимодействуют контексты, и DDD дает на них ответы.
Но эти подходы можно перенести и на уровень внутри сервиса.
Гибкость с точки зрения бизнес-логики интересна и на уровне не внутри сервиса, а снаружи.
- DDD — разработанная методология; имеется большое количество инструментов для решения любых проблем, возникающих в процессе разработки.
Существует множество книг, описывающих, как правильно использовать DDD, и большое количество пояснений для самых разных случаев, которые могут возникнуть при использовании методологии DDD.
Тогда для вас процесс пройдет гораздо менее болезненно, чем это было для нас — без переписывания кода и посыпания головы пеплом.
Видео моего выступления на TechLead Conf 2020:
Совместная конференция пройдет 13 и 14 июня.Теги: #Управление разработкой #Анализ и проектирование систем #ИТ-стандарты #Архитектура приложений #ddd #Проектирование, ориентированное на предметную область #Один язык #Язык, управляемый предметной областью #Бизнес-логика #Проектирование высокого уровня #Проектирование системы #проектирование и рефакторинг #проектирование программного обеспеченияDevOpsConf 2022 И Конференция ТехЛид 2022 .
Местом проведения станет кампус «Сколково», самый инновационный и технологичный объект Москвы.
Мы обсудим инженерные процессы в ИТ от XP до DevOps и далее, необходимо изменить инструменты и методы.
-
Анимации В Мире Состояний
19 Oct, 24 -
Тернистый Путь Blackberry Os 10
19 Oct, 24 -
Как? Где? Какие? Искать Адвокатов
19 Oct, 24 -
Горизонт Windows 7 M3 B6801
19 Oct, 24