¿Qué es Integración Contínua del software?

CI (siglas de integración continua) es una práctica de desarrollo de software en la que un servidor de integración continua sondea un repositorio de control de versiones, crea artefactos y valida el mismo con un conjunto de pruebas definidas. Es una práctica común para la mayoría de las empresas y personas … y no es la verdadera definición!! perdón por la broma, entonces ¿Cuál es el verdadero significado de Integración continua del software?

 

¿Qué es Integración Contínua en realidad?

Bueno, integración continua del software no es “simplemente” una especie de “Jenkins | Travis | Ir | Teamcity “que sondea el repositorio de git del proyecto, lo compila y ejecuta una serie de pruebas contra el artefacto. De hecho, esta es la parte menos interesante de Integración continua del software, que no es una tecnología (Jenkins), sino una práctica ágil creada por Grady Booch y adoptada y prescrita por la metodología de programación Extreme.
Como analogía con otra técnica de programación extrema, TDD no trata de pruebas unitarias (aunque usa pruebas unitarias), sino de retroalimentación, acerca de cómo obtener retroalimentación tan pronto como sea posible y acelerar los ciclos de desarrollo (que se implementa en un uso concreto de pruebas unitarias).
Integración continua del software también se trata de retroalimentación. En CI, el software se construye varias veces al día (idealmente cada pocas horas), cada vez que un desarrollador integra código en la línea principal (que debería ser frecuente), para evitar el “infierno de integración” (fusión de código de diferentes desarrollos al final de una interacción de desarrollo). Integración continua del software evita el código integrador de “infierno de la integración” tan pronto como sea posible y obliga a los miembros del equipo a ver qué están haciendo otros desarrollos y a tomar decisiones en equipo compartido sobre el nuevo código.   
La metodología indica que cada integrante del equipo integra con la mayor frecuencia posible en la línea principal. Cada contribución al VCS (Sistema de control de versiones) es potencialmente una versión, por lo que cada contribución no debería romper la funcionalidad y debería pasar todas las pruebas conocidas.
Un Servidor CI construirá un artefacto a partir de las últimas fuentes de la línea principal y pasará todas las pruebas conocidas. Si hay una falla, la IC advertirá a todos los miembros del equipo sobre el estado de la construcción (RED).
La máxima prioridad del equipo es mantener la compilación en su valor predeterminado (VERDE).

 

¿Qué NO es Integración continua del software?

Una vez que nos dimos cuenta de que Integración continua del software es mucho más que el simple uso de un Servidor de CI, podemos decir que:
Trabajar con ramas de características y tener un maestro de comprobación de CI no es integración continua del software. Trabajar con solicitudes de extracción no es integracion continua de software. 
Es importante tener en cuenta que no estoy juzgando en términos de buenas / malas prácticas tanto las ramas de características como las solicitudes de extracción son simplemente otras metodologías diferentes de integración continua del software.
Tanto las ramas de características como las solicitudes de extracción dependen del trabajo en una rama diferente de la maestra (la supervisada por el servidor de CI), esto lleva a ciclos más largos antes de que pudieran combinarse en maestros.
Las ramas de características y las solicitudes de extracción se basan profundamente en la planificación de recursos / tareas del equipo para evitar refactores en una tarea (rama) que afecta a los desarrollos en otra tarea (rama) que minimiza el “infierno de integración” enhebrado
Ejemplo de un infierno de integración:

Tenemos el siguiente código, dos clases que aprovecha las llamadas de resto de la API a una API externa

 

 

 

APIUsersAccessor
class APIUsersAccessor
{
    const USERS_API_PATH = "/users";
 /**
     * @var string
     */
    private $host;
    /**
     * @var string
     */
    private $username;
    /**
     * @var string
     */
    private $password;
    public function __construct(string $host, string $username, string
$password)
    {
        $this->host = $host;
        $this->username = $username;
        $this->password = $password;
}
    public function getAllUsers(): array
    {
        $data = array(
            "email" => $this->username,
            "password" => $this->password
        );
        $headers = array(
            "Content-Type" => "application/json;charset=UTF-8"
        );
        $request = \Requests::GET($this->host.self::USERS_API_PATH,
$headers, json_encode($data));
        return json_decode($request->body);
    }
}

APIProductsAccessor
class APIProductsAccessor
{
    const PRODUCTS_API_PATH = "/products";
    /**
* @var string
     */
    private $host;
    /**
     * @var string
     */
    private $username;
    /**
     * @var string
     */
    private $password;
    public function __construct(string $host, string $username, string
$password)
    {
        $this->host = $host;
        $this->username = $username;
        $this->password = $password;
}
    public function getAllProducts(): array
    {
        $data = array(
            "email" => $this->username,
            "password" => $this->password
        );
        $headers = array(
            "Content-Type" => "application/json;charset=UTF-8"
        );
        $request = \Requests::GET($this->host.self::PRODUCTS_API_PATH,
$headers, json_encode($data));
        return json_decode($request->body);
    }
}

