You are currently browsing the monthly archive for agosto 2017.

Desde hace dos semanas atrás he querido escribir sobre como resuelvo conflictos; pero, por alguna razón cósmica, se me olvidaba. La idea es simple: divide y vencerás. Veremos como resolver, commit por commit, los conflictos provenientes de un merge en lugar de tratar de resolver el lote completo. No obstante, me reservo una nota importante para el final del artículo. 🙂

(Estimado lectores, mi consejo es que recreen estos pasos.)

1.   Creando el proyecto base

Trabajaremos sobre un repositorio hipotético con un único fichero index.js:

export function sayHello() {
  console.log('Hello world');
}
export function darkOperation(a, b) {
  return a + b;
}

Son dos funciones simples. Una nos «Hello world» y la otra es una función del más creativo de los programadores.

Nuestro log es hasta el momento:

* 18c6bb7 (HEAD -> master) Initial commit

(Si siguen paso a paso el ejemplo, no olviden que el hash no será el mismo en su caso.)

2.   Que pase el desgraciado

Nuestro programador desgraciado decide crear la rama desgraciado. En ella deja el siguiente cambio:

export function sayHello() {
  console.log('Hola world');
  return true;
}
export function times(x, y) {
  return x * y;
}

Además, nos deja de recuerdo su genialidad para crear commits.

* 584061d (HEAD -> desgraciado) Changes
* 18c6bb7 (master) Initial commit

3.   Nosotros

Suponiendo que nos dicen que tenemos que traducir la palabra «mundo» y que cambiemos de nombre a una de las variables de la función desconocida en nuestra rama nosotros, obtendríamos

export function sayHello() {
  console.log('Hello mundo');
  return true;
}
export function darkOperation(a, w) {
  return a + w;
}

Con el log

* 4039ab8 (HEAD -> nosotros) Change argument name
* 88c3c47 i18n mundo
* 18c6bb7 (master) Initial commit

4.   La desgracia

Si la rama del desgraciado es integrada antes en master, obtendremos el siguiente estado

*   9e950b4 (HEAD -> master) Merge branch 'desgraciado'
|\
| * 584061d (desgraciado) Changes
|/
* 18c6bb7 Initial commit

Luego, si desde nuestra rama nosotros hacemos un merge, nos quedamos con el código

