TLDR;
// Как декорировать Connection?
interface Transaction {
begin()
commit()
rollback()
}
interface Connection {
transaction()
query(queryString)
}
Проектирую интерфейс подключения к БД (далее Connection).
Хочется добавить в него
логирование и
ленивое подключение(реальное подключение только после первого запроса).
Не хочется - пихать всю это логику в сам Connection, ибо нарушается принцип единственной ответственности и логирование становиться менее удобным (см ниже).
Изначально имелось следующее (здесь и далее псевдокод):
interface Connection {
query(queryString)
}
Новое поведение легко добавляется декораторами:
LoggedConnection implements Connection {
LoggedConnection(Connection connection) {...}
query(queryString) {
this.log.append(queryString);
return this.connection.query(queryString);
}
}
LazyConnection implements Connection {
query(queryString) {
if (!connected) {
this.connection.connect();
}
return this.connection.query(queryString);
}
}
Довольно легко можно использовать логгер:
Connection loggedConnection = new LoggedConnection(connection);
// здесь логируется
methodToLog(loggedConnection);
// loggedConnection.getLog();
// а здесь уже нет
anotherMethod(connection);
Теперь проблема. К соеденению нужен менеджер транзакций. Добавляю его к соединению т.к они логически связаны.
interface Transaction {
begin()
commit()
rollback()
}
interface Connection {
transaction()
query(queryString)
}
Теперь я не могу использовать декораторы, потому что класс реализующий Transaction будет всегда иметь ссылку на оригинальный Connection и запросы из Transaction не будут логироваться.
PgTransaction implements Transaction {
PgTransaction(connection) {...}
begin(){
this.connection.query("BEGIN");
}
}
PgConnection implements Connection {
PgConnection() {
this.transaction = new PgTransaction(this); // this мы декорировать извне не сможем
}
}
Можно ли как-то элегантно сделать соединение где запросы от менеджера транзакции также логируемыми, при этом не пихая всю логику в класс соединения?
(Код предоставлен в качестве примера, его можно менять)
Вариант 1. Решение в лоб. Не декорируемо// Плюс: интуитивно понятная реализация
// Минус: лишняя логика в Connection
// Минус: надо отлавливать ошибки, чтобы логгер не остался в подключении
interface ConnectionLoggers {
add(logger)
remove(logger)
log(query)
}
Connection {
// ...
ConnectionLoggers loggers()
// ...
}
logger = new ConnectionLogger();
connection.loggers().add(logger);
try {
someMethod(connection);
// logger.getLog();
} finally {
connection.loggers().remove(logger);
}
Вариант 2. Прокидывать connection при каждом вызове// Плюс: можно декорерировать
// Минус: Менее удобен при вызове Transaction
// Минус: в begin() технически можно кинуть другое соединение (другое подключене к БД), что может нарушить логику работы
interface Transaction {
begin(connection)
commit(connection)
rollback(connection)
}
// вызов
connection.transaction.begin(connection);
Вариант 3. Клонирование состояния транзакции, с подменой соединения// Плюс: можно декорерировать
// Плюс: удобно вызывать методы транзакции
interface Transaction {
begin()
commit()
rollback()
// Создаем новый экземпляр Transaction, с тем же состоянием транзакции, но другим соединением.
// Оба экземпляра будут разделять одно и то же состояние (в тразакции или нет, уровень вложенности, уровень изоляции).
// Минус: В clone() технически можно кинуть другое соединение, что может нарушить логику работы.
// Минус: Интуитивно непонятно как должен быть реализован метод clone()
// Минус: clone() виден "пользователям" при работе с транзакциями.
Transaction clone(newConnection);
}
PgTransaction implements Transaction {
private bool inTransaction;
PgTransaction(Connection connection, TransactionState state) {
}
clone(c) {
new PgTransaction(c, this.getState())
}
}
LoggedConnection {
LoggedConnection(Connection connection) {
thix.txn = c.transaction().clone(this);
}
}
// вызов
connection.transaction.begin();