Como se puede ver, ambos códigos son muy similares (es la duplicación clásica del código). Ahora vamos a comenzar dos características de desarrollo con 2 ramas de desarrollo. El primer desarrollo debe agregar un número de teléfono a la solicitud de API de productos, el segundo debe crear una nueva API para consultar todos los automóviles disponibles en una tienda.
Este es el código resultante en la API de productos después de agregar el número de teléfono. 

APIUsersAccessor (with telephone)
class APIUsersAccessor
{
....
    public function __construct(string $host, string $username, string
$password)
{
.......
  $this->telephone = $telephone;
    }
    public function getAllUsers(): array
    {
        $data = array(
            "email" => $this->username,
            "password" => $this->password,
   "tel" => $this->telephone
        );
..... }
}

De acuerdo, el desarrollador agregó el campo faltante y lo agregó a la solicitud. El desarrollador de la rama 1 espera esta diferencia como la fusión con el maestro:

true continuous integration

Pero el problema es que el desarrollador 1 no sabe que el desarrollador 2 ha hecho un refactor para reducir la duplicación de código porque CarAPI es muy similar a UserAPI y ProductAPI, por lo que el código en su rama será:

BaseAPIAccessor
abstract class BaseAPIAccessor
{
    private $apiPath;
    /**
* @var string
     */
    private $host;
    /**
     * @var string
     */
    private $username;
    /**
     * @var string
     */
    private $password;
    protected function __construct(string $host,string $apiPath, string
$username, string $password)
    {
        $this->host = $host;
        $this->username = $username;
        $this->password = $password;
        $this->apiPath = $apiPath;
}
    protected function doGetRequest(): array
    {
        $data = array(
            "email" => $this->username,
            "password" => $this->password
        );
        $headers = array(
            "Content-Type" => "application/json;charset=UTF-8"
        );
        $request = \Requests::GET($this->host.$this->apiPath, $headers,
json_encode($data));
        return json_decode($request->body);
    }
}
concrete APIs
class ApiCarsAccessor extends BaseAPIAccessor
{
    public function __construct(string $host, string $username, string
$password)
    {
        parent::__construct($host, "/cars", $username, $password);
}
    public function getAllUsers(): array
    {
        return $this->doGetRequest();
    }
}
class APIUserAccessor extends BaseAPIAccessor
{
    public function __construct(string $host, string $username, string
$password)
    {
        parent::__construct($host, "/users", $username, $password);
}
    public function getAllUsers(): array
    {
        return $this->doGetRequest();
    }
}
class APIProductsAccessor extends BaseAPIAccessor
{
    public function __construct(string $host, string $username, string
$password)
    {
        parent::__construct($host, "/products", $username, $password);
}
    public function getAllProducts(): array
    {
        return $this->doGetRequest();
    }
}

La imagen de verdad sería algo así:

true continuous integration

Así que, básicamente, tendremos un gran conflicto al final del ciclo de desarrollo cuando fusionas branch1 y branch2 en mainline. Tendremos que hacer una gran cantidad de revisiones de código, lo que implicará un proceso arqueológico de revisión de todas las decisiones de pasados en una fase de desarrollo y ver cómo fusionar el código. En este caso concreto, el número de teléfono también implicará algún tipo de reescritura.
Algunos argumentarán que el desarrollador2 no debería haber hecho un refactor porque la planificación establecía que él tenía que desarrollar SÓLO CarApi, y la planificación establecía claramente que no debería haber una colisión con UserAPI. Bueno, sí … pero para que este tipo de planificación extrema funcione, debe haber una buena planificación de todos los recursos, deberíamos tener muchas reuniones de arquitectura que involucren al desarrollador1 y al desarrollador2.

En estas reuniones arquitectónicas, developer1 y developer2 deberían haberse dado cuenta de que existe algún tipo de duplicación de código y tienen que decidir o intervenir y replanificar, o no hacer nada y aumentar la deuda técnica, trasladando la decisión del refactorio a futuras iteraciones. Esto tal vez no suena a ágil, ¿no? pero el punto es que es difícil mezclar prácticas ágiles y no ágiles.

Si presentamos solicitudes de sucursal / extracción, el proceso de planificación iterativa completa funciona mejor; si lo hacemos, la integración continua ágil es la herramienta adecuada. Una vez más, no estoy diciendo que las ramas de características / solicitudes de extracción sean buenas / malas, simplemente estoy diciendo que no son prácticas ágiles.
Agile tiene que ver con la comunicación, se trata de una mejora continua y se trata de comentarios lo antes posible, en el enfoque ágil, el desarrollador1 será consciente de la refactorización de developer2 al principio, pudiendo iniciar un diálogo con developer1 y verificar si el el tipo de abstracción que está proponiendo será el correcto para incluir también la adición de un número de teléfono.

 