export function sayHello() {
<<<< HEAD
  console.log('Hello mundo');
||||||| merged common ancestors
  console.log('Hello world');
=======
  console.log('Hola world');
  return true;
>>> master
}
<<<< HEAD
export function darkOperation(a, w) {
  return a + w;
||||||| merged common ancestors
export function darkOperation(a, b) {
  return a + b;
=======
export function times(x, y) {
  return x * y;
>>>> master
}

Y acá empieza nuestra pesadilla…

5.   El capo

Pero, si en lugar de un desgraciado hubiese sido un capo quien hiciera esos mismos cambios de manera inteligente, tendríamos un rama capo que luciría como sigue

* b1c09f3 (HEAD -> capo) Fix unkwon function
* b319b3a Add chiche
* 36626c0 Change argument names
* 4dff9c0 i18n Hola
* 18c6bb7 (master) Initial commit

El código resultante es el mismo que lo que hizo el desgraciado.

export function sayHello() {
  console.log('Hola world');
  return true;
}
export function times(x, y) {
  return x * y;
}

La diferencia es que tenemos commits atómicos. Y esto será vital para poder resolver los conflictos.

No olvidemos que esto es muy positivo como equipo tanto como desarrolladores independientes. Nos dice que respetamos a las personas en nuestra profesión.

6.   Nosotros nuevamente

Cuando mezclemos los cambios del capo en master, obtendremos

*   6e5241f (HEAD -> master) Merge branch 'capo'
|\
| * b1c09f3 (capo) Fix unkwon function
| * b319b3a Add chiche
| * 36626c0 Change argument names
| * 4dff9c0 i18n Hola
|/
* 18c6bb7 Initial commit

Ahora, si hacemos un merge en nuestra rama nosotros, tendremos los mismos conflictos. Pero…

7.   Más sencillo, imposible

Lo que podemos hacer para evitar resolver los conflictos como un único lote es empezar creando una rama solved a partir de master, donde dejaremos solucionados los conflictos. En la rama solved veríamos algo como

*   6e5241f (HEAD -> solved, master) Merge branch 'capo'
|\
| * b1c09f3 (capo) Fix unkwon function
| * b319b3a Add chiche
| * 36626c0 Change argument names
| * 4dff9c0 i18n Hola
|/
* 18c6bb7 Initial commit

En ella, obtendremos los conflictos, pero no mendiante un merge, sino mediante un rebase. Es decir, en la rama solved en lugar de hacer un git merge nosotros, haremos un git rebase nosotros.

Esto nos mostrará el mensaje de conflictos, pero en el código veremos sólo el primer conflicto

export function sayHello() {
<<<< HEAD
  console.log('Hello mundo');
||||||| merged common ancestors
  console.log('Hello world');
=======
  console.log('Hola world');
>>>> i18n Hola
}
export function darkOperation(a, w) {
  return a + w;
}

Escribimos el código para luego corregirlo

export function sayHello() {
  console.log('Hola mundo');
}
export function darkOperation(a, w) {
  return a + w;
}

Lo añadimos al stage para decirle a git que lo hemos corregido con git add index.js. Luego, ejecutamos git rebase –-continue para aplicar el siguiente commit obteniendo

export function sayHello() {
  console.log('Hola mundo');
}
<<<< HEAD
export function darkOperation(a, w) {
  return a + w;
||||||| merged common ancestors
export function darkOperation(a, b) {
  return a + b;
=======
export function darkOperation(x, y) {
  return x + y;
>>>> Change argument names
}

Repetimos este proceso hasta acabar con un mensaje como «No rebase in progress?». Esto nos indicará que hemos terminado.

8.   El paso final

Finalmente, sólo nos queda ir a nuestra rama nosotros y desde ella hacer un merge desde master. Obtendremos los mismo conflictos; sin embargo, tenemos el conflicto solucionado en la rama solved. Traemos esa solución desde esa rama con git checkout solved -– index.js. Luego damos por resuelto el conflicto y obtenemos

*   ee2c35a (HEAD -> nosotros) Merge branch 'master' into 'nosotros'
|\
| *   6e5241f (master) Merge branch 'capo'
| |\
| | * b1c09f3 (capo) Fix unkwon function
| | * b319b3a Add chiche
| | * 36626c0 Change argument names
| | * 4dff9c0 i18n Hola
| |/
* | 4039ab8 Change argument name
* | 88c3c47 i18n mundo
|/
* 18c6bb7 Initial commit

Desde este momento, podemos eliminar la rama solved.

9.   Comentarios finales

Lo que no debemos olvidar es que la clave para todo esto es que cada miembro del equipo piense en el equipo. Los commit atómicos nos ayudan en este caso a poder reproducir cada conflicto y resolverlo en su versión más simple, es decir, en el commit en el que se originó.

Hay otras herramientas como habilitar la opción rerere de git para poder recordar la manera en que resolvemos estos conflictos y así reducir incluso más este proceso. Nuevamente, es parte del estilo de cada desarrollador. Saludos a todos y nos vemos. 😉

Anuncios

La semana pasada estuve leyendo código con megaclases ultrareusables mediante configuración de una larga lista de parámetros. Una navaja suiza de la más fina calidad. Recordé que era (o es) una práctica muy común escribir clases con métodos que procesen estructuras de datos mediante estructuras de control tan variadas y construyendo tantos caminos lógicos que el mantenimiento se vuelve una pesadilla. Veamos, por ejemplo, cómo afecta a nuestra lógica sólo usar una variable flag para configurar el comportamiento de un entidad.

Un parámetro flag es aquel que le indica a una función que realice una operación dependiendo de su valor. Imaginemos que deseamos comprar frutas; además, hay dos tipos de frutas: normal y A1. Usando el parámetro flag obtendríamos una implementación como la siguiente:

class Fruiterer...
  Ticket pay(Fruit fruit, bool isA1) {...}

Mi consideración ante los parámetros flags es evitarlos. En su lugar, prefiero definir métodos separados.

class Fruiterer...
  Ticket payNormal(Fruit fruit) {...}
  Ticket payA1(Fruit fruit) {...}

El motivo para hacer esto es que los métodos separados expresan de manera clara la intención que tengo al momento de usarlas. En lugar de recordar el significado del flag cuando escribo {pay(apple, false)}, leo sencillamente {payNormal(apple)}.

1.   Implementación compleja

Lo primero que se me ocurre cuando veo un parámetro flag es una potencial implementación compleja, done el caso más simple es llamar diferentes métodos.

Ticket pay(Fruit fruit, bool isA1) {
  if (isA1)
    // A1 fruit implementation
  else
    // regular fruit implementation
}

Pero la lógica puede ser más enredada.

Ticket pay(Fruit fruit, bool isA1) {
  call().complex();
  methods();
  if (isA1)  // <-
    that();
  over();
  if (isA1) {  // <-
    add().extra();
  } else {  // <-
    nothing();
    it().matters();
  }  // <-
  say.bye();
}

En estos casos, es complicado extraer las implementaciones en métodos separados sin duplicar dentro de las mismas. Una opción es mantener el método con el argumento flag oculto.

class Fruiterer...
public:
  Ticket payNormal(Fruit fruit) {
    return uglyPay(fruit, false);
  }
  Ticket payA1(Fruit fruit) {
    return uglyPay(fruit, true);
  }
private:
  Ticket uglyPay(Fruit fruit, bool isA1) {...}

El punto aquí es que sólo los métodos de compras deberían llamar a {uglyPay}. Le escribo un nombre que resalte para reforzar esa idea. (Considerando, además, que es posible añadir un regex para asegurarnos que nadie más lo llama.)

Observemos que hay una situación incluso más compleja. La explosión de casos si aumentara los tipos de frutas puede llevar a la tentación de usar enums (u otros) para representarlos. Engordaríamos el método y el mantenimiento implicaría… ¡reescribir el método!. Dos pesadillas de balde.

2.   Obteniendo el flag

¿Qué sucedería si la decisión de comprar una fruta A1 sólo dependiera del estado de la fruta? Asumamos que una fruta etiquetada es A1 mientras que las demás reciben un trato normal. En este caso, la fruta misma está actuando como flag. ¿Deberíamos tener un parámetro flag?

Dependerá de quien use la función. Si la venta sólo depende de la etiqueta de la fruta, quien use la función no tiene que preocuparse por las reglas de negocio que diferencian un fruta A1 de una normal; y así, definitivamente es razonable que el método de compra obtenga obtenga ese criterio basado en el estado de la fruta. Sólo necesitamos métodos diferentes cuando quien usar la función necesita especificar lo que quiere.

3.   El método de configuración

Si bien prefería ver

void on();
void off();

que ver

void switch(bool on);

Pero, hay excepciones. Depende de cómo se usar el método. Si se está trabajando con un library para GUIs o data sources configurando una fuente booleana, preferiría tener {switch(value)} en lugar de

if (value)
  on();
else
  off();

Una API debe ser escrita pensando en su usuario. En ocasiones, podríamos proveer ambos estilos si tenemos usuarios que trabajen de ambos contextos.

El mismo argumento aplica a {fruit}. Si tuviéramos un checkbox en la pantalla y sólo pasásemos su valor a {fruit}, entonces el parámetro flag tiene alguna justificación. (Nuevamente, depende.) No diría que es una elección simple, la mayoría de veces argumentaría que es difícil de entender que un setter booleano de un argumento, y por lo tanto es preferible tener los métodos explícitos.

Categorías

Anuncios