Clean Code

@gruizdevilla

        private static int k(int k, int kk) {
		String kkkkkkkk = Integer.toBinaryString(k);
		String kkkkkkk = Integer.toBinaryString(kk);
		// s1, s2 are binary representations
		while(kkkkkkkk.length() > kkkkkkk.length())
			kkkkkkk = "0" + kkkkkkk;
		while(kkkkkkkk.length() < kkkkkkk.length())
			kkkkkkkk = "0" + kkkkkkkk;
		int kkkkkkkkk = 0;
		for(int kkkkkkkkkk = 0; kkkkkkkkkk < kkkkkkkk.length(); kkkkkkkkkk++) {
			if(kkkkkkk.charAt(kkkkkkkkkk) != kkkkkkkk.charAt(kkkkkkkkkk))
				kkkkkkkkk++;
		}
		return kkkkkkkkk;
	}

🤦‍♀️

What is clean code?

The logarithmic rule of ⌛️ in software development

1x write code
10x fix code
100x read code

* completely invented numbers, but they feel realistic, doesn't they?

Bug cost

SELECT note as note_original, 
    REPLACE(
        REPLACE(
            REPLACE(
                REPLACE(
                    REPLACE(
                        REPLACE(
                            REPLACE(
                                REPLACE(
                                    REPLACE(
                                        REPLACE(
                                            REPLACE(
                                                REPLACE(
                                                    REPLACE(
                                                        REPLACE(
                                                            REPLACE(
                                                                REPLACE(
                                                                    REPLACE(
                                                                        REPLACE(
                                                                            REPLACE(
                                                                                REPLACE(
                                                                                    REPLACE(
                                                                                        REPLACE(
                                                                                            REPLACE(
                                                                                                REPLACE(
                                                                                                    REPLACE(
                                                                                                        REPLACE(
                                                                    REPLACE(
                                                                        REPLACE(
                                                                            REPLACE(
                                                                                REPLACE(
                                                                                    REPLACE(
                                                                                        REPLACE(
                                                                                            REPLACE(note, '\"', ''),
                                                                                        '.', ''),
                                                                                    '?', ''),
                                                                                '`', ''),
                                                                            '<', ''),
                                                                        '=', ''),
                                                                    '{', ''),
                                                                                                        '}', ''),
                                                                                                    '[', ''),
                                                                                                ']', ''),
                                                                                            '|', ''),
                                                                                        '\'', ''),
                                                                                    ':', ''),
                                                                                ';', ''),
                                                                            '~', ''),
                                                                        '!', ''),
                                                                    '@', ''),
                                                                '#', ''),
                                                            '$', ''),
                                                        '%', ''),
                                                    '^', ''),
                                                '&', ''),
                                            '*', ''),
                                        '_', ''),
                                    '+', ''),
                                ',', ''),
                            '/', ''),
                        '(', ''),
                    ')', ''),
                '-', ''),
            '>', ''),
        ' ', '-'),
    '--', '-') as note_changed FROM invheader

🤦‍♀️

Busquemos inspiración en algunas definiciones

BJARNE STROUSTRUP

(Inventor de C++)

 

Elegante y eficiente.
 

Lógica directa, mínimas dependencias y fácil de mantener.

GRADY BOOCH

(Desarrolló UML)

 

 

El código limpio se lee como prosa bien escrita

"BIG" DAVE THOMAS

(Padrino de Eclipse)

 

  • El código limpio puede ser leído y mejorado
    por un desarrollador distinto de su autor original.
  • Tiene tests unitarios y de aceptación.
  • Tiene nombres con significado.
  • Proporciona una forma de hacer las cosas en lugar
    de muchas alternativas.

MICHAEL FEATHERS

(Autor de "Working Effectively with Legacy Code")

  • El código limpio parece estar hecho
    por alguien a quien le importa.

RON JEFFRIES