Ok … ¡pero espera! ¡Necesito una rama de características! ¿Qué sucede si no todas las características se pueden entregar al final de una iteración?

 

Las ramas de características son la solución a un problema: qué hacer si no todo el código se puede entregar al final de una iteración, pero no es la única solución.
Integración continua del software tiene otra solución a este problema, son la “característica alterna”. Las ramas de características aíslan la función de trabajo en progreso del producto final a través de una bifurcación (el w.i.p. vive en una copia separada del código), Feature alterna el aislamiento de la función del resto del código utilizando … ¡Código !.

La característica más simple alternar que uno puede escribir es el temido if-then-else, es el ejemplo que encontrará en la mayoría de los sitios cuando buscó en Google “función de alternar”. No es la única forma de implementación, ya que cualquier otro tipo de ingeniería de software puede reemplazar esta lógica condicional con polimorfismo.
En este ejemplo, en Slim estamos creando en la iteración actual un nuevo punto final REST que no queremos que esté listo para producción, tenemos este código

code prior the toggling
<?php
require '../vendor/autoload.php';
use resources\OriginalEndpoint
$config = [
    'settings' => [
        'displayErrorDetails' => true,
        'logger' => [
            'name' => "dexeus",
            'level' => Monolog\Logger::DEBUG,
            'path'  => 'php://stderr',
], ],
];
$app = new \Slim\App(
$config );
$c = $app->getContainer();
$c['logger'] = function ($c) {
    $settings = $c->get('settings');
    $logger = LoggerFactory::getInstance($settings['logger']['name'],
$settings['logger']['level']);
    $logger->pushHandler(new
Monolog\Handler\StreamHandler($settings['logger']['path'],
$settings['logger']['level']));
    return $logger;
};
$app->group("", function () use ($app){
 OriginalEndpoint::get()->add($app); //we are registering the endpoint
in slim });

Podemos definir la característica alternar con una simple cláusula “if”

if clause feature toggle
<?php ....
$app->group("", function () use ($app){
 OriginalEndpoint::get()->add($app);
    if(getenv("APP_ENV") === "development") {
        NewEndpoint::get()->add($app); // we are registering the new
endpoint if the environment is set to development (devs machines should
have APP_ENV envar setted to development)
} });

y podemos refinar nuestro código para expresar mejor lo que estamos haciendo y poder tener varios entornos (¿quizás para tener una situación AB de prueba?)



configuration map feature toggle
<?php
......
$productionEnvironment = function ($app){
    OriginalEndpoint::get()->add($app);
};
$aEnvironment = function ($app){
    productionEnvironment($app);
    NewEndpointA::get()->add($app);
};
$bEnvironment = function ($app){
    productionEnvironment($app);
    NewEndpointB::get()->add($app);
};
$develEnvironment = function ($app){
    productionEnvironment($app);
    NewEndpointInEarlyDevelopment::get()->add($app);
};
$configurationMap = [
    "production" => $productionEnvironment,
    "testA" => $aEnvironment,
    "testB" => $bEnvironment,
    "development" => $develEnvironment
];
$app->group("", function () use ($app, $configurationMap){
    $configurationMap[getenv("APP_ENV")]($app);
});

Las ventajas de esta técnica son coherentes con el objetivo principal de CI (tener retroalimentación constante sobre la integración / validación del código / colisiones con otros desarrollos), el código en progreso se desarrolla y se implementa en la producción y tenemos comentarios constantes sobre la integración de la nueva característica con el resto del código, aprovechando el riesgo de habilitar la característica cuando se desarrolla.

Es una buena práctica eliminar este tipo de conmutadores del código, una vez que la nueva característica se haya estabilizado para evitar agregar complejidad a la base del código.
Ok, hemos llegado al final de esta primera parte. Hemos redescubierto que la integración continua es “no solo” usar un servidor de CI, sino también adoptar una práctica con perseverancia y disciplina. En la segunda parte hablaremos sobre cómo modelar un buen flujo de CI.

 

Si te gustó este artículo sobre integración continua del software , te puede gustar:

 

Notas sobre DDD Europe

Arquitectura de microservicios vs arquitectura monolítica 

Scala generics I: clases genéricas y type bounds 

Scala generics II: covarianza y contravarianza 

Principio de responsabilidad única 

Por qué Kotlin?

Patrón MVP en iOS

F-bound en Scala

Sobre Dioses y procrastinación

Arquitectura de microservicios

Fibers en Nodejs

Simular respuestas del servidor con Nodejs

Barcelona como ciudad intelignete

Mapa de los “main players”: ecosistema startup y tech en Barcelona

Ecosistema de salud digital en Barcelona