Pular para o conteúdo principal

Policy-based design: log writer

Policy-based design

Vamos neste artigo dar mais uma pincelada no Policy-based design. Vamos fazer como exemplo uma classe de log.
Como este é só um exemplo, não vamos considerar múltiplos parâmetros no log, mas somente uma string, assim não fugiremos do assunto.

Uma das coisas mais importantes neste tipo de design é o desacoplamento. Ele é uma excelente alternativa ao uso de interfaces por duas razões:

  1. Não gera chamadas virtuais (ou um nível de indireção em tempo de execução)
    Duck typing (https://pt.wikipedia.org/wiki/Duck_typing)

  2. Eu gosto bastante desse tipo de design, já usado aqui: http://simplycpp.com/2016/02/05/leitura-de-configuracao-em-c/




Vamos ver o seguinte exemplo:
(Sem variadic templates e parâmetros genéricos para não fugir do tema)

[code language="cpp"]
struct log_writer {
void error(const std::string &s) {
cout << s << endl;
}
void info(const std::string &s) {
cout << s << endl;
}
};
//...
log_writer logger;
logger.info("Starting...");
[/code]

Este código é simples e funciona bem, mas vemos de cara um acoplamento - cout.

A primeira alteração que devemos fazer é uma *policy* para a escrita dos logs - writer_policy

[code language="cpp"]
//Concept: Writer must define operator <<
#define Writer typename

template<Writer writer_policy>;
struct log_writer {
writer_policy& _writer;
log_writer(writer_policy& writer) : _writer(writer) { }

void error(const std::string& s) {
_writer << s;
}
void info(const std::string& s) {
_writer << s;
}
};
[/code]

Agora temos uma policy onde o writer precisa definir o operador << somente.
Inicialmente, duas policies naturais para o nosso caso são uma que imprima o log no console e outra que imprima o log em arquivo:

[code language="cpp"]
struct console_writer {
template<typename T>
console_writer& operator << (const T& s) {
std::cout << s << endl;
return *this;
}
};

struct file_writer {
ofstream _file_s;
file_writer(const string& f_name) {
_file_s.open(f_name); //Example - never fails
}
~file_writer() {
_file_s.close();
}

template<typename T>
file_writer& operator << (const T& s) {
_file_s << s << endl;
return *this;
}
};
[/code]

E para usarmos agora o nosso log_writer com uma policy, fazemos da seguinte forma:

[code language="cpp"]
console_writer f;
log_writer<console_writer> logger(f);
// ou
file_writer f(file_name);
log_writer<file_writer> logger(f);
[/code]

Bem simples e fácil.
De uma forma transparente para o algoritmo de log, temos a abstração da saída de log para arquivo ou console.
Uma alternativa para isso é usar uma interface (com membros do tipo funções virtuais pura) e uma implementação. Isso nos causa problemas:

  1. Para que a classe possa ser usada, ela precisa necessariamente herdar de uma interface específica;

  2. Usando o duck typing, o importante não é a herança, mas sim a assinatura dos métodos respeitar o uso;

  3. Com uma interface e duas implementações, teremos uma chamada em vtable sempre que os métodos forem chamados.


Isso será discutido mais profundamente em outro artigo.

Continuando, uma outra policy interessante de ser implementada é a formatação de log.
Um exemplo seria fazer uma formatação como:
[2016-03-01 09:50:47] Starting...

Vamos alterar o log_writer para suportar formatação.

[code language="cpp"]
template<Writer writer_policy, Formatter formatter_policy>
struct log_writer {
writer_policy& _writer;
formatter_policy _formatter;

log_writer(writer_policy& writer, formatter_policy formatter) :
_writer(writer), _formatter(formatter) { }

void error(const std::string& s) {
_writer << _formatter(s);
}
void info(const std::string& s) {
_writer << _formatter(s);
}
};
[/code]

Eu recebi um formatter_policy, sendo que este precisa definir o operador () e retornar uma string.
Agora vamos implementar o formatter:

[code language="cpp"]
std::string dt_format(const string& s) {
//...
timeinfo = localtime(&rawtime);
strftime(buffer, sizeof(buffer), "[%Y-%m-%d %I:%M:%S] ",timeinfo);
std::string str(buffer);
return str + s;
}
struct my_formatter {
std::string operator()(const std::string& s) {
return dt_format(s);
}
};
[/code]

Um detalhe interessante. Como podemos usar o formatter ?
Para nós, tanto uma função quanto um Functor vai funcionar.
Vamos mostrar um exemplo de cada abaixo.

[code language="cpp"]
// usando uma função simples
log_writer<console_writer, std::function<string(const string&)>> logger(out_writer, dt_format);
// 2a forma
log_writer<console_writer, std::function<decltype(dt_format)>> logger(out_writer, dt_format);
// Ou usando um functor
my_formatter fmt;
log_writer<console_writer, my_formatter> logger(out_writer, fmt);
[/code]

Uma outra alternativa interessante é usar um lambda.

[code language="cpp"]
auto l_fmt = [](const string& s) -> string {
return "--> " + s;
};
log_writer<console_writer, decltype(l_fmt)> logger(out_writer, l_fmt);
[/code]

Um último ponto a se ver é a legibilidade. Podemos criar uma função onde os tipos são inferidos e não precisam ser declarados.
Podemos fazer um helper make_logger. Vamos ver como fica o uso:

[code language="cpp"]
template<Writer writer_policy, Formatter formatter_policy>
auto make_logger(writer_policy& writer, formatter_policy formatter) -> log_writer<writer_policy, formatter_policy>
{
return log_writer<writer_policy, formatter_policy>(writer, formatter);
}

//Usage:
auto logger = make_logger(out_writer, dt_format);
[/code]

Bem melhor, não ?

Fontes:
https://github.com/SimplyCpp/examples/blob/master/policy1.cpp
https://github.com/SimplyCpp/examples/blob/master/policy2.cpp
https://github.com/SimplyCpp/examples/blob/master/policy3.cpp
https://github.com/SimplyCpp/examples/blob/master/policy4.cpp

Comentários

Postagens mais visitadas deste blog

Mestre Iota

Iota é a nona letra do alfabeto grego, ela é equivalente à letra i do nosso alfabeto. Por convenção ou hábito, utilizamos a letra i na programação para indicar algum tipo de incrementador, como por exemplo, em um for-loop . https://gist.github.com/fabiogaluppo/a23894ae743f7dd29274 Curiosamente, iota , como identificador, também é utilizado na programação para indicar uma sequência finita e consecutiva de números inteiros, como por exemplo, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]. Inclusive, originalmente na STL existia a função iota , inspirada pela linguagem de programação APL , você pode conferir neste link: http://www.sgi.com/tech/stl/iota.html

Valores Aleatórios Simplificados

A partir do C++ 11, foi introduzido o header <random>  com diversos facilitadores para suporte de geração de números aleatórios. A produção destes números é feita através da combinação de duas categorias de objetos: os geradores e os distribuidores. Os geradores, são responsáveis pela geração dos números, e os distribuidores são responsáveis pela transformação dos números gerados em algum tipo de distribuição de probabilidade.  Como por exemplo, uma distribuição normal (aquela da Gaussiana) ou uma distribuição de Pareto (aquela do 80-20). As opções não faltam, como você pode ver nas referências, por exemplo:   http://www.cplusplus.com/reference/random/ ou  http://en.cppreference.com/w/cpp/header/random .