(Extreme Programming Adventures in C#)

  1. Pasa todos los tests
  2. No tiene duplicidades
  3. Expresa las ideas de diseño del sistema
  4. Minimiza el número de entidades
    como clases, métodos y similares

WARD CUNNINGHAM

(Inventor de la Wiki y mucho más)

  1. Sabes que estás trabajando con código limpio
    cuando cada rutina que lees resulta ser como lo
    que esperabas encontrarte.
  2. Cuando parece que el lenguaje fue hecho para
    el problema que resuelve el código.

Cada cambio en el código:
lo mejora o lo empeora

LA REGLA DEL BOY SCOUT

 

Porque no es suficiente escribir buen código.
El código se tiene que mantener sin pudrirse
ni degradarse.

 

"Deja el lugar donde acampaste un poco
más limpio que como lo encontraste."

🤦‍♀️

NOMBRES SIGNIFICATIVOS

Significativo: que da a entender
o conocer con precisión.

NOMBRES QUE REVELEN INTENCIÓN

  • por qué existe?
  • ¿qué hace?
  • ¿cómo se usa?
 int d; //dias transcurridos
   int diasTranscurridos;
   int diasDesdeCreacion;
   int diasDesdeModificacion;

EVITA DESINFORMACIÓN


   cuentasList; //Si no es de tipo List....
   
   
   XYZControllerConManejoEficienteDeStrings;
   
   XYZControllerConAlmacenamientoEficienteDeStrings;

¿Nombre cortos?



  for(let i = 0; i < max-1; i++){
    for(let j = i+1; j < max; i++){
        //lógica
    }
  }


  for(let posicion_1 = 0; posicion_1 < max-1; posicion_1++){
    for(let posicion_2 = posicion_2 + 1; j < max; posicion_2++){
        //lógica
    }
  }

HAZ DISTINCIONES QUE APORTEN VALOR


    public void copiarObjeto(Clase1 o1, Clase1, o2);
    
    
    //vs
    
    
    public void copiarObjeto(Clase1 origen, Clase1 destino); 

PRONUNCIABLE


    public void copiarObjeto(Clase1 o1, Clase1, o2);
    
    
    //vs
    
    
    public void copiarObjeto(Clase1 origen, Clase1 destino); 

PRONUNCIABLE

The Walking COBOL!

BUSCABLE

Evita encodings

¿No tenemos lenguajes con tipado estático?

 

  • iContador
  • arrNums
  • bOcupado
  • u32Identificador

 

(Y para tipado dinámico, apóyate en TDD o extensiones del lenguaje como TypeScript para JavaScript)

 

Evita mapas mentales

Los lectores no tienen que tener que traducir
tus nombres a otros que entiendan.

 

 

La claridad es fundamental.

Nombres de clases

  • Deberían ser nombres y no verbos
  • Evitar palabras ambiguas como "Manager" ,Gestor", "Procesador", "Controller", "Data", "Info", ...
  • Piensa el nombre y no tengas miedo de cambiarlo; al contrario, ten miedo de no cambiarlo....

 

 

 

Nombres de clases

Real life!

  • SimpleBeanFactoryAwareAspectInstanceFactory
  • AbstractInterceptorDrivenBeanDefinitionDecorator
  • AbstractInterruptibleBatchPreparedStatementSetter
  • SimpleRemoteStatelessSessionProxyFactoryBean
  • TransactionAwarePersistenceManagerFactoryProxy

 

http://projects.haykranen.nl/java/

Nombres de métodos

  • Deben ser un verbo
  • Y en castellano además es bueno
    que sea imperativo
  • En el caso de constructores, mejor
    que sobrecargar es usar métodos estáticos de factoría (si no hay DI).
  • Intenta que describa lo que devuelve
  • Usa opuestos de forma precisa (Open/Close, Insert/Delete, Start/Stop)

 

 

Contexto

APORTA CONTEXTO SI ES NECESARIO

pero

EVITAR CONTEXTO GRATUITO

 

Evita cosas como:

  • Reusar el nombre de una clase dentro de una propiedad: Contacto.ContactoNombre
  • Prefijar las clases con el nombre de una aplicación, para eso están los namespaces/packages.
For the lack of a nail,
    throw new HorseshoeNailNotFoundException("no nails!");

For the lack of a horseshoe,
    EquestrianDoctor.getLocalInstance().getHorseDispatcher().shoot();

For the lack of a horse,
    RidersGuild.getRiderNotificationSubscriberList().getBroadcaster().run(
      new BroadcastMessage(StableFactory.getNullHorseInstance()));

For the lack of a rider,
    MessageDeliverySubsystem.getLogger().logDeliveryFailure(
      MessageFactory.getAbstractMessageInstance(
        new MessageMedium(MessageType.VERBAL),
        new MessageTransport(MessageTransportType.MOUNTED_RIDER),
        new MessageSessionDestination(BattleManager.getRoutingInfo(
                                        BattleLocation.NEAREST))),
      MessageFailureReasonCode.UNKNOWN_RIDER_FAILURE);

For the lack of a message,
    ((BattleNotificationSender)
      BattleResourceMediator.getMediatorInstance().getResource(
        BattleParticipant.PROXY_PARTICIPANT,
        BattleResource.BATTLE_NOTIFICATION_SENDER)).sendNotification(
          ((BattleNotificationBuilder)
            (BattleResourceMediator.getMediatorInstance().getResource(
            BattleOrganizer.getBattleParticipant(Battle.Participant.GOOD_GUYS),
            BattleResource.BATTLE_NOTIFICATION_BUILDER))).buildNotification(
              BattleOrganizer.getBattleState(BattleResult.BATTLE_LOST),
              BattleManager.getChainOfCommand().getCommandChainNotifier()));
//...
// For A/B Testing


    var getModalGreen = function() {
      d = Math.random() * 100;
      if ((d -= 99.5) < 0) return 1;
      return 2;
    };

🤦‍♀️

  class ReturnValues {
    private int    numDays;
    private String lastName;

    public ReturnValues(int i, String s) {
      numDays  = i;
      lastName = s;
    }

    public int    getNumDays()  { return numDays;  }
    public String getLastname() { return lastName; }
  }
  class AlsoReturnTransactionDate extends ReturnValues {
    private Date txnDate;
    public AlsoReturnTransactionDate(int i, String s, Date td) {
      super(i,s);
      txnDate = td;
    }
    public Date getTransactionDate() { return txnDate; }
  }
  
  class AddPriceToReturn extends AlsoReturnTransactionDate {
    private BigDecimal price;
    public AddPriceToReturn(int i, String s, Date td, BigDecimal px) {
      super(i,s,td);
      price = px;
    }
    public BigDecimal getPrice() { return price; }
  }

  class IncludeTransactionData extends AddPriceToReturn {
    private Transaction txn;
    public IncludeTransactionData(int i, String s, Date td, BigDecimal px, Transaction t) {
      super(i,s,td,px);
      txn = t;
    }
    public Transaction getTransaction() { return txn; }
  }

  class IncludeParentTransactionId extends IncludeTransactionData {
    private long id;
    public IncludeParentTransactionId(int i, String s, Date td, BigDecimal px, Transaction t, long id) {
      super(i,s,td,px,t);
      this.id = id;
    }
    public long getParentTransactionId() { return id; }
  }

  class ReturnWithRelatedData extends IncludeParentTransactionId {
    private RelatedData rd;
    public ReturnWithRelatedData(int i, String s, Date td, BigDecimal px, Transaction t, long id, RelatedData rd) {
      super(i,s,td,px,t,id);
      this.rd = rd;
    }
    public RelatedData getRelatedData() { return rd; }
  }

  class ReturnWithCalculatedFees extends ReturnWithRelatedData {
    private BigDecimal calcedFees;
    public ReturnWithCalculatedFees(int i, String s, Date td, BigDecimal px, Transaction t, long id, RelatedData rd, BigDecimal cf) {
      super(i,s,td,px,t,id,rd);
      calcedFees = cf;
    }
    public BigDecimal getCalculatedFees() { return calcedFees; }
  }

  class ReturnWithExpiresDate extends ReturnWithCalculatedFees {
    private Date expiresDate;
    public ReturnWithExpiresDate(int i, String s, Date td, BigDecimal px, Transaction t, long id, RelatedData rd, BigDecimal cf, Date ed) {
      super(i,s,td,px,t,id,rd,cf);
      expiresDate = ed;
    }
    public Date getExpiresDate() { return expiresDate; }
  }

  class ReturnWithRegulatoryQuantities extends ReturnWithExpiresDate {
    private RegulatoryQuantities regQty;
    public ReturnWithRegulatoryQuantities(int i, String s, Date td, BigDecimal px, Transaction t, long id, RelatedData rd, BigDecimal cf, Date ed, RegulatoryQuantities rq) {
      super(i,s,td,px,t,id,rd,cf,ed);
      regQty = rq;
    }
    public RegulatoryQuantities getRegulatoryQuantities() { return regQty; }
  }

  class ReturnWithPriorities extends ReturnWithRegulatoryQuantities {
    private Map<String,Double> priorities;
    public ReturnWithPriorities(int i, String s, Date td, BigDecimal px, Transaction t, long id, RelatedData rd, BigDecimal cf, Date ed, RegulatoryQuantities rq, Map<String,Double> p) {
      super(i,s,td,px,t,id,rd,cf,ed,rq);
      priorities = p;
    }
    public Map<String,Double> getPriorities() { return priorities; }
  }

  class ReturnWithRegulatoryValues extends ReturnWithPriorities {
    private Map<String,Double> regVals;
    public ReturnWithRegulatoryValues(int i, String s, Date td, BigDecimal px, Transaction t, long id, RelatedData rd, BigDecimal cf, Date ed, RegulatoryQuantities rq, Map<String,Double> p, Map<String,Double> rv) {
        super(i,s,td,px,t,id,rd,cf,ed,rq,p);
        regVals = rv;
    }
    public Map<String,Double> getRegulatoryValues() { return regVals; }
  }

🤦‍♀️


enum Bool 
{ 
    True, 
    False, 
    FileNotFound 
};

🤦‍♀️

package com.xxxx.flow.dto;

import com.xxxx.DTOIn;
import com.xxxx.XipConectorDAO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class IniciarProcesoContratacionCarterasAsesoradasSBPModel implements DTOIn {


   private static final long serialVersionUID = -5565412435484318101L;

   private XipConectorDAO XipConectorDAO;
   private String carteraAsesorada;
   private String idTestMifid;
   private String propuestaInversion;
   private String productosVinculados;
   private String idCliente;
   private String intervinientes;

}

🤦‍♀️

Una palabra por concepto

  • Consistencia en toda la aplicación.
  • Evita que una palabra tenga varios significados.
  • Evita usar varias palabras para lo mismo.

¿Cómo decías que se recuperaba el id?


    employee.id.Get()
    dependent.GetId()
    supervisor()
    candidate.id()

Palabras del ámbito de negocio

  • Consistencia en toda la aplicación.
  • Evita que una palabra tenga varios significados.
  • Evita usar varias palabras para lo mismo.

🤦‍♀️

Funciones

 

 

 

 

 

(Personalmente, prefiero FP a declarativa)

Funciones

  • Primera regla: Tienen que ser muy pequeñas.
     
  • Segunda regla: Deben ser aún más pequeñas.
     
  • Tercera regla: Todavía es muy grande.

Funciones

Una función:

hace solo UNA cosa,

y la hace BIEN

y no hace nada más.

Funciones

Una función no debe mezclar
niveles de abstracción.

Por ejemplo, no debe hacer invocaciones
a funciones de alto nivel y cosas técnicas
a la vez.

    String documento = generaDocumento();
    documento.append("Generado automaticamente por el sistema.")
    

SWITCH

Suficiente entidad para ser la única responsabilidad de una función.

public Importe calculaPaga(Empleado e) throws TipoEmpleadoInvalidoException {
  switch(e.type) {
    case COMISIONISTA:
        return calculaComisiones(e);
    case ASALARIADO:
        return calculaSalario(e);
    case FREELANCE:
        return calculaPagoPorHoras(e);
    default:
        throw new TipoEmpleadoInvalidoException(e);
  }
}

Muchas alternativas

var calculadoras = {};

calculadoras.COMISIONISTA = calculaComisiones();
calculadoras.ASALARIADO = calculaSalario();
calculadoras.FREELANCE = calculaPagoPorHoras();

function calculaPaga(empleado) {
    return calculadoras[empleado.type](empleado);
}

Y otra forma más ADT

function calculaPaga(Empleado e): Importe {
  return isComisionista(e) ? calculaComisiones(e) :
         isAsalariado(e)   ? calculaSalario(e) 
                           : calculaPagoPorHoras(e)
}
type Comisionista = ...
type Asalariado = ...
type Freelance = ...

type Empleado = Comisionista | Asalariado | Freelance;
function calculaComisiones(e: Comisionista): Importe{
  //...
}
function calculaSalario(e: Asalariado): Importe{
  //...
}
function calculaPagoPorHoras(e: Freelance): Importe{
  //...
}
function isComionista(e:Empleado): e is Comisionista {
  return e instanceOf Comisionista;
}
function isAsalariado(e:Empleado): e is Asalariado {
  return e instanceOf Asalariado;
}

Nombre descriptivos

  • Sin miedo a los nombres largos.
  • Mejor un nombre largo y claro
    que corto y enigmático.
  • Mejor un nombre largo y claro
    que un comentario.
  • Utiliza un estilo que facilite la lectura.
  • Dado lo anterior, mejor corto que largo.
miObj.elMetodoConNombreTanLargoQueAlFinalNoTeAcuerdasDelPrincipio() 

Argumentos

Número de parámetros:

  • Ninguno: bueno... ¿factoría?¿side effect warning?
  • Uno (función monádica): BIEN, fácil de concatenar.
  • Dos (función diádica):  ok
  • Tres (función triádica): hummmmmm..... ¿estas seguro?
  • Cuatro: ¿estas seguro?
  • Más de cuatro: Refactoriza

También puedes apoyarte en "named parameters" si el lenguaje lo permite

tf.nn.conv2d(
    input,
    filter,
    strides,
    padding,
    use_cudnn_on_gpu=True,
    data_format='NHWC',
    dilations=[1, 1, 1, 1],
    name=None
)

Argumentos Flag

  • Si es un argumento, igual lo puedes cambiar por dos funciones





     
  • Más sencillo con named arguments (para argumentos opcionales)
void setOn();
void setOff();

//... vs

void setSwitch(boolean on);

book(aCustomer, true)

//... vs

book(aCustomer, isPremium:true)
If {commission_rate} = 1.2 
   Then 20 
   Else If {commission_rate} = 1.15 
           Then 15 
           Else If {commission_rate} = 1.0 
                   Then 0 
                   Else If {commission_rate} = 1.16 
                           Then 16 
                           Else If {commission_rate} = 1.125 
                                   Then 12.5 
                                   Else If {commission_rate} = 1.1 
                                           Then 10
                                           Else {commission_rate} 
End

🤦‍♀️

Sin efectos secundarios 

  • Mejor para programación funcional.
  • Los efectos secundarios
    generan acoplamientos.
  • Si no queda más remedio, que quede
    explicitado:
    .compruebaClaveEIniciaSesion()

Sin efectos secundarios: beware of the Date!

function sePuedeConsumir(producto) {
    return new Date() < producto.fechaCaducidad;
}

/*comentarios*/

¿Buenos o malos?

/*comentarios*/

  • Siempre dicen la verdad
  • Nunca dicen la verdad
  • A veces dicen la verdad

/*comentarios*/

 

TooMuchCopyPasteException

Son la primera víctima del: 

// if hasSiblings, actionType == cont; else actionType == end.
if ($hasSiblings)
{
  $actionType = "cont";
} 
else 
{
  $actionType = "end";
}

🤦‍♀️

/*comentarios*/

De los buenos

  • Comentarios con información legal
  • Comentarios informativos (por ejemplo,
    referenciando documentos normativos
    sobre el cálculo de el DC de un CCC)
  • Explicando intenciones
  • Comentando APIs públicas

/*comentarios*/

De los malos

  • Mascullando
  • Redundantes: RUIDOOOO
  • Que conducen a engaño: TooMuchCopyPasteException
  • Comentando código complicado: reescribe el código!!!
  • Código comentado: ¿no tienes control de versiones?
  • Diario: ¿no tienes control de versiones????
  • Marcas personalizadas: porque el cierre del bucle está muy lejos?? ¿en serio?!?!
  • Documentación de métodos privados???

/*comentarios*/

 

 

 

 

Crea una cultura de comentarios

FORMATEANDO

Algunos consejos sobre formateo vertical:

  • Separación de bloques
  • Bloques compactos

Distancia entre elementos:

  • Acerca conceptos relacionados
  • Una función que invoque a otra: acércalas
  • El llamador por encima del llamado (excepción:
    JavaScript que pasa JSLint)
  • Variables declaradas cerca de donde se usan
  • Variables de instancia al principio de la clase

 

FORMATEANDO

 

TABs vs SPACEs

TABs vs SPACEs

 

 

"Developers Who Use Spaces Make More Money Than Those Who Use Tabs"

https://stackoverflow.blog/2017/06/15/developers-use-spaces-make-money-use-tabs/

TABs vs SPACEs

TABs vs SPACEs

TABs vs SPACEs

 

Si pasas de tabuladores a espacios y vas a pedir un aumento de sueldo, te recomiendo que busques en Google la diferencia entre correlación y causalidad 🤪

FORMATEANDO

 

  • Líneas cortas
  • Una acción por línea
  • Indentación, informa de jerarquía

 

FORMATEANDO

 

Team rules!

early optimization is the root of all evil

java

"Storing the data in an array saves cpu time"

 

🤦‍♀️

OBJETOS Y ESTRUCTURAS DE DATOS

Hay un motivo para la existencia de variables privadas:
no queremos que nadie dependan de ellas.

¿Por qué añadir getters y setters a todas ellas?

OBJETOS Y ESTRUCTURAS DE DATOS

Hay un motivo para la existencia de variables privadas:
no queremos que nadie dependan de ellas.

¿Por qué añadir getters y setters a todas ellas?

ABSTRACCIONES CORRECTAS

Un punto

public class Punto {
  public double x;
  public double y;
}

public interface Punto {
  public double getX();
  public double getY();
  public setCartesianas(double x, double y);
  public double getR();
  public double getTheta();
  public setPolares(double r, double theta);
}

LEY DE DEMETER

Un objeto no debe conocer nada
acerca de las interioridades de los objetos
que maneja.

 

Queremos evitar los accidentes de trenes:

ctx.getSession(true).getStorage().get("dato").value

Hablando de estructuras de datos

Buscaros un buen sistema de tipos

Gestión de errores

Programación síncrona:

Programación asíncrona:

  • Patrón de continuación (NodeJS): primer argumento
  • Promesas u Observables: función controladora de error
  • Procedural: Excepciones
  • Funcional: Either (mejor que Maybe)

Para Javeros y similares

¿Unchecked o checked exceptions?

¿Qué debe tener una excepción?

Las excepciones tienen que definirse
en función de las necesidades de los usuarios.

 

Tienen que proporcionar el contexto suficiente
para entender por que se lanzan.

¿Qué debe tener una excepción?

Las excepciones tienen que definirse
en función de las necesidades de los usuarios.

 

Tienen que proporcionar el contexto suficiente
para entender por que se lanzan.

NULL

  • No pases nulos
  • No devuelvas nulos

Inmutabilidad

¿Por qué es importante?

  • Predecible
  • Rendimiento
  • Seguimiento de las mutaciones

Hablemos de Tests

TDD

  • Escribe código de producción sólo cuando
    tengas un test unitario que lo pruebe que falle.
  • Escribe el código de prueba mínimo y necesario
    que haga que el código de producción falle.
  • Solo escribe el código de producción imprescindible
    para que el test pase.

Sobre los tests

  • Limpio
  • Un concepto por test
  • Tres pasos para un test:

 

  1. Prepara el escenario
  2. Ejecuta el código
  3. Comprueba el resultado

Test FIRST

F: FAST.  Rápidos.

I: INDEPENDENT. Independientes entre si.

R: REPEATABLE Repetibles independientemente del  entorno.

S: SELF VALIDATING Si o No. Pasa o no pasa. Resultado claro.

T:TIMELY Escritos secuencialmente, justo antes 
de necesitarse.

 

En serio,

¡hazlos!

Clases

Todo lo anterior aplica:

  • Las clases deben ser pequeñas.
  • Tener una única responsabilidad.
  • Cohesión entre sus métodos y propiedades.
  • Deben ser entendibles, tener sentido

Diseño emergente

Acrónimos y frasecitas

  • SRP: single responsibility principle

  • KISS: Keep it simple and stupid, short, straightforward...

  • YAGNI: you ain't gonna need it

  • DRY and DIE: Don't repeat your self, duplication is evil
          Consejo: RY then DRY

  • Demeter Law: no hables con extraños

  • POLS: principle of least surprise

  • POGE: Principle of good enough

  • Brook's law: nueve mujeres no hacen un bebé en un mes

Consideraciones finales

Alejarse del teclado 5 minutos después de trabajar

Clean Code v2018

By Gonzalo Ruiz de Villa

Clean Code v2018

Charla de Clean Code

  • 554
Loading comments...

More from Gonzalo Ruiz de Villa