0% encontró este documento útil (0 votos)
335 vistas299 páginas

Clean C++ - Es

Cargado por

crow07
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
335 vistas299 páginas

Clean C++ - Es

Cargado por

crow07
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 299

Machine Translated by Google

Limpiar  C++
Desarrollo  de  software  sostenible
Patrones  y  Mejores  Prácticas  con  C++  17

Stephan  Roth

www.allitebooks.com
Machine Translated by Google

Limpiar  C++

Patrones  de  desarrollo  de  software  sostenible  
y  mejores  prácticas  con  C++  17

Stephan  Roth

www.allitebooks.com
Machine Translated by Google

C++  limpio:  patrones  de  desarrollo  de  software  sostenible  y  mejores  prácticas  con  C++  17

Stephan  Roth
Bad  Schwartau,  Schleswig­Holstein,  Alemania

ISBN­13  (pbk):  978­1­4842­2792­3  DOI   ISBN­13  (electrónico):  978­1­4842­2793­0
10.1007/978­1­4842­2793­0

Número  de  control  de  la  Biblioteca  del  Congreso:  2017955209

Copyright  ©  2017  por  Stephan  Roth

Esta  obra  está  sujeta  a  derechos  de  autor.  Todos  los  derechos  están  reservados  por  el  Editor,  ya  sea  total  o  parcialmente  el  
material,  específicamente  los  derechos  de  traducción,  reimpresión,  reutilización  de  ilustraciones,  recitación,  radiodifusión,  
reproducción  en  microfilmes  o  de  cualquier  otra  forma  física,  y  transmisión  o  almacenamiento  de  información.  y  recuperación,  
adaptación  electrónica,  software  de  computadora,  o  por  metodología  similar  o  diferente  ahora  conocida  o  desarrollada  en  el  futuro.

En  este  libro  pueden  aparecer  nombres,  logotipos  e  imágenes  de  marcas  registradas.  En  lugar  de  utilizar  un  símbolo  de  marca  
comercial  con  cada  aparición  de  un  nombre,  logotipo  o  imagen  de  marca  registrada,  utilizamos  los  nombres,  logotipos  e  imágenes  
solo  de  manera  editorial  y  en  beneficio  del  propietario  de  la  marca  comercial,  sin  intención  de  infringir  la  marca  comercial.

El  uso  en  esta  publicación  de  nombres  comerciales,  marcas  registradas,  marcas  de  servicio  y  términos  similares,  incluso  si  no  
están  identificados  como  tales,  no  debe  tomarse  como  una  expresión  de  opinión  sobre  si  están  o  no  sujetos  a  derechos  de  propiedad.

Si  bien  se  cree  que  los  consejos  y  la  información  de  este  libro  son  verdaderos  y  precisos  en  la  fecha  de  publicación,  ni  los  autores  ni  los  
editores  ni  el  editor  pueden  aceptar  ninguna  responsabilidad  legal  por  los  errores  u  omisiones  que  puedan  cometerse.  El  editor  no  
ofrece  ninguna  garantía,  expresa  o  implícita,  con  respecto  al  material  contenido  en  este  documento.

Imagen  de  portada  de  Freepik  (www.freepik.com)

Director  general:  Welmoed  Spahr
Director  editorial:  Todd  Green
Editor  de  adquisiciones:  Steve  Anglin
Editor  de  desarrollo:  Matthew  Moodie
Revisor  técnico:  Marc  Gregoire
Editor  coordinador:  Mark  Powers
Editora  de  estilo:  Karen  Jameson

Distribuido  al  comercio  de  libros  en  todo  el  mundo  por  Springer  Science+Business  Media  New  York,  233  
Spring  Street,  6th  Floor,  New  York,  NY  10013.  Teléfono  1­800­SPRINGER,  fax  (201)  348­4505,  correo  electrónico  
orders­ny  @  springer­sbm.com,  o  visite  www.springeronline.com.  Apress  Media,  LLC  es  una  LLC  de  California  y  el  único  miembro  
(propietario)  es  Springer  Science  +  Business  Media  Finance  Inc  (SSBM  Finance  Inc).  SSBM  Finance  Inc  
es  una  corporación  de  Delaware .

Para  obtener  información  sobre  las  traducciones,  envíe  un  correo  electrónico  a  [email protected],  o  visite  http://www.apress.com/
rights­permissions .

Los  títulos  de  Apress  se  pueden  comprar  al  por  mayor  para  uso  académico,  corporativo  o  promocional.  Las  versiones  y  licencias  de  libros  
electrónicos  también  están  disponibles  para  la  mayoría  de  los  títulos.  Para  obtener  más  información,  consulte  nuestra  página  web  de  ventas  
masivas  de  libros  electrónicos  e  impresos  en  http://www.apress.com/bulk­sales.

Cualquier  código  fuente  u  otro  material  complementario  al  que  haga  referencia  el  autor  en  este  libro  está  disponible  para  los  lectores  
en  GitHub  a  través  de  la  página  del  producto  del  libro,  ubicada  en  www.apress.com/9781484227923.  Para  obtener  información  más  
detallada,  visite  http://www.apress.com/source­code.

Impreso  en  papel  libre  de  ácido

www.allitebooks.com
Machine Translated by Google

A  Caroline  y  Maximilian:  mi  querida  y  maravillosa  familia.

www.allitebooks.com
Machine Translated by Google

Contenido  de  un  vistazo

Sobre  el  autor        xiii

Acerca  del  revisor  técnico     xiv

Agradecimientos   xvi

■Capítulo  1:  Introducción        1

■Capítulo  2:  Construir  una  red  de  seguridad        9

■Capítulo  3:  Tenga  principios      27

■Capítulo  4:  Conceptos  básicos  de  Clean  C++     41

■Capítulo  5:  Conceptos  avanzados  de  C++  moderno      85

■Capítulo  6:  Orientación  a  objetos        133

■Capítulo  7:  Programación  funcional        167

■Capítulo  8:  Desarrollo  dirigido  por  pruebas     191

■Capítulo  9:  Patrones  de  diseño  y  expresiones  idiomáticas        217

■Apéndice  A:  Guía  UML  pequeña        273

■Bibliografía           285

Índice         287

www.allitebooks.com
Machine Translated by Google

Contenido

Sobre  el  autor        xiii

Acerca  del  revisor  técnico     xiv

Agradecimientos   xvi

■Capítulo  1:  Introducción        1

Entropía  del  software        2

Código  limpio           3

¿Por  qué  C++?           4

C++11:  el  comienzo  de  una  nueva  era     4

¿Para  quién  es  este  libro?        5

Convenciones  utilizadas  en  este  libro     5

Barras  laterales           5

Notas,  consejos  y  advertencias           6

Ejemplos  de  código        6

Sitio  web  complementario  y  repositorio  de  código  fuente     7

diagramas  UML        7

■Capítulo  2:  Construir  una  red  de  seguridad        9

La  necesidad  de  probar           9

Introducción  a  las  pruebas      11

Pruebas  unitarias…………………………………………………………………………        13

¿Qué  pasa  con  el  control  de  calidad?        14

Reglas  para  buenas  pruebas  unitarias     15

Calidad  del  código  de  prueba     15

Denominación  de  la  prueba  unitaria        15

viii

www.allitebooks.com
Machine Translated by Google

■  Contenido

Prueba  unitaria  Independencia           16

Una  aserción  por  prueba        17

Inicialización  independiente  de  entornos  de  prueba  unitaria     18

Excluir  getters  y  setters        18

Excluir  código  de  terceros           18

Excluir  sistemas  externos           19

¿Y  qué  hacemos  con  la  base  de  datos?        19

No  mezcle  el  código  de  prueba  con  el  código  de  producción     19

Las  pruebas  deben  ejecutarse  rápido        22

Dobles  de  prueba  (objetos  falsos)           22

■Capítulo  3:  Tenga  principios      27

¿Qué  es  un  principio?..............................................................................................................        27

BESO         28

YAGNI         28

SECO         29

Ocultación  de  información        29

Cohesión  fuerte        32

Acoplamiento  flojo        35

Tenga  cuidado  con  las  optimizaciones     39

Principio  de  menor  asombro  (PLA)      39

La  Regla  de  Boy  Scout           39

■Capítulo  4:  Conceptos  básicos  de  Clean  C++     41

Buenos  nombres        41

Los  nombres  deben  explicarse  por  sí  mismos        43

Usar  nombres  del  dominio        44

Elija  nombres  en  un  nivel  apropiado  de  abstracción     45

Evite  la  redundancia  al  elegir  un  nombre     46

Evite  las  abreviaturas  crípticas        46

Evite  la  notación  y  los  prefijos  húngaros        47

Evite  usar  el  mismo  nombre  para  diferentes  propósitos     48

viii

www.allitebooks.com
Machine Translated by Google

■  Contenido

Comentarios           48

Deje  que  el  código  cuente  una  historia        49

No  Comentar  Cosas  Obvias        49

No  deshabilite  el  código  con  comentarios           50

No  escribir  comentarios  en  bloque        50

Los  raros  casos  en  los  que  los  comentarios  son  útiles     53

Funciones         56

¡Una  cosa,  no  más!           58

Que  sean  pequeños        59

Denominación  de  funciones           60

Usar  nombres  que  revelen  la  intención        61

Argumentos  y  valores  de  retorno           61

Acerca  del  antiguo  estilo  C  en  proyectos  C++        72

Preferir  C++  Strings  and  Streams  sobre  el  antiguo  estilo  C  char*      72

Evite  el  uso  de  printf(),  sprintf(),  gets(),  etc.        74

Preferir  contenedores  de  biblioteca  estándar  en  lugar  de  arreglos  de  estilo  C  simples.     77

Usar  moldes  de  C++  en  lugar  de  moldes  antiguos  de  estilo  C     80

Evite  las  macros        82

■Capítulo  5:  Conceptos  avanzados  de  C++  moderno      85

Gestión  de  recursos           85

La  adquisición  de  recursos  es  inicialización  (RAII)     87

Punteros  inteligentes.        87

Evitar  nuevos  y  borrados  explícitos        93

Gestión  de  recursos  propios           93

Nos  gusta  moverlo        94

¿Qué  son  las  semánticas  de  movimiento?        95

El  asunto  con  esos  valores  l  y  valores  r        96

rvalue  Referencias         97

No  haga  cumplir  la  mudanza  en  todas  partes           98

La  regla  del  cero        99

ix

www.allitebooks.com
Machine Translated by Google

■  Contenido

El  compilador  es  su  colega     102

Tipo  Deducción  Automática           103

Cálculos  durante  el  tiempo  de  compilación        106

Plantillas  variables           108

No  permitir  un  comportamiento  indefinido     109

Programación  rica  en  tipos     110

Conozca  sus  bibliotecas        116

Aproveche  el  <algoritmo>         117

Aprovecha  Boost           122

Más  bibliotecas  que  debe  conocer      123

Manejo  adecuado  de  excepciones  y  errores     123

La  prevención  es  mejor  que  el  cuidado  posterior        124

Una  excepción  es  una  excepción,  ¡literalmente!     128

Si  no  puede  recuperarse,  salga  rápidamente        129

Definir  tipos  de  excepción  específicos  del  usuario           129

Lanzamiento  por  valor,  captura  por  const  Referencia        131

Preste  Atención  al  Orden  Correcto  de  las  Cláusulas  Captura     131

■Capítulo  6:  Orientación  a  objetos        133

Pensamiento  Orientado  a  Objetos     133

Abstracción:  la  clave  para  dominar  la  complejidad     135

Principios  para  un  buen  diseño  de  clase     135

Mantenga  las  clases  pequeñas           136

Principio  de  responsabilidad  única  (SRP)         137

Principio  Abierto­Cerrado  (OCP)           137

Principio  de  sustitución  de  Liskov  (LSP)         138

Principio  de  segregación  de  interfaz  (ISP)         149

Principio  de  dependencia  acíclica        150

Principio  de  inversión  de  dependencia  (DIP)      153

No  hables  con  extraños  (Ley  de  Deméter)……………………………………………………………………        158

Evite  las  clases  anémicas        163

www.allitebooks.com
Machine Translated by Google

■  Contenido

¡Di,  no  preguntes!        163

Evitar  miembros  de  clase  estáticos        165

■Capítulo  7:  Programación  funcional        167

¿Qué  es  la  programación  funcional?        168

¿Qué  es  una  función?        169

Funciones  puras  vs.  impuras        170

Programación  funcional  en  C++  moderno     171

Programación  Funcional  con  Plantillas  C++     171

Objetos  similares  a  funciones  (funtores)         173

Carpetas  y  envoltorios  de  funciones        179

Expresiones  lambda        181

Expresiones  lambda  genéricas  (C++14)         183

Funciones  de  orden  superior        184

Mapear,  filtrar  y  reducir           185

Código  Limpio  en  Programación  Funcional     189

■Capítulo  8:  Desarrollo  dirigido  por  pruebas     191

Los  inconvenientes  de  las  pruebas  unitarias  simples  (POUT)     192

El  desarrollo  basado  en  pruebas  como  elemento  de  cambio     193

El  flujo  de  trabajo  de  TDD        193

TDD  por  ejemplo:  el  código  de  números  romanos  Kata        196

Las  ventajas  de  TDD        213

Cuándo  no  debemos  usar  TDD      215

■Capítulo  9:  Patrones  de  diseño  y  expresiones  idiomáticas        217

Principios  de  diseño  frente  a  patrones  de  diseño     217

Algunos  patrones  y  cuándo  usarlos     218

Inyección  de  dependencia  (DI)           218

Adaptador         230

Estrategia           231

Comando           235

Procesador  de  comandos           239

xi

www.allitebooks.com
Machine Translated by Google

■  Contenido

Compuesto         242

Observador         245

Fábricas         250

Fachada         253

Clase  de  dinero           254

Objeto  de  caso  especial  (Objeto  nulo)           257

¿Qué  es  un  modismo?        260

Algunas  expresiones  idiomáticas  útiles  de  C++        261

■Apéndice  A:  Guía  UML  pequeña        273

Diagramas  de  clases           273

Clase         273

Interfaz           275

Asociación           278

Generalización        280

Dependencia         281

Componentes           282

Estereotipos        283

■Bibliografía           285

Índice         287

xi
Machine Translated by Google

Sobre  el  Autor

Stephan  Roth,  nacido  el  15  de  mayo  de  1968,  es  un  apasionado  entrenador,  consultor  y  
formador  de  Ingeniería  de  Sistemas  y  Software  en  la  consultora  alemana  oose  
Innovative  Informatik  eG  ubicada  en  Hamburgo.
Antes  de  unirse  a  oose,  Stephan  trabajó  durante  muchos  años  como  desarrollador  de  
software,  arquitecto  de  software  e  ingeniero  de  sistemas  en  el  campo  de  los  sistemas  de  
inteligencia  de  comunicación  y  reconocimiento  de  radio.  Ha  desarrollado  aplicaciones  
sofisticadas,  especialmente  para  sistemas  distribuidos  con  requisitos  de  rendimiento  
ambiciosos,  e  interfaces  gráficas  de  usuario  utilizando  C++  y  otros  lenguajes  de  programación.  
Stephan  también  es  ponente  en  congresos  profesionales  y  autor  de  varias  publicaciones.  
Como  miembro  de  la  Gesellschaft  für  Systems  Engineering  eV,  la  alemana

capítulo  de  la  organización  internacional  de  Ingeniería  de  Sistemas  INCOSE,  también  participa  en  la  comunidad  de  Ingeniería  de  
Sistemas.  Además,  es  un  partidario  activo  del  movimiento  Software  Craftsmanship  y  se  preocupa  por  los  principios  y  prácticas  de  Clean  
Code  Development  (CCD).
Stephan  Roth  vive  con  su  esposa  Caroline  y  su  hijo  Maximilian  en  Bad  Schwartau,  un  balneario  en  el
Estado  federal  alemán  de  Schleswig­Holstein,  cerca  del  Mar  Báltico.
Puede  visitar  el  sitio  web  y  el  blog  de  Stephan  sobre  Ingeniería  de  Sistemas,  Ingeniería  de  Software  y  Software
Artesanía  a  través  de  la  URL  roth­soft.de.  Tenga  en  cuenta  que  los  artículos  están  escritos  principalmente  en  alemán.
Además  de  eso,  puede  contactarlo  por  correo  electrónico  o  seguirlo  en  las  redes  que  se  enumeran  a  continuación.
Correo  electrónico:  stephan@clean­
cpp.com  Twitter:  @_StephanRoth  (https://twitter.com/_StephanRoth)
Página  de  perfil  de  Google+:  http://gplus.to/sro  LinkedIn:  
http://www.linkedin.com/pub/stephan­roth/79/3a1/514

XIII
Machine Translated by Google

Acerca  del  revisor  técnico

Marc  Gregoire  es  un  ingeniero  de  software  de  Bélgica.  Se  graduó  de  la  Universidad  de  Lovaina,  Bélgica,  con  un  título  en  
"Burgerlijk  ingenieur  in  de  computer  wetenschappen" (equivalente  a  Master  of  Science  en  ingeniería  en  informática).  
Al  año  siguiente,  obtuvo  una  maestría,  cum  laude,  en  inteligencia  artificial  en  la  misma  universidad.  Después  de  
completar  sus  estudios,  Marc  comenzó  a  trabajar  para  una  empresa  de  consultoría  de  software  llamada  Ordina  Bélgica.  
Como  consultor,  trabajó  para  Siemens  y  Nokia  Siemens  Networks  en  software  crítico  2G  y  3G  que  se  ejecuta  en  Solaris  
para  operadores  de  telecomunicaciones.  Esto  requería  trabajar  en  equipos  internacionales  que  se  extendían  desde  
América  del  Sur  y  los  Estados  Unidos  hasta  EMEA  y  Asia.  Marc  ahora  trabaja  para  Nikon  Metrology  en  software  
industrial  de  escaneo  láser  3D.
Su  principal  experiencia  es  C/C++,  y  específicamente  Microsoft  VC++  y  el  framework  MFC.  Tiene  
experiencia  en  el  desarrollo  de  programas  C++  que  se  ejecutan  24x7  en  plataformas  Windows  y  Linux:  por  ejemplo,  el  
software  de  automatización  del  hogar  KNX/EIB.  Además  de  C/C++,  a  Marc  también  le  gusta  C#  y  usa  PHP  para  crear  páginas  web.
Desde  abril  de  2007,  ha  recibido  el  premio  anual  Microsoft  MVP  (Most  Valuable  Professional)  por  su  experiencia  en  
Visual  C++.
Marc  es  el  fundador  del  grupo  belga  de  usuarios  de  C++  (www.becpp.org),  autor  de  Professional  C++  y  un
miembro  del  foro  CodeGuru  (como  Marc  G).  Mantiene  un  blog  en  www.nuonsoft.com/blog/.

XV
Machine Translated by Google

Expresiones  de  gratitud

Escribir  un  libro  como  este  nunca  es  solo  el  trabajo  de  una  persona  individual,  el  autor.  Siempre  hay  numerosas  y  fabulosas  personas  
que  contribuyen  significativamente  a  un  proyecto  tan  grande.
En  primer  lugar,  me  gustaría  agradecer  a  Steve  Anglin  de  Apress.  Steve  se  puso  en  contacto  conmigo  en  marzo  de  2016  y  me  
convenció  para  que  continuara  con  mi  proyecto  de  libro  con  Apress  Media  LLC,  que  hasta  entonces  había  sido  autoeditado  en  
Leanpub.  Fue  una  gran  suerte  y  te  lo  agradezco,  querido  Steve.  En  julio  de  2016  se  firmaron  los  contratos.  No  obstante,  también  me  
gustaría  agradecer  a  la  excelente  plataforma  de  autoedición  Leanpub  que  sirvió  algunos  años  como  una  especie  de  "incubadora"  para  
este  libro.
A  continuación,  me  gustaría  agradecer  a  Mark  Powers,  Gerente  de  Operaciones  Editoriales  de  Apress,  por  su  gran  apoyo  durante  
la  redacción  del  manuscrito.  Mark  no  solo  estuvo  siempre  disponible  para  responder  preguntas:  su  incesante  seguimiento  del  progreso  del  
manuscrito  fue  un  incentivo  positivo  para  mí.  Te  estoy  muy  agradecida,  querido  Mark.  Además,  muchas  gracias  también  a  Matthew  
Moodie,  editor  principal  de  desarrollo  de  Apress,  quien  brindó  la  ayuda  adecuada  durante  todo  el  proceso  de  desarrollo  del  libro.

Un  agradecimiento  especial  para  mi  revisor  técnico  Marc  Gregoire.  Marc,  gracias  por  examinar  críticamente  cada  capítulo.  Has  
encontrado  muchos  problemas  que  probablemente  yo  nunca  hubiera  encontrado.  Me  presionaste  mucho  para  mejorar  varias  
secciones,  y  eso  fue  muy  valioso  para  mí.
Por  supuesto,  también  me  gustaría  dar  las  gracias  a  todo  el  equipo  de  producción  de  Apress.  han  hecho
un  excelente  trabajo  con  respecto  a  la  finalización  (edición  de  copia,  indexación,  composición/diseño,  etc.)  de  todo  el  libro  hasta  la  
distribución  de  los  archivos  finales  de  impresión  (y  libro  electrónico).
Por  supuesto,  también  doy  las  gracias  a  todos  mis  compañeros  de  oose.  Gracias  por  las  muchas  discusiones  inspiradoras.
Por  último,  pero  no  menos  importante,  me  gustaría  agradecer  a  mi  querida  y  única  familia,  especialmente  por  
comprender  que  el  proyecto  de  un  libro  requiere  mucho  tiempo.  Maximilian  y  Caroline,  sois  maravillosos.

xvii
Machine Translated by Google

CAPÍTULO  1

Introducción

Cómo  se  hace  es  tan  importante  como  que  se  haga.
—Eduardo  Namur

Todavía  es  una  triste  realidad  que  muchos  proyectos  de  desarrollo  de  software  se  encuentran  en  malas  condiciones,  y  algunos  
incluso  podrían  estar  en  una  grave  crisis.  Las  razones  para  esto  son  múltiples.  Algunos  proyectos,  por  ejemplo,  se  ven  afectados  
por  una  pésima  gestión  de  proyectos.  En  otros  proyectos,  las  condiciones  y  los  requisitos  cambian  constantemente,  pero  el  
proceso  no  es  compatible  con  este  entorno  de  alta  dinámica.
En  algunos  proyectos  hay  razones  puramente  técnicas:  su  código  es  de  mala  calidad.  Eso  no  significa  
necesariamente  que  el  código  no  esté  funcionando  correctamente.  Su  calidad  externa,  medida  por  el  departamento  de  
control  de  calidad  mediante  pruebas  de  caja  negra,  de  usuario  o  de  aceptación,  puede  ser  bastante  alta.  Puede  pasar  el  
control  de  calidad  sin  quejas,  y  el  informe  de  prueba  dice  que  no  encuentran  nada  malo.  También  los  usuarios  del  software  
pueden  estar  satisfechos  y  contentos,  y  su  desarrollo  se  ha  completado  a  tiempo  y  dentro  del  presupuesto  (...  lo  cual  es  raro,  lo  sé).
Todo  parece  estar  bien...  ¿realmente  todo?
Sin  embargo,  la  calidad  interna  de  este  código,  que  podría  funcionar  correctamente,  puede  ser  muy  pobre.  A  menudo  el
el  código  es  difícil  de  entender  y  horrible  de  mantener  y  extender.  Innumerables  unidades  de  software,  como  clases  o  
funciones,  son  muy  grandes,  algunas  de  ellas  con  miles  de  líneas  de  código.  Demasiadas  dependencias  entre  unidades  de  
software  provocan  efectos  secundarios  no  deseados  si  se  cambia  algo.  El  software  no  tiene  una  arquitectura  perceptible.  
Su  estructura  parece  tener  un  origen  aleatorio  y  algunos  desarrolladores  hablan  de  "software  desarrollado  históricamente"  o  
"arquitectura  por  accidente".  Las  clases,  funciones,  variables  y  constantes  tienen  nombres  malos  y  misteriosos,  y  el  código  
está  plagado  de  muchos  comentarios:  algunos  de  ellos  están  desactualizados,  solo  describen  cosas  obvias  o  simplemente  están  
equivocados.  Los  desarrolladores  tienen  miedo  de  cambiar  algo  o  de  ampliar  el  software  porque  saben  que  está  podrido  y  
es  frágil,  y  saben  que  la  cobertura  de  las  pruebas  unitarias  es  deficiente,  si  es  que  las  hay.  "Nunca  toque  un  sistema  en  
ejecución"  es  una  afirmación  que  se  escucha  con  frecuencia  en  este  tipo  de  proyectos.  La  implementación  de  una  nueva  función  
no  necesita  unos  días  hasta  que  esté  lista  para  su  implementación;  tarda  varias  semanas  o  incluso  meses.

Este  tipo  de  software  malo  a  menudo  se  conoce  como  Big  Ball  Of  Mud.  Este  término  fue  utilizado  por  primera  vez  en  1997  
por  Brian  Foote  y  Joseph  W.  Yoder  en  un  documento  para  la  Cuarta  Conferencia  sobre  Patrones  Lenguajes  de  Programas  
(PLoP  '97/EuroPLoP  '97).  Foote  y  Yoder  describen  la  Gran  Bola  de  Barro  como  "...  una  jungla  de  código  de  espagueti  
estructurada  al  azar,  en  expansión,  descuidada,  con  cinta  adhesiva  y  alambre  para  embalar".  Dichos  sistemas  de  software  son  
pesadillas  de  mantenimiento  costosas  y  que  consumen  mucho  tiempo,  ¡y  pueden  poner  de  rodillas  a  una  organización  de  desarrollo!
Los  fenómenos  patológicos  que  acabamos  de  describir  se  pueden  encontrar  en  proyectos  de  software  en  todos  los  
sectores  y  dominios  industriales.  El  lenguaje  de  programación  utilizado  no  importa.  Encontrarás  Big  Ball  Of  Muds  escrito  en  
Java,  PHP,  C,  C#,  C++,  o  cualquier  otro  lenguaje  más  o  menos  popular.  Pero  ¿por  qué  es  así?

©  Stephan  Roth  2017   1
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0_1
Machine Translated by Google

Capítulo  1  ■  Introducción

Entropía  del  software
En  primer  lugar,  hay  algo  que  parece  ser  como  una  ley  natural.  Al  igual  que  cualquier  otro  sistema  cerrado  y  complejo,  el  software  
tiende  a  ensuciarse  con  el  tiempo.  Este  fenómeno  se  llama  entropía  del  software.  El  término  se  basa  en  la  segunda  ley  de  la  
termodinámica.  Afirma  que  el  desorden  de  un  sistema  cerrado  no  se  puede  reducir;  solo  puede  permanecer  sin  cambios  o  
aumentar.  El  software  parece  comportarse  de  esta  manera.  Cada  vez  que  se  agrega  una  nueva  función  o  se  cambia  algo,  el  código  
se  vuelve  un  poco  más  desordenado.  También  existen  numerosos  factores  influyentes  que  podrían  reenviar  la  entropía  del  software,  
por  ejemplo,  los  siguientes:

•Calendarios  de  proyectos  poco  realistas  que  aumentarán  la  presión  y,  por  lo  tanto,  obligarán  a  los  
desarrolladores  a  estropear  las  cosas  ya  hacer  su  trabajo  de  una  manera  mala  y  poco  profesional.

•Inmensa  complejidad  de  los  sistemas  de  software  en  la  actualidad.

•Los  desarrolladores  tienen  diferentes  niveles  de  habilidad  y  experiencia.

•Equipos  multiculturales  distribuidos  globalmente,  lo  que  impone  problemas  de  comunicación.

•El  desarrollo  presta  atención  principalmente  a  los  aspectos  funcionales  (requisitos  funcionales  y  casos  
de  uso  del  sistema)  del  software,  por  lo  que  los  requisitos  de  calidad  (también  conocidos  como  
requisitos  no  funcionales),  como  la  eficiencia  del  rendimiento,  la  mantenibilidad,  la  usabilidad,  la  
portabilidad,  la  seguridad,  etc.,  son  descuidados  o,  en  el  peor  de  los  casos,  están  siendo  
completamente  olvidados.

•Entorno  de  desarrollo  inapropiado  y  malas  herramientas.

•La  gerencia  se  enfoca  en  ganar  dinero  rápido  y  no  comprende  el  valor  del  desarrollo  de  software  sostenible.

•Hacks  rápidos  y  sucios  e  implementaciones  que  no  se  ajustan  al  diseño  (también  conocido  como  Broken
ventanas).

LA  TEORÍA  DE  LA  VENTANA  ROTA

La  teoría  de  la  ventana  rota  se  desarrolló  en  la  investigación  criminal  estadounidense.  La  teoría  establece  
que  una  sola  ventana  destruida  en  un  edificio  abandonado  puede  ser  el  desencadenante  del  deterioro  de  
todo  un  vecindario.  La  ventana  rota  envía  una  señal  fatal  al  medio  ambiente:  “¡Mira,  a  nadie  le  
importa  este  edificio!”  Esto  atrae  más  deterioro,  vandalismo  y  otros  comportamientos  antisociales.  La  
Teoría  de  la  Ventana  Rota  se  ha  utilizado  como  base  para  varias  reformas  en  la  política  criminal,  
especialmente  para  el  desarrollo  de  estrategias  de  Tolerancia  Cero.

En  el  desarrollo  de  software,  esta  teoría  fue  retomada  y  aplicada  a  la  calidad  del  código.  Los  hacks  y  las  
malas  implementaciones,  que  no  cumplen  con  el  diseño  del  software,  se  denominan  "ventanas  rotas".
Si  estas  malas  implementaciones  no  se  reparan,  pueden  aparecer  más  hacks  para  tratar  con  ellos  en  
su  vecindario.  Y  así,  la  dilapidación  del  código  se  pone  en  marcha.

No  tolere  "ventanas  rotas"  en  su  código:  ¡ arréglelas!

Sin  embargo,  parece  ser  que  los  proyectos  particulares  de  C  y  C++  son  propensos  al  desorden  y  tienden  más  que  otros  a  
caer  en  mal  estado.  Incluso  la  World  Wide  Web  está  llena  de  ejemplos  de  código  C++  malos,  pero  aparentemente  muy  rápidos  y  
altamente  optimizados,  con  una  sintaxis  cruel  e  ignorando  por  completo  los  principios  elementales  para  un  buen  diseño  y  un  código  
bien  escrito.

2
Machine Translated by Google

Capítulo  1  ■  Introducción

Una  razón  para  esto  podría  ser  que  C  ++  es  un  lenguaje  de  programación  de  múltiples  paradigmas  en  un  nivel  intermedio,  
es  decir,  comprende  características  de  lenguaje  de  alto  y  bajo  nivel.  Con  C++,  puede  escribir  sistemas  de  software  empresarial  
grandes  y  distribuidos  con  interfaces  de  usuario  sofisticadas,  así  como  software  para  pequeños  sistemas  integrados  con  
comportamiento  en  tiempo  real,  vinculado  muy  de  cerca  al  hardware  subyacente.  El  lenguaje  multiparadigma  significa  que  puede  
escribir  programas  procedimentales,  funcionales  u  orientados  a  objetos,  o  incluso  una  combinación  de  los  tres  paradigmas.  
Además,  C  ++  permite  la  metaprogramación  de  plantillas  (TMP),  una  técnica  en  la  que  un  compilador  utiliza  las  llamadas  plantillas  
para  generar  un  código  fuente  temporal,  que  el  compilador  fusiona  con  el  resto  del  código  fuente  y  luego  compila.  Y  desde  el  
lanzamiento  del  estándar  ISO  C  ++  11,  se  han  agregado  aún  más  formas,  por  ejemplo,  la  programación  funcional  con  funciones  
anónimas  ahora  es  compatible  de  una  manera  muy  elegante  con  expresiones  lambda.  Como  consecuencia  de  estas  capacidades  
diversas,  C++  tiene  la  reputación  de  ser  muy  complejo,  complicado  y  engorroso.

Otra  causa  del  mal  software  podría  ser  que  muchos  desarrolladores  no  tenían  experiencia  en  TI.
Cualquiera  puede  comenzar  a  desarrollar  software  hoy  en  día,  sin  importar  si  tiene  un  título  universitario  o  cualquier  otro  
aprendizaje  en  informática.  La  gran  mayoría  de  los  desarrolladores  de  C++  son  (o  eran)  no  expertos.  Especialmente  en  los  dominios  
tecnológicos  automotriz,  transporte  ferroviario,  aeroespacial,  eléctrico/electrónico  o  ingeniería  mecánica,  muchos  ingenieros  
se  deslizaron  hacia  la  programación  durante  las  últimas  décadas  sin  tener  una  educación  en  informática.  A  medida  que  la  complejidad  
crecía  y  los  sistemas  técnicos  contenían  más  y  más  software,  había  una  necesidad  urgente  de  programadores.  Esta  demanda  fue  
cubierta  por  la  mano  de  obra  existente.
Ingenieros  eléctricos,  matemáticos,  físicos  y  también  mucha  gente  de  disciplinas  estrictamente  no  técnicas  comenzaron  
a  desarrollar  software  y  a  aprenderlo  principalmente  de  manera  autodidacta  y  práctica  simplemente  haciéndolo.  Y  lo  han  hecho  a  
su  leal  saber  y  entender.
Básicamente,  no  hay  absolutamente  nada  de  malo  en  ello.  ¡Pero  a  veces  solo  conocer  las  herramientas  y  el  lenguaje  
de  programación  no  es  suficiente!  Desarrollo  de  software  no  es  lo  mismo  que  programación.  El  mundo  está  lleno  de  software  que  
fue  manipulado  por  desarrolladores  de  software  mal  capacitados.  Hay  muchas  cosas  en  niveles  abstractos  que  un  desarrollador  debe  
considerar  para  crear  un  sistema  sostenible,  por  ejemplo,  arquitectura  y  diseño.
¿Cómo  debe  estructurarse  un  sistema  para  lograr  ciertos  objetivos  de  calidad?  ¿Para  qué  sirve  esta  cosa  orientada  a  objetos  y  cómo  
la  uso  de  manera  eficiente?  ¿Cuáles  son  las  ventajas  y  desventajas  de  un  determinado  marco  o  biblioteca?  ¿Cuáles  son  las  
diferencias  entre  varios  algoritmos  y  por  qué  un  algoritmo  no  se  ajusta  a  todos  los  problemas  similares?  ¿Y  qué  diablos  es  un  
autómata  finito  determinista,  y  por  qué  ayuda  a  hacer  frente  a  la  complejidad?
¡Pero  no  hay  razón  para  desanimarse!  Lo  que  realmente  importa  para  la  salud  continua  de  un  software  es  que  alguien  se  
preocupe  por  él,  ¡y  el  código  limpio  es  la  clave!

código  limpio
Un  gran  malentendido  es  confundir  el  código  limpio  con  algo  que  se  puede  llamar  "código  hermoso".
El  código  limpio  no  tiene  razones  de  belleza.  A  los  programadores  profesionales  no  se  les  paga  por  escribir  código  hermoso  o  
bonito.  Son  contratados  por  empresas  de  desarrollo  como  expertos  para  crear  valor  para  el  cliente.
El  código  está  limpio  si  cualquier  miembro  del  equipo  puede  entenderlo  y  mantenerlo  fácilmente.
El  código  limpio  es  la  base  para  ser  rápido.  Si  su  código  está  limpio  y  la  cobertura  de  prueba  es  buena,  un  cambio  o  una  
nueva  función  solo  tomará  unas  pocas  horas  o  un  par  de  días,  y  no  semanas  o  meses,  hasta  que  se  implemente,  pruebe  e  implemente.

El  código  limpio  es  la  base  para  un  software  sostenible  y  mantiene  un  proyecto  de  desarrollo  de  software
funcionando  durante  mucho  tiempo  sin  acumular  una  gran  cantidad  de  deuda  técnica.  Los  desarrolladores  deben  cuidar  
activamente  el  software  y  asegurarse  de  que  se  mantenga  en  forma  porque  el  código  es  crucial  para  la  supervivencia  de  una  
organización  de  desarrollo  de  software.
El  código  limpio  también  es  clave  para  hacerte  un  desarrollador  más  feliz.  Conduce  a  una  vida  libre  de  estrés.  Si  su  
código  está  limpio  y  se  siente  cómodo  con  él,  puede  mantener  la  calma  en  todas  las  situaciones,  incluso  frente  a  un  plazo  de  
entrega  ajustado.
Todos  los  puntos  mencionados  anteriormente  son  ciertos,  pero  el  punto  clave  es  este:  ¡ El  código  limpio  ahorra  dinero!  
En  esencia,  se  trata  de  eficiencia  económica.  Cada  año,  las  organizaciones  de  desarrollo  pierden  mucho  dinero  porque  su  código  
está  en  mal  estado.

3
Machine Translated by Google

Capítulo  1  ■  Introducción

¿Por  qué  C++?

C  hace  que  sea  fácil  pegarse  un  tiro  en  el  pie.  C++  lo  hace  más  difícil,  pero  cuando  lo  haces,  ¡te  vuelas  toda  la  
pierna!

—Bjarne  Stroustrup,  Preguntas  frecuentes  de  Bjarne  Stroustrup:  ¿De  verdad  dijiste  eso?

Cada  lenguaje  de  programación  es  una  herramienta,  y  cada  uno  tiene  sus  fortalezas  y  debilidades.  Una  parte  importante  del  
trabajo  de  un  arquitecto  de  software  es  elegir  el  lenguaje  de  programación,  o  actualmente  el  conjunto  de  lenguajes  de  
programación,  que  se  adapte  perfectamente  al  proyecto.  Es  una  decisión  arquitectónica  importante  que  nunca  debe  tomarse  
sobre  la  base  de  una  intuición  o  preferencias  personales.  Del  mismo  modo,  un  principio  como  "En  nuestra  empresa  hacemos  todo  con  
<reemplace  esto  con  el  idioma  de  su  elección>"  podría  no  ser  una  buena  guía.
Como  lenguaje  de  programación  multiparadigma,  C++  es  un  crisol  que  combina  diferentes  ideas  y  conceptos.  El  lenguaje  
siempre  ha  sido  una  excelente  opción  cuando  se  trata  del  desarrollo  de  sistemas  operativos,  controladores  de  dispositivos,  sistemas  
integrados,  sistemas  de  administración  de  bases  de  datos,  juegos  de  computadora  ambiciosos,  animaciones  3D  y  diseño  
asistido  por  computadora,  procesamiento  de  audio  y  video  en  tiempo  real,  administración  de  big  data,  y  muchas  otras  aplicaciones  
críticas  para  el  rendimiento.  Hay  ciertos  dominios  en  los  que  C++  es  la  lingua  franca.  Grandes  bases  de  código  C++  con  miles  de  millones  
de  líneas  de  código  todavía  están  disponibles  y  en  uso.
Hace  unos  años,  una  opinión  muy  difundida  era  que  C++  es  difícil  de  aprender  y  usar.  El  lenguaje  puede  ser  complejo  y  
desalentador  para  los  programadores  que  a  menudo  tienen  la  tarea  de  escribir  programas  grandes  y  complejos.  Debido  a  esto,  los  
lenguajes  principalmente  interpretados  y  administrados,  como  Java  o  C#,  se  estaban  volviendo  populares.
Un  marketing  desmedido  por  parte  del  fabricante  de  estos  lenguajes  hizo  el  resto.  En  consecuencia,  los  lenguajes  administrados  
han  llegado  a  dominar  en  ciertos  dominios,  pero  los  lenguajes  compilados  de  forma  nativa  aún  dominan  en  otros.  Un  lenguaje  de  
programación  no  es  una  religión.  Si  no  necesita  el  rendimiento  de  C++,  pero  Java,  por  ejemplo,  facilita  su  trabajo,  entonces  utilícelo.

C++11  –  El  comienzo  de  una  nueva  era
Algunas  personas  dicen  que  C++  actualmente  está  experimentando  un  renacimiento.  Algunos  incluso  hablan  de  una  revolución.  
Dicen  que  el  C++  moderno  de  hoy  ya  no  es  comparable  con  el  “C++  histórico”  de  principios  de  los  90.  El  catalizador  de  esta  tendencia  
fue  principalmente  la  aparición  del  estándar  C++  ISO/IEC  14882:2011  [ISO11],  más  conocido  como  C++11,  en  septiembre  de  2011.

Sin  duda,  C++11  ha  traído  grandes  innovaciones.  Parecía  que  la  publicación  de  esta  norma
ha  puesto  algunas  cosas  en  marcha.  Y  mientras  este  libro  está  en  producción,  el  comité  de  estandarización  de  C++  ha  completado  
su  trabajo  en  el  nuevo  estándar  C++17,  que  ahora  se  encuentra  en  su  proceso  final  de  votación  ISO.
Además,  C++20  ya  está  comenzando.
Actualmente  están  sucediendo  muchas  cosas  en  el  espacio  de  desarrollo  nativo,  especialmente  en  las  empresas.
de  la  industria  manufacturera,  porque  el  software  se  ha  convertido  en  el  factor  de  valor  agregado  más  importante  para  los  sistemas  
técnicos.  Las  herramientas  de  desarrollo  para  C++  son  mucho  más  poderosas  hoy  en  día  y  hay  disponibles  una  multitud  de  bibliotecas  
y  marcos  útiles.  Pero  no  necesariamente  llamaría  a  todo  este  desarrollo  una  revolución.
Creo  que  es  la  evolución  habitual.  Además,  los  lenguajes  de  programación  deben  mejorarse  y  adaptarse  continuamente  para  
cumplir  con  los  nuevos  requisitos,  y  C  ++  98  respectivamente  C  ++  03  (que  era  principalmente  una  versión  de  corrección  de  errores  en  C  
++  98)  era  un  poco  largo  en  el  diente.

4
Machine Translated by Google

Capítulo  1  ■  Introducción

Para  quien  es  este  libro
Como  formador  y  consultor,  tengo  la  oportunidad  de  echar  un  vistazo  a  muchas  empresas  que  están  desarrollando  software.  Además,  observo  muy  de  
cerca  lo  que  sucede  en  la  escena  de  los  desarrolladores.  y  he  reconocido
un  hueco.

Mi  impresión  es  que  los  programadores  de  C++  han  sido  ignorados  por  aquellos  que  promueven  la  artesanía  del  software.
y  desarrollo  de  código  limpio.  Muchos  principios  y  prácticas,  que  son  relativamente  bien  conocidos  en  el  entorno  de  Java  y  en  el  moderno  mundo  del  
desarrollo  web  o  de  juegos,  parecen  ser  en  gran  parte  desconocidos  en  el  mundo  de  C++.  Libros  pioneros,  como  The  Pragmatic  Programmer  [Hunt99]  
de  Andrew  Hunt  y  David  Thomas,  o  Clean  Code  [Martin09]  de  Robert  C.  Martin ,  a  menudo  ni  siquiera  son  conocidos.

Este  libro  trata  de  cerrar  un  poco  esa  brecha,  porque  incluso  con  C++,  ¡el  código  se  puede  escribir  limpio!  Si  desea  aprender  a  escribir  en  C++  
limpio,  este  libro  es  para  usted.
¡Este  libro  no  es  un  manual  básico  de  C++!  Ya  debe  estar  familiarizado  con  los  conceptos  básicos  del  idioma  para  usar  el  conocimiento  de  este  
libro  de  manera  eficiente.  Si  solo  desea  comenzar  con  el  desarrollo  de  C  ++  y  aún  no  tiene  conocimientos  básicos  del  lenguaje,  primero  debe  aprenderlos,  
lo  que  se  puede  hacer  con  otros  libros  o  con  una  buena  capacitación  de  introducción  a  C  ++.

Además,  este  libro  no  contiene  ningún  truco  o  truco  esotérico.  Sé  que  un  montón  de  chiflados  y  mente
Es  posible  explotar  cosas  con  C++,  pero  generalmente  no  tienen  el  espíritu  del  código  limpio  y  solo  rara  vez  se  deben  usar  para  un  programa  C++  
limpio  y  moderno.  Si  estás  realmente  loco  por  la  misteriosa  calistenia  con  punteros  de  C++,  este  libro  no  es  para  ti.

Para  ver  algunos  ejemplos  de  código  en  este  libro,  varias  características  de  lenguaje  de  los  estándares  C++11  (ISO/IEC  14882:2011),  C+
+14  (ISO/IEC  14882:2014)  y  también  algunas  de  las  últimas  versiones  de  C++.  Se  utilizan  17.  Si  no  está  familiarizado  con  estas  funciones,  no  
se  preocupe.  Proporcionaré  breves  introducciones  sobre  algunos  de  ellos  con  la  ayuda  de  las  barras  laterales.  Tenga  en  cuenta  que,  en  realidad,  no  todos  
los  compiladores  de  C++  son  compatibles  con  todas  las  características  del  lenguaje  nuevo  por  completo.
Aparte  de  eso,  este  libro  está  escrito  para  ayudar  a  los  desarrolladores  de  C++  de  todos  los  niveles  y  muestra  con  ejemplos  cómo  escribir  código  
C++  comprensible,  flexible,  fácil  de  mantener  y  eficiente.  Incluso  si  usted  es  un  desarrollador  de  C++  experimentado,  hay  algunos  puntos  de  información  
y  datos  en  este  libro  que  creo  que  encontrará  útiles  en  su  trabajo.  Los  principios  y  prácticas  presentados  se  pueden  aplicar  tanto  a  nuevos  sistemas  de  
software,  a  veces  llamados  proyectos  greenfield;  así  como  sistemas  heredados  con  una  larga  historia,  que  a  menudo  se  denominan  proyectos  brownfield  
de  forma  peyorativa.

Las  convenciones  usadas  en  este  libro
En  este  libro  se  utilizan  las  siguientes  convenciones  tipográficas:

La  fuente  en  cursiva  se  utiliza  para  introducir  nuevos  términos  y  nombres.

La  fuente  en  negrita  se  usa  dentro  de  los  párrafos  para  enfatizar  términos  o  información  importante.
declaraciones.

La  fuente  monoespaciada  se  usa  dentro  de  los  párrafos  para  referirse  a  elementos  del  programa,  como  nombres  
de  clases,  variables  o  funciones,  declaraciones  y  palabras  clave  de  C++.  Esta  fuente  también  se  usa  para  mostrar  
entradas  de  línea  de  comando,  una  dirección  de  un  sitio  web  (URL),  una  secuencia  de  pulsaciones  de  teclas  o  
la  salida  producida  por  un  programa.

Barras  laterales

A  veces,  les  paso  pequeños  fragmentos  de  información  que  están  tangencialmente  relacionados  con  el  contenido  que  los  rodea,  que  podrían  considerarse  
separados  de  ese  contenido.  Estas  secciones  se  conocen  como  barras  laterales.  A  veces  uso  una  barra  lateral  para  presentar  una  discusión  adicional  o  
contrastante  sobre  el  tema  que  la  rodea.  Ejemplo:

5
Machine Translated by Google

Capítulo  1  ■  Introducción

ESTE  ENCABEZADO  CONTIENE  EL  TÍTULO  DE  UNA  BARRA  LATERAL

Este  es  el  texto  en  una  barra  lateral.

Notas,  consejos  y  advertencias  Otro  tipo  de  barra  

lateral  para  fines  especiales  se  utiliza  para  notas,  consejos  y  advertencias.  Se  utilizan  para  brindarle  información  especial,  para  
brindarle  un  consejo  útil  o  para  advertirle  sobre  cosas  que  pueden  ser  peligrosas  y  deben  evitarse.  Ejemplo:

■  Nota  Este  es  el  texto  de  la  nota.

Ejemplos  de  código  Los  

ejemplos  de  código  y  los  fragmentos  de  código  aparecerán  separados  del  texto,  resaltados  por  la  sintaxis  (las  palabras  clave  del  
lenguaje  C++  están  en  negrita)  y  en  una  fuente  monoespaciada.  Las  secciones  de  código  más  largas  suelen  tener  títulos.  Para  
hacer  referencia  a  líneas  específicas  del  ejemplo  de  código  en  el  texto,  los  ejemplos  de  código  a  veces  se  decoran  con  números  de  línea.

Listado  1­1.  Un  ejemplo  de  código  de  línea  numerada

01  clase  Clazz  { 02  
público:
03 Clazz();  
04  virtual  ~Clazz();  void  
hacerAlgo();  05  06

07  privado:  08  
int  _atributo;
09
10  función  vacía  ();  11};

Para  centrarse  mejor  en  aspectos  específicos  del  código,  las  partes  irrelevantes  a  veces  se  oscurecen  y  representan
por  un  comentario  con  puntos  suspensivos  (…),  como  en  este  ejemplo:

void  Clazz::función()  { // ...

Estilo  de  codificación

Solo  unas  pocas  palabras  sobre  el  estilo  de  codificación  que  he  usado  en  este  libro.
Puede  tener  la  impresión  de  que  mi  estilo  de  programación  se  parece  mucho  al  código  típico  de  Java,  mezclado  con  
el  estilo  de  Kernighan  y  Ritchie  (K&R).  En  mis  casi  20  años  como  desarrollador  de  software,  e  incluso  más  adelante  en  
mi  carrera,  todavía  he  aprendido  otros  lenguajes  de  programación  además  de  C++,  por  ejemplo,  ANSI­C,  Java,  Delphi,  
Scala  y  varios  lenguajes  de  secuencias  de  comandos.  Por  lo  tanto,  adopté  mi  propio  estilo  de  programación,  que  es  
un  crisol  de  diferentes  influencias.

6
Machine Translated by Google

Capítulo  1  ■  Introducción

Tal  vez  no  te  guste  mi  estilo,  y  prefieras  el  estilo  Kernel  de  Linus  Torvald,  el  estilo  Allman,  o  cualquier  otro.
otro  popular  estándar  de  codificación  C++.  Por  supuesto,  esto  está  perfectamente  bien.  Me  gusta  mi  estilo  y  a  ti  te  gusta  el  tuyo.

Sitio  web  complementario  y  repositorio  de  código  fuente
Este  libro  va  acompañado  de  un  sitio  web  complementario:  www.clean­cpp.com.
El  sitio  web  incluye:

•Un  foro  de  discusión  donde  los  lectores  pueden  discutir  temas  específicos  con  otros  lectores  y,
por  supuesto,  con  el  autor.

•La  discusión  de  temas  adicionales  que  quizás  aún  no  hayan  sido  cubiertos  en  este  libro.

•Versión  en  alta  resolución  de  todas  las  figuras  de  este  libro.

La  mayoría  de  los  ejemplos  de  código  fuente  de  este  libro  y  otras  adiciones  útiles  están  disponibles  en  GitHub  en:

https://github.com/clean­cpp

Puede  consultar  el  código  usando  Git  con  el  siguiente  comando:

$>  git  clonar  https://github.com/clean­cpp/book­samples.git

Puede  obtener  un  archivo .zip  del  código  en  https://github.com/clean­cpp/book­samples  y  haciendo  clic  en  el  botón  “Descargar  ZIP”.

Diagramas  UML
Algunas  ilustraciones  de  este  libro  son  diagramas  UML.  El  lenguaje  de  modelado  unificado  (UML)  es  un  lenguaje  gráfico  estandarizado  para  
crear  modelos  de  software  y  otros  sistemas.  En  su  versión  actual  2.5,  UML  ofrece  14  tipos  de  diagramas  para  describir  un  sistema  por  
completo.
No  se  preocupe  si  no  está  familiarizado  con  todos  los  tipos  de  diagramas;  Utilizo  en  este  libro  sólo  algunos  de  ellos.  estoy  presente
Diagramas  UML  de  vez  en  cuando  para  proporcionar  una  descripción  general  rápida  de  ciertos  problemas  que  posiblemente  no  se  
puedan  detectar  lo  suficientemente  rápido  con  solo  leer  el  código.  En  el  Apéndice  A  encontrará  una  breve  descripción  de  las  notaciones  utilizadas.

7
Machine Translated by Google

CAPITULO  2

Construya  una  red  de  seguridad

Probar  es  una  habilidad.  Si  bien  esto  puede  sorprender  a  algunas  personas,  es  un  hecho  simple.

—Mark  Fewster  y  Dorothy  Graham,  Automatización  de  pruebas  de  software,  1999

Que  comience  la  parte  principal  de  este  libro  con  un  capítulo  sobre  pruebas  puede  sorprender  a  algunos  lectores,  pero  esto  
se  debe  a  varias  buenas  razones.  Durante  los  últimos  años,  las  pruebas  en  ciertos  niveles  se  han  convertido  en  una  piedra  
angular  esencial  del  desarrollo  de  software  moderno.  Los  beneficios  potenciales  de  una  buena  estrategia  de  prueba  son  enormes.  
Todo  tipo  de  pruebas,  si  están  bien  diseñadas,  pueden  ser  útiles  y  útiles.  En  este  capítulo  describiré  por  qué  pienso  que  las  
Pruebas  Unitarias,  en  especial,  son  indispensables  para  asegurar  un  nivel  fundamental  de  excelente  calidad  en  el  software.
Tenga  en  cuenta  que  este  capítulo  trata  sobre  lo  que  a  veces  se  denomina  POUT  ("Pruebas  unitarias  sencillas")  y  no
la  herramienta  de  apoyo  al  diseño  Test­Driven  Development  (TDD),  de  la  que  hablaré  más  adelante  en  este  libro.

La  necesidad  de  las  pruebas

1962:  NASA  MARINER  1

La  nave  espacial  Mariner  1  se  lanzó  el  22  de  julio  de  1962  como  una  misión  de  sobrevuelo  de  Venus  para  la  exploración  
planetaria.  Debido  a  un  problema  con  su  antena  direccional,  el  cohete  de  lanzamiento  Atlas­Agena  B  funcionó  de  manera  poco  
confiable  y  perdió  su  señal  de  control  desde  el  control  de  tierra  poco  después  del  lanzamiento.

Este  caso  excepcional  se  había  considerado  durante  el  diseño  y  la  construcción  del  cohete.  El  vehículo  de  
lanzamiento  Atlas­Agena  cambió  a  control  automático  por  la  computadora  de  guía  a  bordo.
Desafortunadamente,  un  error  en  el  software  de  esta  computadora  condujo  a  comandos  de  control  incorrectos  que  causaron  
una  desviación  crítica  del  rumbo  e  imposibilitaron  el  gobierno.  El  cohete  se  dirigió  hacia  la  tierra  y  apuntó  a  un  área  crítica.

A  los  T+293  segundos,  el  oficial  de  seguridad  de  campo  envió  el  comando  de  destrucción  para  hacer  estallar  el  cohete.  Un  
informe  de  examen  de  la  NASA1  menciona  un  error  tipográfico  en  el  código  fuente  de  la  computadora,  la  falta  de  un  guión  
('­'),  como  la  causa  del  error.  La  pérdida  total  fue  de  $18,5  millones,  que  era  una  gran  cantidad  de  dinero  en  esos  días.

1
Centro  Nacional  de  Datos  de  Ciencias  Espaciales  de  la  NASA  (NSSDC):  Mariner  1,  http://nssdc.gsfc.nasa.gov/nmc/spacecraft  
Display.do?id=MARIN1,  consultado  el  28  de  abril  de  2014.

©  Stephan  Roth  2017   9
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0_2
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

Si  se  les  pregunta  a  los  desarrolladores  de  software  por  qué  las  pruebas  son  buenas  y  esenciales,  supongo  que  lo  más  común
La  respuesta  será  la  reducción  de  bugs,  errores  o  fallas.  Sin  duda,  esto  es  básicamente  correcto:  las  pruebas  son  una  parte  
elemental  de  la  garantía  de  calidad.
Los  errores  de  software  generalmente  se  perciben  como  una  molestia  desagradable.  Los  usuarios  están  molestos  por  el  mal
el  comportamiento  del  programa,  que  produce  una  salida  no  válida,  o  están  seriamente  molestos  por  los  bloqueos  regulares.  A  veces,  
incluso  las  cosas  raras,  como  un  texto  truncado  en  un  cuadro  de  diálogo  de  una  interfaz  de  usuario,  son  suficientes  para  molestar  
significativamente  a  los  usuarios  de  software  en  su  trabajo  diario.  La  consecuencia  puede  ser  una  creciente  insatisfacción  con  el  software  y,  
en  el  peor  de  los  casos,  su  sustitución  por  otro  producto.  Además  de  una  pérdida  financiera,  la  imagen  del  fabricante  del  software  sufre  errores.  
En  el  peor  de  los  casos,  la  empresa  se  mete  en  serios  problemas  y  se  pierden  muchos  puestos  de  trabajo.

Pero  el  escenario  descrito  anteriormente  no  se  aplica  a  todas  las  piezas  de  software.  Las  implicaciones  de  un
error  puede  ser  mucho  más  dramático.

1986:  DESASTRE  DEL  ACELERADOR  MÉDICO  THERAC­25

Este  caso  es  probablemente  el  fracaso  más  importante  en  la  historia  del  desarrollo  de  software.  El  
Therac­25  era  un  dispositivo  de  radioterapia.  Fue  desarrollado  y  producido  desde  1982  hasta  1985  por  la  empresa  
estatal  Atomic  Energy  of  Canada  Limited  (AECL).  Se  produjeron  e  instalaron  once  dispositivos  en  clínicas  de  
EE.  UU.  y  Canadá.

Debido  a  errores  en  el  software  de  control,  un  proceso  de  garantía  de  calidad  insuficiente  y  otras  deficiencias,  
tres  pacientes  perdieron  la  vida  a  causa  de  una  sobredosis  de  radiación.  Otros  tres  pacientes  fueron  irradiados  
y  se  llevaron  daños  permanentes  y  graves  para  la  salud.

Un  análisis  de  este  caso  tiene  como  resultado  que,  entre  otras  cosas,  el  software  fue  escrito  por  una  sola  
persona  que  también  era  responsable  de  las  pruebas.

Cuando  las  personas  piensan  en  computadoras,  generalmente  tienen  en  mente  una  PC  de  escritorio,  una  computadora  portátil,  una  
tableta  o  un  teléfono  inteligente.  Y  si  piensan  en  software,  generalmente  piensan  en  tiendas  web,  suites  ofimáticas  o  sistemas  de  TI  comerciales.
Pero  este  tipo  de  software  y  computadoras  representan  solo  un  porcentaje  muy  pequeño  de  todos  los  sistemas  con  los  que  tenemos  
contacto  todos  los  días.  La  mayor  parte  del  software  que  nos  rodea  controla  máquinas  que  interactúan  físicamente  con  el  mundo.  Toda  
nuestra  vida  está  gestionada  por  software.  En  pocas  palabras:  ¡ Hoy  no  hay  vida  sin  software!  El  software  está  en  todas  partes  y  es  una  parte  
esencial  de  nuestra  infraestructura.
Si  subimos  a  un  ascensor,  damos  nuestras  vidas  en  manos  del  software.  Las  aeronaves  son  controladas  por
software,  y  todo  el  sistema  mundial  de  control  del  tráfico  aéreo  depende  del  software.  Nuestros  automóviles  modernos  contienen  una  cantidad  
significativa  de  pequeños  sistemas  informáticos  con  software  que  se  comunican  a  través  de  una  red,  responsables  de  muchas  funciones  críticas  
para  la  seguridad  del  vehículo.  Climatización,  puertas  automáticas,  dispositivos  médicos,  trenes,  líneas  de  producción  automatizadas  en  
fábricas…  Hagamos  lo  que  hagamos  hoy  en  día,  estamos  permanentemente  en  contacto  con  el  software.  Y  con  la  revolución  digital  y  el  
Internet  de  las  cosas  (IoT),  la  relevancia  del  software  para  nuestra  vida  volverá  a  aumentar  significativamente.  Casi  ningún  otro  tema  es  más  
evidente  que  con  el  automóvil  autónomo  (sin  conductor).

Creo  que  es  innecesario  enfatizar  que  cualquier  error  en  estos  sistemas  intensivos  en  software  puede  tener  consecuencias  
catastróficas.  Una  falla  o  mal  funcionamiento  de  estos  importantes  sistemas  puede  ser  una  amenaza  para  la  vida  o  la  condición  física.  
En  el  peor  de  los  casos,  cientos  de  personas  pueden  perder  la  vida  durante  un  accidente  aéreo,  posiblemente  causado  por  una  declaración  
if  incorrecta  en  una  subrutina  del  subsistema  Fly­by­Wire.  La  calidad  en  ningún  caso  es  negociable  en  este  tipo  de  sistemas.  ¡Nunca!

Pero  incluso  en  sistemas  sin  requisitos  de  seguridad  funcional,  los  errores  pueden  tener  serias  implicaciones,
especialmente  si  son  más  sutiles  en  su  destructividad.  Es  fácil  imaginar  que  los  errores  en  el  software  financiero  podrían  ser  un  desencadenante  
de  una  crisis  bancaria  mundial  en  la  actualidad.  Solo  asuma  que  el  software  financiero  de  un  gran  banco  arbitrario  realiza  cada  publicación  
dos  veces  debido  a  un  error,  y  este  problema  no  se  notará  durante  un  par  de  días.

10
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

1990:  EL  ACCIDENTE  DE  AT&T

El  15  de  enero  de  1990,  la  red  telefónica  de  larga  distancia  de  AT&T  colapsó  y  75  millones  de  llamadas  telefónicas  
fallaron  durante  9  horas.  El  apagón  fue  causado  por  una  sola  línea  de  código  (una  declaración  de  interrupción  incorrecta)  
en  una  actualización  de  software  que  AT&T  implementó  en  los  114  interruptores  electrónicos  operados  por  computadora  
(4ESS)  en  diciembre  de  1989.  El  problema  comenzó  la  tarde  del  15  de  enero  cuando  un  El  mal  funcionamiento  en  el  
centro  de  control  de  AT&T  en  Manhattan  provocó  una  reacción  en  cadena  y  desactivó  los  interruptores  en  la  mitad  de  la  red.

La  pérdida  estimada  para  AT&T  fue  de  $60  millones,  y  probablemente  una  gran  cantidad  de  pérdidas  para  las  empresas  
que  dependían  de  la  red  telefónica.

Introducción  a  las  pruebas
Hay  diferentes  niveles  de  medidas  de  aseguramiento  de  la  calidad  en  los  proyectos  de  desarrollo  de  software.  Estos  niveles  a  menudo  se  
visualizan  en  forma  de  pirámide,  la  llamada  pirámide  de  prueba.  El  concepto  fundamental  fue  desarrollado  por  el  desarrollador  de  software  
estadounidense  Mike  Cohn,  uno  de  los  fundadores  de  Scrum  Alliance.  Describió  la  pirámide  de  automatización  de  pruebas  en  su  libro  
Succeeding  with  Agile  [Cohn09].  Con  la  ayuda  de  la  pirámide,  Cohn  describe  el  grado  de  automatización  requerido  para  una  prueba  de  
software  eficiente.  En  los  años  siguientes,  la  Pirámide  de  prueba  ha  sido  desarrollada  por  diferentes  personas.  El  que  se  muestra  en  la  Figura  
2­1  es  mi  versión.

Figura  2­1.  La  pirámide  de  prueba

La  forma  de  pirámide,  por  supuesto,  no  es  una  coincidencia.  El  mensaje  detrás  de  esto  es  que  deberías  tener  muchos
más  pruebas  unitarias  de  bajo  nivel  (aproximadamente  100%  de  cobertura  de  código)  que  otro  tipo  de  pruebas.  Pero  ¿por  qué  es  eso?

11
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

La  experiencia  ha  demostrado  que  los  costos  totales  relacionados  con  la  implementación  y  el  mantenimiento  de  las  pruebas  están  
aumentando  hacia  la  parte  superior  de  la  pirámide.  Las  pruebas  de  sistemas  grandes  y  las  pruebas  manuales  de  aceptación  del  usuario  
suelen  ser  complejas,  a  menudo  requieren  una  gran  organización  y  no  se  pueden  automatizar  fácilmente.  Por  ejemplo,  una  prueba  de  IU  
automatizada  es  difícil  de  escribir,  a  menudo  frágil  y  relativamente  lenta.  Por  lo  tanto,  estas  pruebas  a  menudo  se  realizan  manualmente,  lo  que  
es  adecuado  para  la  aprobación  del  cliente  (pruebas  de  aceptación)  y  las  pruebas  exploratorias  periódicas  por  parte  del  control  de  calidad,  
pero  consumen  demasiado  tiempo  y  son  demasiado  costosas  para  el  uso  diario  durante  el  desarrollo.
Además,  las  pruebas  de  sistemas  grandes,  o  las  pruebas  basadas  en  UI,  son  totalmente  inadecuadas  para  verificar  todas  las  rutas  
posibles  de  ejecución  a  través  de  todo  el  sistema.  Hay  mucho  código  en  un  sistema  de  software  que  se  ocupa  de  rutas  alternativas,  
excepciones  y  manejo  de  errores,  preocupaciones  transversales  (seguridad,  manejo  de  transacciones,  registro...)  y  otras  funciones  auxiliares  
que  se  requieren,  pero  que  a  menudo  no  se  pueden  alcanzar  a  través  del  usuario  normal.  interfaz.
Sobre  todo,  si  falla  una  prueba  a  nivel  del  sistema,  la  causa  exacta  del  error  puede  ser  difícil  de  localizar.  Las  pruebas  del  sistema  
generalmente  se  basan  en  los  casos  de  uso  del  sistema.  Durante  la  ejecución  de  un  caso  de  uso,  muchos  componentes  están  involucrados.  
Esto  significa  que  se  ejecutan  muchos  cientos,  o  incluso  miles,  de  líneas  de  código.  ¿Cuál  de  estas  líneas  fue  responsable  de  la  prueba  
fallida?  Esta  pregunta  a  menudo  no  se  puede  responder  fácilmente  y  requiere  un  análisis  costoso  y  que  requiere  mucho  tiempo.

Desafortunadamente,  en  varios  proyectos  de  desarrollo  de  software  encontrará  pirámides  de  prueba  degeneradas,  como  se  
muestra  en  la  figura  2­2.  En  tales  proyectos,  se  pone  un  enorme  esfuerzo  en  las  pruebas  de  nivel  superior,  mientras  que  se  descuidan  las  
pruebas  unitarias  elementales  (antipatrón  de  cono  de  helado).  En  el  caso  extremo  faltan  por  completo  (Cup  Cake  Anti­Pattern).

Figura  2­2.  Pirámides  de  prueba  degeneradas  (antipatrones)

Por  lo  tanto,  una  base  amplia  de  pruebas  unitarias  completamente  automatizadas,  bien  diseñadas,  muy  rápidas,  con  
mantenimiento  regular  y  económicas,  respaldada  por  una  selección  de  pruebas  de  componentes  útiles,  puede  ser  una  base  sólida  para  
garantizar  una  calidad  bastante  alta  de  un  sistema  de  software.

12
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

Pruebas  unitarias

“refactorizar”  sin  pruebas  no  es  refactorizar,  es  solo  mover  cosas.

—Corey  Haines  (@coreyhaines),  20  de  diciembre  de  2013,  en  Twitter

Una  prueba  unitaria  es  una  pieza  de  código  que  ejecuta  una  pequeña  parte  de  su  base  de  código  de  producción  en  un  contexto  particular.
La  prueba  le  mostrará  en  una  fracción  de  segundo  que  su  código  funciona  como  espera  que  funcione.  Si  la  cobertura  de  la  prueba  unitaria  
es  bastante  alta  y  puede  verificar  en  menos  de  un  minuto  que  todas  las  partes  de  su  sistema  en  desarrollo  funcionan  correctamente,  tendrá  
numerosas  ventajas:

•Numerosas  investigaciones  y  estudios  han  demostrado  que  corregir  errores  después  de  que  el  software  es
Se  ha  demostrado  que  el  envío  es  mucho  más  costoso  que  tener  pruebas  unitarias.

•Las  pruebas  unitarias  brindan  una  respuesta  inmediata  sobre  toda  su  base  de  código.  Siempre  que  la  cobertura  de  la  
prueba  sea  lo  suficientemente  alta  (aprox.  100  %),  los  desarrolladores  saben  en  tan  solo  unos  segundos  si  el  
código  funciona  correctamente.

•Las  pruebas  unitarias  brindan  a  los  desarrolladores  la  confianza  para  refactorizar  su  código  sin  temor  a  hacer  algo  
mal  que  rompa  el  código.  De  hecho,  un  cambio  estructural  en  un  código  base  sin  una  red  de  seguridad  de  
pruebas  unitarias  es  peligroso  y  no  debería  llamarse  Refactorización.

•  Una  alta  cobertura  con  pruebas  unitarias  puede  evitar  sesiones  de  depuración  frustrantes  y  que  consumen  
mucho  tiempo.  Las  búsquedas,  que  a  menudo  duran  horas,  de  la  causa  de  un  error  utilizando  un  
depurador  se  pueden  reducir  drásticamente.  Por  supuesto,  nunca  podrá  eliminar  por  completo  el  uso  de  un  
Depurador.  Esta  herramienta  todavía  se  puede  usar  para  analizar  problemas  sutiles  o  para  encontrar  la  
causa  de  una  prueba  unitaria  fallida.  Pero  ya  no  será  la  herramienta  de  desarrollo  fundamental  para  garantizar  
la  calidad  del  código.

•Las  pruebas  unitarias  son  un  tipo  de  documentación  ejecutable  porque  muestran  exactamente  cómo
el  código  está  diseñado  para  ser  utilizado.  Son,  por  así  decirlo,  una  especie  de  ejemplo  de  uso.

•Las  pruebas  unitarias  pueden  detectar  regresiones  fácilmente,  es  decir,  pueden  mostrar  cosas  de  inmediato
que  solía  funcionar,  pero  inesperadamente  dejó  de  funcionar  después  de  que  se  realizó  un  cambio  en  el  código.

•Las  pruebas  unitarias  fomentan  la  creación  de  interfaces  limpias  y  bien  formadas.  Eso  puede  ayudar
para  evitar  dependencias  no  deseadas  entre  unidades.  Un  diseño  para  la  capacidad  de  prueba  también  es  
un  buen  diseño  para  la  usabilidad,  es  decir,  si  una  pieza  de  código  se  puede  montar  fácilmente  en  un  
dispositivo  de  prueba,  entonces  también  se  puede  integrar  con  menos  esfuerzo  en  el  código  de  producción  
del  sistema.

•Las  pruebas  unitarias  hacen  que  el  desarrollo  sea  más  rápido.

Especialmente  el  último  elemento  de  esta  lista  parece  ser  paradójico  y  necesita  un  poco  de  explicación.  Unidad
las  pruebas  ayudan  a  que  el  desarrollo  avance  más  rápido,  ¿cómo  puede  ser  eso?  Eso  no  parece  lógico.
No  hay  duda  al  respecto:  escribir  pruebas  unitarias  significa  esfuerzo.  En  primer  lugar,  los  gerentes  solo  ven  ese  esfuerzo  y  no  
entienden  por  qué  los  desarrolladores  deberían  invertir  tiempo  en  las  pruebas.  Y  especialmente  en  la  fase  inicial  de  un  proyecto,  el  efecto  
positivo  de  las  pruebas  unitarias  en  la  velocidad  de  desarrollo  puede  no  ser  visible.  En  estas  primeras  etapas  de  un  proyecto,  cuando  la  
complejidad  del  sistema  es  relativamente  baja  y  casi  todo  funciona  bien,  al  principio  parece  que  escribir  pruebas  unitarias  solo  requiere  esfuerzo.  
Pero  los  tiempos  están  cambiando…

13
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

Cuando  el  sistema  se  vuelve  más  y  más  grande  (+  100,000  LOC)  y  la  complejidad  aumenta,  se  vuelve  más  difícil  entender  y  
verificar  el  sistema  (recuerde  la  entropía  del  software  que  describí  en  el  Capítulo  1 ).
Con  frecuencia,  cuando  muchos  desarrolladores  en  diferentes  equipos  están  trabajando  en  un  sistema  enorme,  se  enfrentan  todos  los  
días  con  el  código  escrito  por  otros  desarrolladores.  Sin  pruebas  unitarias,  esto  puede  convertirse  en  un  trabajo  muy  frustrante.  Estoy  
seguro  de  que  todos  conocen  esas  estúpidas  e  interminables  sesiones  de  depuración,  recorriendo  el  código  en  modo  de  un  solo  paso  
mientras  analizan  los  valores  de  las  variables  una  y  otra  vez.  …  ¡Esto  es  una  gran  pérdida  de  tiempo!
Y  ralentizará  significativamente  la  velocidad  de  desarrollo.
Particularmente  en  las  etapas  medias  y  tardías  de  desarrollo,  y  en  la  fase  de  mantenimiento  después  de  la  entrega  del  producto,  
las  buenas  pruebas  unitarias  despliegan  sus  efectos  positivos.  El  mayor  ahorro  de  tiempo  de  las  pruebas  unitarias  se  produce  unos  
meses  o  años  después  de  escribir  una  prueba,  cuando  es  necesario  cambiar  o  ampliar  una  unidad  o  su  API.
Si  la  cobertura  de  la  prueba  es  alta,  es  casi  irrelevante  si  un  código  editado  por  un  desarrollador  fue  escrito  por  él  mismo  o  por  
otro  desarrollador.  Las  buenas  pruebas  unitarias  ayudan  a  los  desarrolladores  a  comprender  rápidamente  un  fragmento  de  código  
escrito  por  otra  persona,  incluso  si  se  escribió  hace  tres  años.  Si  una  prueba  falla,  muestra  exactamente  dónde  se  rompe  algún  
comportamiento.  Los  desarrolladores  pueden  confiar  en  que  todo  seguirá  funcionando  correctamente  si  se  superan  todas  las  pruebas.
Las  sesiones  de  depuración  largas  y  molestas  se  vuelven  una  rareza,  y  el  Depurador  sirve  principalmente  para  encontrar  
rápidamente  la  causa  de  una  prueba  fallida  si  esta  causa  no  es  obvia.  Y  eso  es  genial  porque  es  divertido  trabajar  de  esa  manera.  Es  
motivador  y  conduce  a  mejores  y  más  rápidos  resultados.  Los  desarrolladores  tendrán  mayor  confianza  en  el  código  base  y  se  sentirán  
cómodos  con  él.  ¿Cambiar  los  requisitos  o  solicitar  nuevas  funciones?  No  hay  problema,  porque  pueden  enviar  el  nuevo  producto  rápido  
y  con  frecuencia,  y  con  una  calidad  excelente.

MARCOS  DE  PRUEBA  DE  UNIDAD

Hay  varios  marcos  de  prueba  de  unidad  diferentes  disponibles  para  el  desarrollo  de  C++,  por  ejemplo,  CppUnit,  
Boost.Test,  CUTE,  Google  Test  y  un  par  más.

En  principio,  todos  estos  marcos  siguen  el  diseño  básico  de  los  llamados  xUnit,  que  es  un  nombre  colectivo  para  
varios  marcos  de  pruebas  unitarias  que  derivan  su  estructura  y  funcionalidad  de  SUnit  de  Smalltalk.
Aparte  del  hecho  de  que  el  contenido  de  este  capítulo  no  se  fija  en  un  marco  de  pruebas  unitarias  específico,  y  
porque  su  contenido  es  aplicable  a  las  pruebas  unitarias  en  general,  una  comparación  completa  y  detallada  de  todos  
los  marcos  disponibles  estaría  más  allá  del  alcance  de  este  libro.  Además,  elegir  un  marco  adecuado  depende  
de  muchos  factores.  Por  ejemplo,  si  es  muy  importante  para  usted  que  pueda  agregar  rápidamente  nuevas  pruebas  
con  una  cantidad  mínima  de  trabajo,  entonces  este  podría  ser  un  criterio  de  eliminación  para  ciertos  marcos.

¿Qué  pasa  con  el  control  de  calidad?

Un  desarrollador  podría  tener  la  siguiente  actitud:  “¿Por  qué  debo  probar  mi  software?  Tenemos  probadores  y  un  departamento  de  
control  de  calidad,  es  su  trabajo”.
La  pregunta  esencial  es  esta:  ¿Es  la  calidad  del  software  una  preocupación  exclusiva  del  departamento  de  control  de  calidad?
La  respuesta  simple  y  clara:  ¡ No!

He  dicho  esto  antes,  y  lo  diré  de  nuevo.  A  pesar  de  que  su  empresa  puede  tener  un  grupo  de  control  de  calidad  
separado  para  probar  el  software,  el  objetivo  del  grupo  de  desarrollo  debe  ser  que  el  control  de  calidad  no  
encuentre  nada  malo.

—Robert  C.  Martin,  El  codificador  limpio  [Martin11]

14
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

Sería  extremadamente  poco  profesional  entregar  una  pieza  de  software  a  control  de  calidad  de  la  que  se  sabe  que  contiene  errores.  Los  
desarrolladores  profesionales  nunca  imponen  la  responsabilidad  de  la  calidad  de  un  sistema  a  otros  departamentos.  Por  el  contrario,  los  
artesanos  de  software  profesionales  construyen  asociaciones  productivas  con  la  gente  de  control  de  calidad.  Deben  trabajar  en  estrecha  
colaboración  y  complementarse  entre  sí.
Por  supuesto,  es  un  objetivo  muy  ambicioso  entregar  software  100%  libre  de  defectos.  De  vez  en  cuando,  QA  encontrará
Ocurre  algo.  Y  eso  es  bueno.  QA  es  nuestra  segunda  red  de  seguridad.  Comprueban  si  las  medidas  de  garantía  de  calidad  anteriores  fueron  
suficientes  y  efectivas.

De  nuestros  errores  podemos  aprender  y  mejorar.  Los  desarrolladores  profesionales  solucionan  esos  déficits  de  calidad  de  inmediato  
corrigiendo  los  errores  que  encontró  el  control  de  calidad  y  escribiendo  pruebas  unitarias  automatizadas  para  detectarlos  en  el  futuro.  Luego,  deben  
pensar  cuidadosamente  en  esto:  "¿Cómo,  en  el  nombre  de  Dios,  puede  suceder  que  hayamos  pasado  por  alto  este  problema?"  El  resultado  de  esta  
retrospectiva  debe  servir  como  insumo  para  mejorar  el  proceso  de  desarrollo.

Reglas  para  buenas  pruebas  unitarias

He  visto  muchas  pruebas  unitarias  que  son  bastante  inútiles.  Las  pruebas  unitarias  deben  agregar  valor  a  su  proyecto.  Para  lograr  este  objetivo,  
se  deben  seguir  algunas  reglas  esenciales,  que  describiré  en  esta  sección.

Calidad  del  código  de  prueba  Los  

mismos  requisitos  de  alta  calidad  para  el  código  de  producción  tienen  que  ser  válidos  para  el  código  de  prueba  unitario.  Iré  aún  más  lejos:  
idealmente,  no  debería  haber  una  distinción  crítica  entre  el  código  de  producción  y  el  de  prueba,  ambos  son  iguales.  Si  decimos  que  hay  código  
de  producción  por  un  lado  y  código  de  prueba  por  el  otro,  separamos  cosas  que  van  juntas  inseparablemente.  ¡No  hagas  eso!  Pensar  en  la  
producción  y  el  código  de  prueba  en  dos  categorías  sienta  las  bases  para  poder  descuidar  las  pruebas  más  adelante  en  el  proyecto.

Denominación  de  prueba  unitaria

Si  una  prueba  unitaria  falla,  el  desarrollador  quiere  saber  de  inmediato:

•Cuál  es  el  nombre  de  la  unidad;  ¿De  quién  fue  la  prueba  que  falló?

•¿Qué  se  probó  y  cuál  fue  el  entorno  de  la  prueba  (el  escenario  de  la  prueba)?

•  ¿Cuál  era  el  resultado  esperado  de  la  prueba  y  cuál  es  el  resultado  real  de  la  prueba  fallida?

Por  lo  tanto,  una  denominación  expresiva  y  descriptiva  de  las  pruebas  unitarias  es  muy  importante.  Mi  consejo  es  establecer  estándares  
de  nomenclatura  para  todas  las  pruebas.
En  primer  lugar,  es  una  buena  práctica  nombrar  el  módulo  de  prueba  de  unidad  (según  el  marco  de  prueba  de  unidad,  se  denominan  
Arneses  de  prueba  o  Dispositivos  de  prueba)  de  tal  manera  que  la  unidad  probada  pueda  derivarse  fácilmente  de  él.
Deben  tener  un  nombre  como  <Unidad_bajo_Prueba>Prueba,  donde  el  marcador  de  posición  <Unidad_bajo_Prueba>  debe  ser  sustituido  por  el  
nombre  del  sujeto  de  prueba,  obviamente.  Por  ejemplo,  si  su  sistema  bajo  prueba  (SUT)  es  la  unidad  Money,  el  dispositivo  de  prueba  
correspondiente  que  se  adjunta  a  esa  unidad  y  contiene  todos  los  casos  de  prueba  de  la  unidad,  debe  llamarse  MoneyTest  (vea  la  Figura  2­3).

Figura  2­3.  El  sistema  bajo  prueba  (SUT)  y  su  Contexto  de  Prueba

15
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

Más  allá  de  eso,  las  pruebas  unitarias  deben  tener  nombres  expresivos  y  descriptivos.  No  es  útil  si  las  pruebas  unitarias  
tienen  nombres  más  o  menos  sin  sentido  como  testConstructor(),  test4391()  o  sumTest().  Aquí  hay  dos  sugerencias  para  encontrar  
un  buen  nombre  para  ellos.
Para  clases  generales  multipropósito  que  se  pueden  usar  en  diferentes  contextos,  un  nombre  expresivo  podría
contienen  las  siguientes  partes:

•La  condición  previa  del  escenario  de  prueba,  es  decir,  el  estado  del  SUT  antes  de  que  se  realizara  la  prueba.
ejecutado.

•La  parte  probada  de  la  unidad  bajo  prueba,  típicamente  el  nombre  del  procedimiento  probado,
función  o  método  (API).

•El  resultado  esperado  de  la  prueba.

Eso  lleva  a  una  plantilla  de  nombre  para  procedimientos/métodos  de  prueba  unitaria,  como  esta:

<Condición  previa  y  estado  de  la  unidad  bajo  prueba>_<Parte  probada  de  la  API>_<Comportamiento  esperado>

Aquí  están  algunos  ejemplos:

Listado  2­1.  Algunos  ejemplos  de  nombres  de  pruebas  unitarias  buenos  y  expresivos

void  CustomerCacheTest::cacheIsEmpty_addElement_sizeIsOne();  void  
CustomerCacheTest::cacheContainsOneElement_removeElement_sizeIsZero();  void  
ComplexNumberCalculatorTest::givenTwoComplexNumbers_add_Works();  void  PruebaDinero::  
dadosDosObjetosDineroConDiferentesBalance_theInequalityComparason_Works();  void  
MoneyTest::createMoneyObjectWithParameter_getBalanceAsString_returnsCorrectString();  void  
InvoiceTest::invoiceIsReadyForAccounting_getInvoiceDate_returnsToday();

Otro  enfoque  posible  para  crear  nombres  expresivos  de  pruebas  unitarias  es  manifestar  un  requisito  específico  en
el  nombre.  Estos  nombres  suelen  reflejar  los  requisitos  del  dominio  de  la  aplicación.  Por  ejemplo,  se  derivan  de  los  requisitos  de  las  
partes  interesadas.

Listado  2­2.  Algunos  ejemplos  más  de  nombres  de  pruebas  unitarias  que  verifican  los  requisitos  específicos  del  dominio

void  UserAccountTest::creatingNewAccountWithExistingEmailAddressThrowsException();  void  
ChessEngineTest::aPawnCanNotMoveBackwards();  void  
ChessEngineTest::aEl  enroque  no  está  permitido  si  el  rey  involucrado  ha  sido  movido  antes  ();  void  
ChessEngineTest::aNo  se  permite  el  enroque  si  la  torre  involucrada  se  ha  movido  antes  ();  void  Prueba  de  control  del  
calentador::  si  la  temperatura  del  agua  es  mayor  que  92  grados,  apague  el  calentador  ();  void  Prueba  de  inventario  
de  libro::  un  libro  que  está  en  el  inventario  puede  ser  prestado  por  personas  autorizadas  ();  void  Prueba  de  inventario  de  libro::  
un  libro  que  ya  está  prestado  no  puede  tomar  prestado  dos  veces  ();

A  medida  que  lea  los  nombres  de  estos  métodos  de  prueba,  quedará  claro  que  incluso  si  la  implementación  de  las  pruebas  y  los  
métodos  de  prueba  no  se  muestran  aquí,  se  puede  derivar  fácilmente  una  gran  cantidad  de  información  útil.  Y  esto  también  es  una  gran  
ventaja  si  tal  prueba  falla.  Casi  todos  los  marcos  de  pruebas  unitarias  escriben  el  nombre  de  la  prueba  fallida  en  la  salida  estándar  
(stdout).  Por  lo  tanto,  la  ubicación  del  error  se  facilita  enormemente.

Independencia  de  las  pruebas  unitarias  Cada  

prueba  unitaria  debe  ser  independiente  de  todas  las  demás.  Sería  fatal  si  las  pruebas  deben  ejecutarse  en  un  orden  específico  porque  
una  prueba  se  basa  en  el  resultado  de  la  anterior.  Nunca  escriba  una  prueba  unitaria  cuyo  resultado  sea  el  requisito  previo  para  una  
prueba  posterior.  Nunca  deje  la  unidad  bajo  prueba  en  un  estado  alterado,  lo  cual  es  una  condición  previa  para  las  siguientes  pruebas.

dieciséis
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

Los  problemas  principales  pueden  ser  causados  por  estados  globales,  por  ejemplo,  el  uso  de  Singletons  o  miembros  estáticos  
en  su  unidad  bajo  prueba.  No  solo  es  que  los  Singleton  aumentan  el  acoplamiento  entre  las  unidades  de  software.  También  suelen  
tener  un  estado  global  que  elude  la  independencia  de  las  pruebas  unitarias.  Por  ejemplo,  si  un  determinado  estado  global  es  la  
condición  previa  para  una  prueba  exitosa,  pero  la  prueba  anterior  ha  mutado  ese  estado  global,  puede  causar  serios  problemas.
Especialmente  en  los  sistemas  heredados,  que  a  menudo  están  plagados  de  Singletons,  esto  plantea  la  pregunta:  ¿cómo  puedo  
deshacerme  de  todas  esas  desagradables  dependencias  de  esos  Singletons  y  hacer  que  mi  código  sea  mejor  comprobable?  Bueno,  esa  
es  una  pregunta  importante  que  discuto  en  la  sección  Inyección  de  dependencia  en  el  Capítulo  6.

TRATAR  CON  SISTEMAS  LEGADOS

Si  se  enfrenta  a  los  llamados  sistemas  heredados  y  enfrenta  muchas  dificultades  al  intentar  agregar  pruebas  unitarias,  le  
recomiendo  el  libro  Trabajar  de  manera  efectiva  con  código  heredado  [Feathers07]  de  Michael  C.
Plumas.  El  libro  de  Feathers  contiene  muchas  estrategias  para  trabajar  con  grandes  bases  de  código  heredadas  no  probadas.
También  incluye  un  catálogo  de  24  técnicas  de  ruptura  de  dependencia.  Estas  estrategias  y  técnicas  están  más  allá  del  alcance  
de  este  libro.

Una  afirmación  por  prueba  Sé  que  este  es  un  

tema  controvertido,  pero  intentaré  explicar  por  qué  creo  que  es  importante.  Mi  consejo  es  limitar  una  prueba  unitaria  para  usar  una  sola  
afirmación,  como  esta:

Listado  2­3.  Una  prueba  unitaria  que  verifica  el  operador  no  igual  de  una  clase  de  dinero

void  MoneyTest::givenTwoMoneyObjectsWithDifferentBalance_theInequalityComparison_Works()  {
constante  Dinero  m1(­4000.0);  const  
Dinero  m2(2000.0);  
ASSERT_TRUE(m1 !=  m2); }

Ahora  se  podría  argumentar  que  también  podríamos  verificar  si  otros  operadores  de  comparación  (por  ejemplo,  
Money::operator==())  están  funcionando  correctamente  en  esta  prueba  unitaria.  Sería  fácil  hacerlo  simplemente  agregando  más  
afirmaciones,  como  esta:

Listado  2­4.  Pregunta:  ¿Es  realmente  una  buena  idea  verificar  todos  los  operadores  de  comparación  en  una  prueba  unitaria?

void  MoneyTest::givenTwoMoneyObjectsWithDifferentBalance_testAllComparisonOperators()  {
constante  Dinero  m1(­4000.0);  const  
Dinero  m2(2000.0);  
ASSERT_TRUE(m1 !=  m2);  
ASSERT_FALSE(m1  ==  m2);  
ASSERT_TRUE(m1  <  m2);  
ASSERT_FALSE(m1  >  
m2); // ...más  afirmaciones  aquí...
}

17

www.allitebooks.com
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

Creo  que  los  problemas  con  este  enfoque  son  obvios:
• Si  una  prueba  puede  fallar  por  varias  razones,  puede  ser  difícil  para  los  desarrolladores  encontrar  
rápidamente  la  causa  del  error.  Sobre  todo,  una  aserción  temprana  que  falla  oscurece  errores  
adicionales,  es  decir,  oculta  aserciones  posteriores,  porque  se  detiene  la  ejecución  de  la  prueba.

•Como  ya  se  explicó  en  la  sección  Nombre  de  prueba  unitaria,  debemos  nombrar  una  prueba  en  un
manera  precisa  y  expresiva.  Con  múltiples  aserciones,  una  prueba  unitaria  realmente  prueba  
muchas  cosas  (lo  que,  dicho  sea  de  paso,  es  una  violación  del  principio  de  responsabilidad  
única;  consulte  el  capítulo  6),  y  sería  difícil  encontrarle  un  buen  nombre.  Lo  anterior...  
testAllComparisonOperators()  no  es  lo  suficientemente  preciso.

Inicialización  independiente  de  entornos  de  pruebas  unitarias  Esta  regla  es  algo  similar  a  la  

independencia  de  pruebas  unitarias.  Cuando  se  completa  una  prueba  implementada  limpia,  todos  los  estados  relacionados  con  
esa  prueba  deben  desaparecer.  En  términos  más  específicos:  cuando  se  ejecutan  todas  las  pruebas  unitarias,  cada  prueba  debe  
ser  una  instanciación  parcial  aislada  de  una  aplicación.  Cada  prueba  tiene  que  configurar  e  inicializar  su  entorno  requerido  
completamente  por  su  cuenta.  Lo  mismo  se  aplica  a  la  limpieza  después  de  la  ejecución  de  la  prueba.

Excluir  getters  y  setters
No  escriba  pruebas  unitarias  para  getters  y  setters  habituales  de  una  clase,  como  esta:

Listado  2­5.  Un  simple  setter  y  getter

void  Cliente::setForename(const  std::string&  nombre)  { this­>nombre  =  nombre; }

std::string  Cliente::getForename()  const  { return  nombre; }

¿Realmente  espera  que  algo  pueda  salir  mal  con  métodos  tan  sencillos?  Estas  funciones  miembro  suelen  ser  tan  
simples  que  sería  una  tontería  escribir  pruebas  unitarias  para  ellas.  Además,  los  getters  y  setters  habituales  se  prueban  
implícitamente  mediante  otras  pruebas  unitarias  más  importantes.
Atención,  acabo  de  escribir  que  no  es  necesario  probar  getters  y  setters  habituales  y  simples .  A  veces,
getters  y  setters  no  son  tan  simples.  De  acuerdo  con  el  Principio  de  ocultación  de  información  (consulte  la  sección  
Ocultación  de  información  en  el  Capítulo  3)  que  discutiremos  más  adelante,  debe  ocultarse  para  el  cliente  si  un  getter  es  
simple  y  estúpido,  o  si  tiene  que  hacer  cosas  complejas  para  determinar  su  valor  de  retorno. .  Por  lo  tanto,  a  veces  puede  
ser  útil  escribir  una  prueba  explícita  para  un  getter  o  setter.

Excluir  código  de  terceros
¡No  escriba  pruebas  para  código  de  terceros!  No  tenemos  que  verificar  que  las  bibliotecas  o  los  marcos  funcionen  como  
se  esperaba.  Por  ejemplo,  podemos  asumir  con  la  conciencia  tranquila  que  la  función  miembro  utilizada  innumerables  veces  
std::vector::push_back()  de  la  biblioteca  estándar  de  C++  funciona  correctamente.  Por  el  contrario,  podemos  esperar  que  el  
código  de  terceros  venga  con  sus  propias  pruebas  unitarias.  Puede  ser  una  sabia  decisión  arquitectónica  no  utilizar  bibliotecas  o  
marcos  en  su  proyecto  que  no  tengan  pruebas  unitarias  propias  y  cuya  calidad  sea  dudosa.

18
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

Excluir  sistemas  externos
Lo  mismo  que  para  el  código  de  terceros  se  aplica  a  los  sistemas  externos.  No  escriba  pruebas  para  sistemas  que  están  en  el  contexto  de  su  
sistema  a  desarrollar  y,  por  lo  tanto,  no  están  bajo  su  responsabilidad.  Por  ejemplo,  si  su  software  financiero  utiliza  un  sistema  de  conversión  de  
moneda  externo  existente  que  está  conectado  a  través  de  Internet,  no  debe  probarlo.  Además  del  hecho  de  que  un  sistema  de  este  tipo  no  puede  
proporcionar  una  respuesta  definida  (el  factor  de  conversión  entre  monedas  varía  minuto  a  minuto)  y  que  dicho  sistema  puede  ser  imposible  de  
alcanzar  debido  a  problemas  de  red,  no  somos  responsables  del  sistema  externo.

Mi  consejo  es  burlarse  (consulte  la  sección  Probar  dobles  (objetos  falsos)  más  adelante  en  este  capítulo)  estas  cosas  y
para  probar  su  código,  no  el  de  ellos.

¿Y  qué  hacemos  con  la  base  de  datos?
Muchos  sistemas  de  TI  contienen  bases  de  datos  (relacionales)  hoy  en  día.  Se  requieren  para  conservar  grandes  cantidades  de  objetos  o  datos  
en  un  almacenamiento  a  largo  plazo,  de  modo  que  estos  objetos  o  datos  puedan  consultarse  de  manera  cómoda  y  sobrevivan  a  un  apagado  del  
sistema.
Una  pregunta  importante  es  esta:  ¿qué  haremos  con  la  base  de  datos  durante  las  pruebas  unitarias?

Mi  primer  y  principal  consejo  sobre  este  tema  es:  cuando  haya  alguna  forma  de  probar  sin  una  
base  de  datos,  ¡pruebe  sin  la  base  de  datos!

—Gerard  Meszaros,  xUnit  Patterns

Las  bases  de  datos  pueden  causar  problemas  diversos  y,  en  ocasiones,  sutiles  durante  las  pruebas  unitarias.  Por  ejemplo,  si  muchas  pruebas  
unitarias  usan  la  misma  base  de  datos,  la  base  de  datos  tiende  a  convertirse  en  un  gran  almacenamiento  central  que  esas  pruebas  deben  compartir  
para  diferentes  propósitos.  Este  intercambio  puede  afectar  adversamente  la  independencia  de  las  pruebas  unitarias  que  he  discutido  anteriormente  en  
este  capítulo.  Podría  ser  difícil  garantizar  la  condición  previa  requerida  para  cada  prueba  unitaria.  La  ejecución  de  una  prueba  unitaria  puede  causar  
efectos  secundarios  no  deseados  para  otras  pruebas  a  través  de  la  base  de  datos  de  uso  común.
Otro  problema  es  que  las  bases  de  datos  son  básicamente  lentas.  Son  mucho  más  lentos  que  el  acceso  a  la  memoria  de  la  computadora  
local.  Las  pruebas  unitarias  que  interactúan  con  la  base  de  datos  tienden  a  ejecutar  magnitudes  más  lentas  que  las  pruebas  que  pueden  ejecutarse  
completamente  en  la  memoria.  Imagine  que  tiene  unos  cientos  de  pruebas  unitarias,  y  cada  prueba  necesita  un  lapso  de  tiempo  adicional  de  
500  ms  en  promedio,  causado  por  las  consultas  de  la  base  de  datos.  En  resumen,  todas  las  pruebas  tardan  varios  minutos  más  que  sin  una  base  de  datos.
Mi  consejo  es  simular  la  base  de  datos  (consulte  la  sección  sobre  Probar  objetos  dobles/simulacros  más  adelante  en  este  capítulo)  y  
ejecutar  todas  las  pruebas  unitarias  únicamente  en  la  memoria.  No  se  preocupe:  la  base  de  datos,  si  existe,  estará  involucrada  a  nivel  de  integración  
y  prueba  del  sistema.

No  mezcle  el  código  de  prueba  con  el  código  de  producción
A  veces,  a  los  desarrolladores  se  les  ocurre  la  idea  de  equipar  su  código  de  producción  con  un  código  de  prueba.  Por  ejemplo,  una  clase  puede  
contener  código  para  manejar  una  dependencia  con  una  clase  colaboradora  durante  una  prueba  de  la  siguiente  manera:

Listado  2­6.  Una  posible  solución  para  lidiar  con  una  dependencia  durante  la  prueba

#include  <memoria>  
#include  "DataAccessObject.h"  #include  
"CustomerDAO.h"  #include  
"FakeDAOForTest.h"

usando  DataAccessObjectPtr  =  std::unique_ptr<DataAccessObject>;

19
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

clase  Cliente  { público:

Cliente()  {}  
explícito  Cliente(bool  testMode) :  inTestMode(testMode)  {}

void  save()  
{ DataAccessObjectPtr  dataAccessObject  =  getDataAccessObject(); // ...usar  
dataAccessObject  para  guardar  este  cliente... };

// ...

privado:
DataAccessObjectPtr  getDataAccessObject()  const  {
if  (inTestMode)  { return  
std::make_unique<FakeDAOForTest>(); }  else  { return  

std::make_unique<CustomerDAO>(); }

} // ...más  operaciones  aquí...

bool  enModoPrueba{ falso }; // ...más  
atributos  aquí... };

DataAccessObject  es  la  clase  base  abstracta  de  DAO  específicos,  en  este  caso,  CustomerDAO  y  
FakeDAOForTest.  El  último  es  el  llamado  objeto  falso,  que  no  es  más  que  un  doble  de  prueba  (consulte  la  
sección  sobre  Dobles  de  prueba  (objetos  falsos)  más  adelante  en  este  capítulo).  Está  destinado  a  reemplazar  el  DAO  
real,  ya  que  no  queremos  probarlo  y  no  queremos  salvar  al  cliente  durante  la  prueba  (recuerde  mi  consejo  sobre  las  
bases  de  datos).  El  miembro  de  datos  booleano  en  Modo  de  prueba  controla  cuál  de  los  dos  DAO  se  usa.
Bueno,  este  código  funcionaría,  pero  la  solución  tiene  varias  desventajas.
En  primer  lugar,  nuestro  código  de  producción  está  repleto  de  código  de  prueba.  Aunque  no  parezca  dramático  a  
primera  vista,  puede  aumentar  la  complejidad  y  reducir  la  legibilidad.  Necesitamos  un  miembro  adicional  para  distinguir  
entre  el  modo  de  prueba  y  el  uso  de  producción  de  nuestro  sistema.  Este  miembro  booleano  no  tiene  nada  que  ver  
con  un  cliente,  y  mucho  menos  con  el  dominio  de  nuestro  sistema.  Y  es  fácil  imaginar  que  ese  tipo  de  miembro  se  
requiere  en  muchas  clases  de  nuestro  sistema.
Además,  nuestra  clase  Customer  tiene  dependencias  con  CustomerDAO  y  FakeDAOForTest.  Puede  verlo  en  la  
lista  de  inclusiones  en  la  parte  superior  del  código  fuente.  Esto  significa  que  el  dummy  de  prueba  FakeDAOForTest  
también  forma  parte  del  sistema  en  el  entorno  de  producción.  Es  de  esperar  que  el  código  del  doble  de  prueba  nunca  se  
llame  en  producción,  sino  que  se  compile,  enlace  e  implemente.
Por  supuesto,  hay  formas  más  elegantes  de  lidiar  con  estas  dependencias  y  de  mantener  el  código  de  producción  
libre  de  código  de  prueba.  Por  ejemplo,  podemos  inyectar  el  DAO  específico  como  parámetro  de  referencia  en  Customer::save().

Listado  2­7.  Evitar  dependencias  para  probar  el  código  (1)

clase  ObjetoAccesoDatos;

clase  Cliente  { public:  
void  
save(DataAccessObject&  dataAccessObject)  {
// ...usar  dataAccessObject  para  guardar  este  cliente... }

20
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

// ... };

Alternativamente,  esto  se  puede  hacer  durante  la  construcción  de  instancias  de  tipo  Cliente.  En  este  caso  debemos
mantener  una  referencia  a  la  DAO  como  un  atributo  de  la  clase.  Además,  tenemos  que  suprimir  la  generación  
automática  del  constructor  predeterminado  a  través  del  compilador  porque  no  queremos  que  ningún  usuario  del  
Cliente  pueda  crear  una  instancia  incorrectamente  inicializada  del  mismo.

Listado  2­8.  Evitar  dependencias  para  probar  el  código  (2)

clase  ObjetoAccesoDatos;

clase  Cliente  
{ public:  
Cliente()  =  borrar;  
Customer(DataAccessObject&  dataAccessObject) :  dataAccessObject(dataAccessObject)  {}  void  
save()  { // ...use  
el  miembro  dataAccessObject  para  guardar  este  cliente... }

// ...
privado:  
DataAccessObject&  dataAccessObject; // ...

FUNCIONES  ELIMINADAS  [C++11]

En  C++,  el  compilador  genera  automáticamente  las  denominadas  funciones  miembro  especiales  (constructor  
predeterminado,  constructor  de  copia,  operador  de  asignación  de  copia  y  destructor)  para  un  tipo  si  no  declara  el  suyo  
propio.  Desde  C++11,  esta  lista  de  funciones  miembro  especiales  se  amplía  con  el  constructor  de  movimiento  y  el  
operador  de  asignación  de  movimiento.  C  ++  11  (y  superior)  proporciona  una  manera  fácil  y  declarativa  de  suprimir  la  
creación  automática  de  cualquier  función  de  miembro  especial,  así  como  funciones  de  miembros  normales  y  funciones  
de  no  miembros:  puede  eliminarlas.  Por  ejemplo,  puede  evitar  la  creación  de  un  constructor  predeterminado  de  esta  manera:

clase  Clazz  
{ público:
Clazz()  =  borrar; };

Y  otro  ejemplo:  puede  eliminar  el  operador  nuevo  para  evitar  que  las  clases  se  asignen  dinámicamente  en  el  montón:

class  Clazz  
{ public:  
void*  operator  new(std::size_t)  =  delete; };

21
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

Una  tercera  alternativa  podría  ser  que  la  DAO  específica  sea  creada  por  una  Fábrica  (ver  sección  Fábrica  en  el  Capítulo  9  
sobre  Patrones  de  Diseño)  el  Cliente  conoce.  Esta  fábrica  se  puede  configurar  desde  el  exterior  para  crear  el  tipo  de  DAO  que  se  
requiere  si  el  sistema  se  ejecuta  en  un  entorno  de  prueba.  No  importa  cuál  de  estas  posibles  soluciones  elija,  el  Cliente  está  libre  de  
código  de  prueba.  No  hay  dependencias  de  DAO  específicos  en  Customer.

Las  pruebas  deben  ejecutarse  rápido

En  proyectos  grandes,  un  día  llegará  al  punto  en  que  tendrá  miles  de  pruebas  unitarias.  Esto  es  excelente  en  términos  de  calidad  
del  software.  Pero  un  efecto  secundario  incómodo  podría  ser  que  las  personas  dejen  de  ejecutar  estas  pruebas  antes  de  realizar  
un  registro  en  el  repositorio  del  código  fuente,  porque  lleva  demasiado  tiempo.
Es  fácil  imaginar  que  existe  una  fuerte  correlación  entre  el  tiempo  que  lleva  realizar  las  pruebas  y  la  productividad  de  un  equipo.  
Si  la  ejecución  de  todas  las  pruebas  unitarias  lleva  15  minutos,  1/2  hora  o  más,  los  desarrolladores  no  pueden  hacer  su  trabajo  y  
pierden  el  tiempo  esperando  los  resultados  de  la  prueba.  Si  bien  la  ejecución  de  cada  prueba  unitaria  toma  “solo”  medio  segundo  en  
promedio,  se  necesitan  más  de  8  minutos  para  realizar  1000  pruebas.  Eso  significa  que  la  ejecución  de  todo  el  conjunto  de  pruebas  
10  veces  al  día  resultará  en  casi  1,5  horas  de  tiempo  de  espera  en  total.  Como  resultado,  los  desarrolladores  ejecutarán  las  pruebas  
con  menos  frecuencia.
Mi  consejo  es:  ¡ Las  pruebas  deben  ejecutarse  rápido!  Las  pruebas  unitarias  deben  establecer  un  ciclo  de  retroalimentación  rápido  para  los  desarrolladores.

La  ejecución  de  todas  las  pruebas  unitarias  para  un  proyecto  grande  no  debería  durar  más  de  unos  3  minutos,  y  mucho  menos  que  
eso.  Para  una  ejecución  de  prueba  local  más  rápida  (<=  unos  segundos)  durante  el  desarrollo,  el  marco  de  prueba  debe  proporcionar  
una  manera  fácil  de  desactivar  grupos  de  pruebas  irrelevantes  temporalmente.
No  hace  falta  decir  que  en  el  sistema  de  compilación  automatizado,  todas  las  pruebas  deben  ejecutarse  sin  excepción.
continuamente  cada  vez  antes  de  que  se  construya  el  producto  final.  El  equipo  de  desarrollo  debe  recibir  una  notificación  
inmediata  si  una  o  más  pruebas  fallan  en  el  sistema  de  compilación.  Por  ejemplo,  esto  se  puede  hacer  por  correo  electrónico  o  con  la  
ayuda  de  una  visualización  óptica  (por  ejemplo,  debido  a  una  pantalla  plana  en  la  pared  o  un  "semáforo"  controlado  por  el  sistema  de  
construcción)  en  un  lugar  destacado.  ¡Si  falla  una  sola  prueba,  bajo  ninguna  circunstancia  debe  liberar  y  enviar  el  producto!

Dobles  de  prueba  (objetos  falsos)
Las  pruebas  unitarias  solo  deben  llamarse  "pruebas  unitarias"  si  las  unidades  a  probar  son  completamente  independientes  de  
los  colaboradores  durante  la  ejecución  de  la  prueba,  es  decir,  la  unidad  bajo  prueba  no  utiliza  otras  unidades  o  sistemas  externos.
Por  ejemplo,  mientras  que  la  participación  de  una  base  de  datos  durante  una  prueba  de  integración  no  es  crítica  y  necesaria,  
porque  ese  es  el  propósito  de  una  prueba  de  integración,  el  acceso  (por  ejemplo,  una  consulta)  a  esta  base  de  datos  durante  
una  prueba  de  unidad  real  está  prohibido  (consulte  la  sección  Y  qué  ¿Qué  hacemos  con  la  base  de  datos?,  anteriormente  en  este  
capítulo).  Por  lo  tanto,  las  dependencias  de  la  unidad  a  probar  con  otros  módulos  o  sistemas  externos  deben  reemplazarse  por  los  
llamados  Test  Doubles,  también  conocidos  como  Fake  Objects  o  Mock­Ups.
Para  trabajar  de  manera  elegante  con  tales  Test  Doubles,  se  debe  evitar  el  acoplamiento  flojo  de  la  unidad  bajo  prueba.
por  lo  que  se  ha  esforzado  (consulte  la  sección  Acoplamiento  flojo  en  el  capítulo  Tenga  principios).  Por  ejemplo,  se  puede  introducir  
una  abstracción  (p.  ej.,  una  interfaz  en  forma  de  una  clase  puramente  abstracta)  en  el  punto  donde  se  accede  a  un  colaborador  
que  no  es  deseado  para  la  prueba,  como  se  muestra  en  la  Figura  2­4.

22
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

Figura  2­4.  Una  interfaz  facilita  el  reemplazo  de  X  con  un  Test  Double  XMock

Supongamos  que  desea  desarrollar  una  aplicación  que  utilice  un  servicio  web  externo  para  las  conversiones  de  
divisas  actuales.  Durante  una  prueba  unitaria,  no  puede  utilizar  este  servicio  externo  de  forma  natural,  ya  que  ofrece  
diferentes  factores  de  conversión  cada  segundo.  Además,  el  servicio  se  consulta  a  través  de  Internet,  que  es  básicamente  
lento  y  puede  fallar.  Y  es  imposible  simular  casos  límite.  Por  lo  tanto,  debe  reemplazar  la  conversión  de  moneda  real  
por  un  doble  de  prueba  durante  la  prueba  unitaria.
Primero,  tenemos  que  introducir  un  punto  de  variación  en  nuestro  código,  donde  podemos  reemplazar  el  
módulo  que  se  comunica  con  el  servicio  de  conversión  de  moneda  por  un  doble  de  prueba.  Esto  se  puede  hacer  con  la  
ayuda  de  una  interfaz,  que  en  C++  es  una  clase  abstracta  con  funciones  miembro  puramente  virtuales.

Listado  2­9.  Una  interfaz  abstracta  para  convertidores  de  divisas

class  Conversor  de  divisas  
{ public:  
virtual  ~Conversor  de  divisas()  { }  virtual  
long  double  getConversionFactor()  const  =  0; };

El  acceso  al  servicio  de  conversión  de  moneda  a  través  de  Internet  está  encapsulado  en  una  clase  que  implementa
la  interfaz  del  convertidor  de  divisas.

Listado  2­10.  La  clase  que  accede  al  servicio  de  conversión  de  moneda  en  tiempo  real

clase  RealtimeCurrencyConversionService:  public  CurrencyConverter  { public:  virtual  long  
double  
getConversionFactor()  const  override; // ...más  miembros  aquí  que  se  
requieren  para  acceder  al  servicio... };

23
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

Para  fines  de  prueba,  existe  una  segunda  implementación:  Test  Double  CurrencyConversionServiceMock.
Los  objetos  de  esta  clase  devolverán  un  factor  de  conversión  definido  y  predecible,  ya  que  se  requiere  para  las  pruebas  unitarias.
Además,  los  objetos  de  esta  clase  proporcionan  además  la  capacidad  de  establecer  el  factor  de  conversión  desde  el  exterior,  
por  ejemplo,  para  simular  casos  límite.

Listado  2­11.  El  doble  de  prueba

clase  CurrencyConversionServiceMock:  public  CurrencyConverter  { public:  virtual  long  
double  
getConversionFactor()  const  override  { return  conversionFactor; }

void  setConversionFactor(const  long  double  value)  {
factorconversion  =  valor; }

privado:  
long  double  conversionFactor{0.5}; };

En  el  lugar  del  código  de  producción  donde  se  usa  el  convertidor  de  divisas,  ahora  se  usa  la  interfaz
para  acceder  al  servicio.  Debido  a  esta  abstracción,  es  totalmente  transparente  para  el  código  del  cliente  qué  tipo  de  
implementación  se  usa  durante  el  tiempo  de  ejecución,  ya  sea  el  convertidor  de  moneda  real  o  su  Test  Double.

Listado  2­12.  El  encabezado  de  la  clase  que  usa  el  servicio.

#include  <memoria>

clase  Conversor  de  Moneda;

clase  UserOfConversionService  { public:  

UserOfConversionService()  =  eliminar;  
UserOfConversionService(const  std::shared_ptr<CurrencyConverter>&  conversionService);  void  hacerAlgo(); //  Más  
de  la  interfaz  de  clase  
pública  sigue  aquí...

privado:  
std::shared_ptr<CurrencyConverter>  conversionService; //...implementación  
interna... };

Listado  2­13.  Un  extracto  del  archivo  de  implementación.

UserOfConversionService::UserOfConversionService  (const  std::shared_  ptr<CurrencyConverter>&  
conversionService) :  conversionService(conversionService)  
{ }

void  UserOfConversionService::hacerAlgo()  {
long  double  conversionFactor  =  conversionService­>getConversionFactor(); // ...

24
Machine Translated by Google

Capítulo  2  ■  Construir  una  red  de  seguridad

En  una  prueba  unitaria  para  la  clase  UserOfConversionService,  el  caso  de  prueba  ahora  puede  pasar  
el  objeto  simulado  a  través  del  constructor  de  inicialización.  Por  otro  lado,  en  el  funcionamiento  normal  del  
software,  el  servicio  real  se  puede  pasar  a  través  del  constructor.  Esta  técnica  se  conoce  como  un  patrón  de  
diseño  denominado  Inyección  de  dependencia,  que  se  analiza  en  detalle  en  la  sección  homónima  del  capítulo  Patrón  de  diseño.

Listado  2­14.  Un  ejemplo  de  cómo  UserOfConversionService  obtiene  su  objeto  CurrencyConverter  requerido

std::shared_ptr<CurrencyConverter>  serviceToUse  =  std::make_shared<nombre  de  la  clase  deseada  aquí  */>();  

UserOfConversionService  usuario(servicioParaUsar); //  
La  instancia  de  UserOfConversionService  está  lista  para  usarse...  
user.doSomething();

25
Machine Translated by Google

CAPÍTULO  3

ser  de  principios

Aconsejaría  a  los  estudiantes  que  presten  más  atención  a  las  ideas  fundamentales  que  a  la  
última  tecnología.  La  tecnología  estará  obsoleta  antes  de  que  se  gradúen.  Las  ideas  
fundamentales  nunca  pasan  de  moda.
—David  L.  Parnás

En  este  capítulo,  presento  los  principios  más  importantes  y  fundamentales  del  software  bien  diseñado  y  elaborado.  Lo  
especial  de  estos  principios  es  que  no  están  vinculados  a  ciertos  paradigmas  de  programación  o  lenguajes  de  programación.  
Algunos  de  ellos  ni  siquiera  son  específicos  del  desarrollo  de  software.  Por  ejemplo,  el  principio  KISS  discutido  puede  ser  
relevante  para  muchas  áreas  de  la  vida:  en  términos  generales,  no  es  una  mala  idea  hacer  que  todo  sea  lo  más  simple  posible  
en  la  vida,  no  solo  el  desarrollo  de  software.
Es  decir,  no  debe  aprender  los  siguientes  principios  una  vez  y  luego  olvidarlos.  Estos  consejos  son
dado  para  que  lo  interiorices.  Estos  principios  son  tan  importantes  que,  idealmente,  deberían  convertirse  en  una  segunda  
naturaleza  para  todos  los  desarrolladores.  Y  muchos  de  los  principios  más  concretos  que  analizo  más  adelante  en  este  libro  
tienen  sus  raíces  en  los  siguientes  principios  básicos.

¿Qué  es  un  principio?
En  este  libro  encontrará  varios  principios  para  mejorar  el  código  C++  y  el  software  bien  diseñado.  Pero,  ¿qué  es  un  principio  
en  general?
Muchas  personas  tienen  principios  que  las  guían  a  lo  largo  de  su  vida.  Por  ejemplo,  si  estás  en  contra  de  comer  
carne  por  varias  razones,  eso  sería  un  principio.  Si  quiere  proteger  a  su  hijo,  bríndele  principios  a  lo  largo  del  camino,  
guiándolo  para  que  tome  las  decisiones  correctas  por  su  cuenta,  por  ejemplo,  "¡Ten  cuidado  y  no  hables  con  extraños!"  Con  
este  principio  en  mente,  el  niño  puede  deducir  el  comportamiento  correcto  en  ciertas  situaciones  específicas.
Un  principio  es  un  tipo  de  regla,  creencia  o  idea  que  te  guía.  Los  principios  a  menudo  se  relacionan  directamente  
con  los  valores  o  un  sistema  de  valores.  Por  ejemplo,  no  necesitamos  que  nos  digan  que  el  canibalismo  está  mal  porque  los  
humanos  tienen  un  valor  innato  con  respecto  a  la  vida  humana.  Y  como  otro  ejemplo,  el  Manifiesto  Agile  [Beck01]  contiene  doce  
principios  que  guían  a  los  equipos  de  proyecto  en  la  implementación  de  proyectos  Agile.
Los  principios  no  son  leyes  irrevocables.  No  están  tallados  en  piedra.  Las  violaciones  deliberadas  de  los  principios  a  
veces  son  necesarias  en  la  programación.  Si  tiene  muy  buenas  razones  para  violar  los  principios,  hágalo,  ¡pero  hágalo  con  
mucho  cuidado!  Debería  ser  una  excepción.
Algunos  de  los  siguientes  principios  básicos  son,  en  varios  puntos  más  adelante  en  el  libro,  revisados  y  profundizados.

©  Stephan  Roth  2017   27
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0_3
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

BESO

Todo  debe  hacerse  lo  más  simple  posible,  pero  no  más  simple.

—Albert  Einstein,  físico  teórico,  1879  ­  1955

KISS  es  un  acrónimo  de  "Mantenlo  simple,  estúpido"  o  "Mantenlo  simple  y  estúpido" (OK,  lo  sé,  hay  otros  significados  para  este  
acrónimo,  pero  estos  dos  son  los  más  comunes).  En  eXtreme  Programming  (XP),  este  principio  está  representado  por  una  práctica  
llamada  "Haz  lo  más  simple  que  pueda  funcionar" (DTSTTCPW).
El  principio  KISS  establece  que  la  simplicidad  debe  ser  un  objetivo  principal  en  el  desarrollo  de  software  y  que  se  debe  
evitar  la  complejidad  innecesaria.
Creo  que  KISS  es  uno  de  esos  principios  que  los  desarrolladores  suelen  olvidar  cuando  están  desarrollando  software.
Los  desarrolladores  de  software  tienden  a  escribir  el  código  de  forma  elaborada  y  hacen  las  cosas  más  complicadas  de  lo  que  
deberían  ser.  Lo  sé,  todos  somos  desarrolladores  excelentemente  capacitados  y  altamente  motivados,  y  sabemos  todo  sobre  
patrones  de  diseño  y  arquitectura,  marcos,  tecnologías,  herramientas  y  otras  cosas  interesantes  y  sofisticadas.  La  creación  de  
software  genial  no  es  nuestro  trabajo  de  9  a  5:  es  nuestra  misión  y  logramos  el  cumplimiento  a  través  de  nuestro  trabajo.
Pero  debe  tener  en  cuenta  que  cualquier  sistema  de  software  tiene  una  complejidad  intrínseca  que  ya  es  un  desafío  
en  sí  mismo.  Sin  duda,  los  problemas  complejos  a  menudo  requieren  un  código  complejo.  La  complejidad  intrínseca  no  se  
puede  reducir.  Este  tipo  de  complejidad  simplemente  está  ahí,  debido  a  los  requisitos  que  debe  cumplir  el  sistema.
Pero  sería  fatal  agregar  una  complejidad  casera  e  innecesaria  a  esta  complejidad  intrínseca.  Por  lo  tanto,  es  aconsejable  no  usar  
todas  las  características  sofisticadas  de  su  lenguaje  o  patrones  de  diseño  geniales  solo  porque  puede  hacerlo.  Por  otro  lado,  no  
exageres  en  la  sencillez.  Si  son  necesarias  diez  decisiones  en  un  caso  de  cambio,  así  es  como  es.
¡Mantén  tu  código  tan  simple  como  puedas!  Por  supuesto,  si  hay  requisitos  de  calidad  de  alta  prioridad  sobre
flexibilidad  y  extensibilidad,  debe  agregar  complejidad  para  cumplir  con  estos  requisitos.  Por  ejemplo,  puede  utilizar  el  conocido  
Patrón  de  estrategia  (consulte  el  Capítulo  9  sobre  patrones  de  diseño)  para  introducir  un  punto  de  variación  flexible  en  su  código  
cuando  los  requisitos  lo  exijan.  Pero  tenga  cuidado  y  agregue  solo  esa  cantidad  de  complejidad  que  facilita  las  cosas.

Centrarse  en  la  simplicidad  es  probablemente  una  de  las  cosas  más  difíciles  para  un  programador.
Y  es  una  experiencia  de  aprendizaje  de  por  vida.

—Adrian  Bolboaca  (@adibolb),  3  de  abril  de  2014,  en  Twitter

YAGNI

Siempre  implemente  las  cosas  cuando  realmente  las  necesite,  nunca  cuando  prevea  que  las  necesitará.

—Ron  Jeffries,  ¡NO  lo  vas  a  necesitar!  [Jeffries98]

Este  principio  está  estrechamente  relacionado  con  el  principio  KISS  discutido  anteriormente.  YAGNI  es  un  acrónimo  de  "¡No  lo  
vas  a  necesitar!"  A  veces  se  traduce  como  "¡No  lo  vas  a  necesitar!"  YAGNI  es  la  declaración  de
guerra  contra  la  generalización  especulativa  y  el  exceso  de  ingeniería.  Establece  que  no  debe  escribir  código  que  no  sea  necesario  
en  este  momento,  pero  que  podría  serlo  en  el  futuro.

28
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

Probablemente  todo  desarrollador  conoce  este  tipo  de  impulsos  tentadores  en  su  trabajo  diario:  “Tal  vez  podríamos
úsalo  más  tarde…”,  o  “Vamos  a  necesitar…” ¡ No,  no  lo  vas  a  necesitar!  En  cualquier  caso,  debe  resistirse  a  producir  algo  para  un  
posible  uso  posterior.  Puede  que  no  lo  necesites  después  de  todo.  Pero  si  ha  implementado  esa  cosa  innecesaria,  ha  perdido  el  tiempo  y  
el  código  se  ha  vuelto  más  complicado  de  lo  que  debería  ser.  Y  por  supuesto,  también  violas  el  principio  KISS.  ¡Las  peores  consecuencias  
podrían  ser  que  estas  piezas  de  código  para  el  futuro  tengan  errores  y  causen  problemas  graves!

Mi  consejo  es  este:  confíe  en  el  poder  de  refactorizar  y  construya  cosas  no  sin  antes  saber  que  son  realmente  necesarias.

SECO

Copiar  y  pegar  es  un  error  de  diseño.
—David  L.  Parnás

Aunque  este  principio  es  uno  de  los  más  importantes,  estoy  bastante  seguro  de  que  a  menudo  se  viola,  sin  querer  o  intencionalmente.  
SECO  es  un  acrónimo  de  "¡No  te  repitas!"  y  establece  que  debemos  evitar  la  duplicación,  porque  la  duplicación  es  mala.  A  veces,  este  
principio  también  se  conoce  como  "Una  vez  y  solo  una  vez" (OAOO).
La  razón  por  la  cual  la  duplicación  es  muy  peligrosa  es  obvia:  cuando  se  cambia  una  pieza,  sus  copias  deben  cambiarse  
en  consecuencia.  Y  no  tengas  grandes  esperanzas.  Es  una  apuesta  segura  que  el  cambio  ocurrirá.  Creo  que  es  innecesario  mencionar  
que  cualquier  pieza  copiada  será  olvidada  tarde  o  temprano  y  podemos  saludar  a  los  errores.
OK,  eso  es  todo,  ¿nada  más  que  decir?  Espera,  todavía  hay  algo  y  tenemos  que  ir  más  profundo.
En  su  brillante  libro,  The  Pragmatic  Programmer  [Hunt99],  Dave  Thomas  y  Andy  Hunt  afirman  que  aplicar  el  principio  DRY  significa  
que  tenemos  que  asegurarnos  de  que  “cada  pieza  de  conocimiento  debe  tener  una  representación  única,  inequívoca  y  autorizada  dentro  
de  un  sistema”.  Es  notable  que  Dave  y  Andy  no  mencionaron  explícitamente  el  código,  pero  hablan  sobre  el  conocimiento.  Y  el  
conocimiento  de  un  sistema  es  mucho  más  amplio  que  solo  su  código.  Por  ejemplo,  el  principio  DRY  también  es  válido  para  la  documentación,  
el  proyecto  y  los  planes  de  prueba,  o  los  datos  de  configuración  del  sistema.  ¡EL  SECO  afecta  a  todo!  Quizás  puedas  imaginar  que  el  
cumplimiento  estricto  de  este  principio  no  es  tan  fácil  como  parece  a  primera  vista.

Ocultación  de  información

La  ocultación  de  información  es  un  principio  fundamental  y  conocido  desde  hace  mucho  tiempo  en  el  desarrollo  de  software.  Se  
documentó  por  primera  vez  en  el  artículo  seminal  "Sobre  los  criterios  que  se  utilizarán  en  la  descomposición  de  sistemas  en  módulos".
[Parnas72]  escrito  por  el  destacado  David  L.  Parnas  en  1972.
El  principio  establece  que  una  pieza  de  código  que  llama  a  otra  pieza  de  código  no  debe  conocer  los  aspectos  internos  de  esa  otra  
pieza  de  código.  Esto  hace  posible  cambiar  partes  internas  de  la  pieza  de  código  llamada  sin  verse  obligado  a  cambiar  la  pieza  de  
código  de  llamada  en  consecuencia.
David  L.  Parnas  describe  la  ocultación  de  información  como  el  principio  básico  para  descomponer  sistemas  en  módulos.  
Parnas  argumentó  que  la  modularización  del  sistema  debería  involucrar  la  ocultación  de  decisiones  de  diseño  difíciles  o  decisiones  de  
diseño  que  probablemente  cambien.  Cuantas  menos  partes  internas  exponga  una  unidad  de  software  (por  ejemplo,  una  clase  o  
componente)  a  su  entorno,  menor  será  el  acoplamiento  entre  la  implementación  de  la  unidad  y  sus  clientes.
Como  resultado,  los  cambios  en  la  implementación  interna  de  una  unidad  de  software  no  se  propagarán  a  su  entorno.
Las  ventajas  de  ocultar  información  son  numerosas:

•Limitación  de  las  consecuencias  de  los  cambios  en  los  módulos

•Influencia  mínima  en  otros  módulos  si  es  necesaria  una  corrección  de  errores

• Aumento  significativo  de  la  reutilización  de  los  módulos.

• Mejor  capacidad  de  prueba  de  los  módulos.

29
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

La  ocultación  de  información  a  menudo  se  confunde  con  la  encapsulación,  pero  no  es  lo  mismo.  Sé  que  ambos  términos  
se  han  usado  como  sinónimos  en  muchos  libros  destacados,  pero  no  estoy  de  acuerdo.  La  ocultación  de  información  es  un  
principio  de  diseño  para  ayudar  a  los  desarrolladores  a  encontrar  buenos  módulos.  El  principio  funciona  en  múltiples  niveles  de  
abstracción  y  despliega  su  efecto  positivo,  especialmente  en  grandes  sistemas.
La  encapsulación  es  a  menudo  una  técnica  dependiente  del  lenguaje  de  programación  para  restringir  el  acceso  a  las  entrañas  
de  un  módulo.  Por  ejemplo,  en  C++  puede  preceder  una  lista  de  miembros  de  la  clase  con  la  palabra  clave  privada  para  garantizar  que  
no  se  pueda  acceder  a  ellos  desde  fuera  de  la  clase.  Pero  solo  porque  usamos  este  tipo  de  guardias  para  el  control  de  acceso,  todavía  
estamos  lejos  de  ocultar  la  información  automáticamente.  La  encapsulación  facilita,  pero  no  garantiza,  la  ocultación  de  
información.
El  siguiente  ejemplo  de  código  muestra  una  clase  encapsulada  con  poca  información  oculta:

Listado  3­1.  Una  clase  para  la  dirección  automática  de  puertas  (extracto)

class  AutomaticDoor  { público:  
enum  
class  State  { cerrado  =  1,  
abriendo,  
abierto,  

cerrando };

privado:  
Estado  
estatal; // ...más  atributos  aquí...

público:  
Estado  getState()  const; // ...más  
funciones  miembro  aquí... };

Esto  no  es  ocultar  información,  porque  partes  de  la  implementación  interna  de  la  clase  están  expuestas  al  entorno,  incluso  si  la  
clase  parece  estar  bien  encapsulada.  Tenga  en  cuenta  el  tipo  del  valor  de  retorno  de  getState.
Los  clientes  que  usan  esta  clase  requieren  la  clase  de  enumeración  Estado,  como  lo  demuestra  el  siguiente  ejemplo:

Listado  3­2.  Un  ejemplo  de  cómo  se  debe  usar  AutomaticDoor  para  consultar  el  estado  actual  de  la  puerta

#include  "PuertaAutomática.h"

int  main()  
{ PuertaAutomáticaPuertaAutomática;  
PuertaAutomática::Estado  estadoPuertas  =  PuertaAutomática.getEstado();  if  (doorsState  
==  AutomaticDoor::State::closed)  { //  hacer  algo... }  return  0; }

30
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

CLASE  DE  ENUMERACIÓN  (ESTRUCTURA)  [C++11]

Con  C++11  también  ha  habido  una  innovación  en  los  tipos  de  enumeraciones.  Para  la  compatibilidad  hacia  abajo  con  los  estándares  
anteriores  de  C++,  todavía  existe  la  conocida  enumeración  con  su  palabra  clave  enum.  Desde  C  ++  11,  también  existen  las  clases  
de  enumeración,  respectivamente,  las  estructuras  de  enumeración.

Un  problema  con  esas  antiguas  enumeraciones  de  C++  es  que  exportan  sus  literales  de  enumeración  al  espacio  de  nombres  
circundante,  lo  que  provoca  conflictos  de  nombres,  como  en  el  siguiente  ejemplo:

const  std::oso  de  cuerda; // ...y  
en  otros  lugares  del  mismo  espacio  de  nombres...  enum  
Animal  { perro,  venado,  gato,  pájaro,  oso }; //  error:  'oso'  redeclarado  como  otro  tipo  de  símbolo

Además,  las  enumeraciones  antiguas  de  C++  se  convierten  implícitamente  a  int,  lo  que  provoca  errores  sutiles  cuando  no  se  espera  
o  no  se  desea  dicha  conversión:

enum  Animal  {perro,  venado,  gato,  pájaro,  oso};
animal  animal  =  perro;  int  
unNúmero  =  animal; //  Conversión  implícita:  funciona

Estos  problemas  ya  no  existen  cuando  se  usan  clases  de  enumeración,  también  llamadas  "nuevas  enumeraciones"  o  "enumeraciones  
fuertes".  Sus  literales  de  enumeración  son  locales  a  la  enumeración  y  sus  valores  no  se  convierten  implícitamente  a  otros  
tipos  (como  a  otra  enumeración  o  un  int).

const  std::oso  de  cuerda; // ...y  
en  otros  lugares  del  mismo  espacio  de  nombres...  enum  
class  Animal  { perro,  venado,  gato,  pájaro,  oso }; //  No  hay  conflicto  con  la  cadena  nombrada
'oso'
Animal  animal  =  Animal::perro;  int  
unNúmero  =  animal; //  ¡Error  del  compilador!

Se  recomienda  enfáticamente  usar  clases  de  enumeración  en  lugar  de  enumeraciones  simples  y  antiguas  para  un  programa  C++  
moderno,  porque  hace  que  el  código  sea  más  seguro.  Y  debido  a  que  las  clases  de  enumeración  también  son  clases,  se  pueden  
declarar  hacia  adelante.

¿Qué  sucederá  si  se  debe  cambiar  la  implementación  interna  de  AutomaticDoor  y  la  enumeración
¿El  estado  de  la  clase  se  elimina  de  la  clase?  Es  fácil  ver  que  tendrá  un  impacto  significativo  en  el  código  del  cliente.
Dará  como  resultado  cambios  en  todas  partes  donde  se  use  la  función  miembro  AutomaticDoor::getState().
El  siguiente  es  un  AutomaticDoor  encapsulado  con  buena  ocultación  de  información:

Listado  3­3.  Una  clase  mejor  diseñada  para  la  dirección  automática  de  puertas

class  PuertaAutomática  
{ public:  
bool  isClosed()  const;  bool  
esApertura()  const;

31
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

bool  isOpen()  const;  bool  
se  cierra()  const; // ...más  
operaciones  aquí...

privado:  
enum  class  Estado  
{cerrado  =  1,  
apertura,  

apertura,  
cierre};

Estado  
estado; // ...más  atributos  aquí... };

Listado  3­4.  Un  ejemplo  de  cómo  se  puede  usar  la  clase  elegante  AutomaticDoor  después  de  cambiarla

#include  "PuertaAutomática.h"

int  main()  
{ PuertaAutomáticaPuertaAutomática;  
if  (automaticDoor.isClosed())  { //  hacer  
algo... }  return  0; }

Ahora  es  mucho  más  fácil  cambiar  las  entrañas  de  AutomaticDoor.  El  código  del  cliente  ya  no  depende  de  las  
partes  internas  de  la  clase.  Ahora  puede  eliminar  el  estado  de  enumeración  y  reemplazarlo  por  otro  tipo  de  
implementación  sin  que  ningún  usuario  de  la  clase  lo  note.

Cohesión  fuerte
Un  consejo  general  en  el  desarrollo  de  software  es  que  cualquier  entidad  de  software  (sinónimos:  módulo,  
componente,  unidad,  clase,  función…)  debe  tener  una  fuerte  (o  alta)  cohesión.  En  términos  muy  generales,  la  cohesión  es  
fuerte  cuando  el  módulo  hace  un  trabajo  bien  definido.
Para  profundizar  en  este  principio,  echemos  un  vistazo  a  dos  ejemplos  donde  la  cohesión  es  débil,  comenzando  con  
la  Figura  3­1.

32
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

Figura  3­1.  MyModule  tiene  demasiadas  responsabilidades,  y  esto  genera  muchas  dependencias  desde  y  hacia  otros  
módulos.

En  esta  ilustración  de  la  modularización  de  un  sistema  arbitrario,  tres  aspectos  diferentes  del  negocio
dominio  se  colocan  dentro  de  un  solo  módulo.  Los  aspectos  A,  B  y  C  no  tienen  nada,  o  casi  nada,  en  común,  
pero  los  tres  están  ubicados  dentro  de  MyModule.  Una  mirada  al  código  del  módulo  podría  revelar  que  las  
funciones  de  A,  B  y  C  están  operando  en  piezas  de  datos  diferentes  y  completamente  independientes.
Ahora  eche  un  vistazo  a  todas  las  flechas  discontinuas  en  esa  imagen.  Cada  uno  de  ellos  es  una  dependencia.  
El  elemento  en  la  cola  de  dicha  flecha  requiere  el  elemento  en  la  punta  de  la  flecha  para  su  implementación.  En  este  
caso,  cualquier  otro  módulo  del  sistema  que  quiera  utilizar  los  servicios  ofrecidos  por  A,  B  o  C,  se  hará  dependiente  de  
todo  el  módulo  MyModule.  El  principal  inconveniente  de  un  diseño  de  este  tipo  es  obvio:  generará  demasiadas  
dependencias  y  la  capacidad  de  mantenimiento  se  desvanecerá.
Para  aumentar  la  cohesión,  los  aspectos  de  A,  B  y  C  deben  separarse  y  trasladarse  a  sus  propios  módulos  
(Figura  3­2).

33
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

Figura  3­2.  Alta  cohesión:  los  aspectos  A,  B  y  C  previamente  mezclados  se  han  separado  en  módulos  discretos

Ahora  es  fácil  ver  que  cada  uno  de  estos  módulos  tiene  muchas  menos  dependencias  que  nuestro  antiguo  MyModule.  
Está  claro  que  A,  B  y  C  no  tienen  nada  que  ver  entre  sí  directamente.  El  único  módulo,  que  depende  de  los  tres  módulos  A,  
B  y  C,  es  el  denominado  Módulo  1.
Otra  forma  de  cohesión  débil  se  llama  Shot  Gun  Anti­Pattern.  Creo  que  es  de  conocimiento  general  que  un
La  escopeta  es  un  arma  de  fuego  que  dispara  una  gran  cantidad  de  pequeños  perdigones  esféricos.  El  arma  tiene  
típicamente  una  gran  dispersión.  En  el  desarrollo  de  software,  esta  metáfora  se  utiliza  para  expresar  que  cierto  aspecto  
del  dominio,  o  idea  lógica  única,  está  muy  fragmentado  y  distribuido  en  muchos  módulos.  La  Figura  3­3  representa  tal  situación.

34
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

Figura  3­3.  El  Aspecto  A  estaba  disperso  en  cinco  módulos.

Incluso  con  esta  forma  de  cohesión  débil,  surgieron  muchas  dependencias  desfavorables.  el  distribuido
los  fragmentos  del  Aspecto  A  deben  trabajar  en  estrecha  colaboración.  Eso  significa  que  cada  módulo  que  implementa  un  subconjunto  
del  Aspecto  A  debe  interactuar  al  menos  con  otro  módulo  que  contenga  otro  subconjunto  del  Aspecto  A.  Esto  genera  una  gran  cantidad  
de  dependencias  transversales  a  lo  largo  del  diseño.  En  el  peor  de  los  casos,  puede  dar  lugar  a  dependencias  cíclicas,  como  entre  los  
módulos  1  y  3,  o  entre  los  módulos  6  y  7.  Esto  tiene,  una  vez  más,  un  impacto  negativo  en  la  capacidad  de  mantenimiento  y  la  
capacidad  de  ampliación.  Y,  por  supuesto,  la  capacidad  de  prueba  de  este  diseño  es  extremadamente  mala.
Este  tipo  de  diseño  conducirá  a  algo  que  se  llama  Cirugía  de  escopeta.  Cierto  tipo  de  cambio  con  respecto  al  Aspecto  A  
lleva  a  realizar  muchos  cambios  pequeños  en  muchos  módulos.  Eso  es  realmente  malo  y  debe  evitarse.  Tenemos  que  arreglar  esto  
juntando  todas  las  partes  del  código  que  son  fragmentos  del  mismo  aspecto  lógico  en  un  solo  módulo  cohesivo.

Existen  otros  principios,  por  ejemplo,  el  Principio  de  Responsabilidad  Única  (SRP)  del  diseño  orientado  a  objetos  (consulte  el  
Capítulo  6),  que  fomentan  una  alta  cohesión.  La  alta  cohesión  a  menudo  se  correlaciona  con  un  acoplamiento  flojo  y  viceversa.

Bajo  acoplamiento
Considere  el  siguiente  pequeño  ejemplo:

Listado  3­5.  Un  interruptor  que  puede  encender  y  apagar  una  lámpara

lámpara  de  clase  
{ público:  
vacío  en  ()  {

35
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

//...
}

anular  apagado()  
{ //...

} };

Interruptor  de  clase  
{ privado:
Lámpara  y  
lámpara;  estado  booleano  {falso};

público:
Interruptor  (lámpara  y  lámpara):  lámpara  (lámpara)  {}

void  toggle()  { if  
(estado)  { estado  
=  falso;  

lampara.apagada(); }  
más  { estado  
=  verdadero;  lámpara.encendido(); }

} };

Básicamente,  este  fragmento  de  código  funcionará.  Primero  puede  crear  una  instancia  de  la  clase  Lamp.  Luego,  esto  se  pasa  por  
referencia  al  instanciar  la  clase  Switch.  Visualizado  con  UML,  este  pequeño  ejemplo  se  vería  así  en  la  Figura  3­4.

Figura  3­4.  Un  diagrama  de  clase  de  interruptor  y  lámpara

¿Cuál  es  el  problema  con  este  diseño?
El  problema  es  que  nuestro  Switch  contiene  una  referencia  directa  a  la  clase  concreta  Lamp.  En  otras  palabras:
el  interruptor  sabe  que  hay  una  lámpara.
Tal  vez  usted  argumentará:  “Bueno,  pero  ese  es  el  propósito  del  cambio.  Tiene  que  encender  y  apagar  las  lámparas”.  me  gustaría
diga:  Sí,  si  eso  es  lo  único  que  debe  hacer  el  interruptor,  entonces  este  diseño  podría  ser  adecuado.  Pero  vaya  a  una  tienda  de  bricolaje  
y  eche  un  vistazo  a  los  interruptores  que  puede  comprar  allí.  ¿Saben  que  existen  las  lámparas?
¿Y  qué  piensas  sobre  la  capacidad  de  prueba  de  este  diseño?  ¿Se  puede  probar  el  interruptor  de  forma  independiente  como  se  
requiere  para  la  prueba  unitaria?  No,  esto  no  es  posible.  ¿Y  qué  haremos  cuando  el  interruptor  tenga  que  encender  no  solo  una  lámpara,  
sino  un  ventilador  o  una  persiana  eléctrica?
En  nuestro  ejemplo  anterior,  el  interruptor  y  la  lámpara  están  estrechamente  acoplados.
En  el  desarrollo  de  software,  se  debe  buscar  un  acoplamiento  débil  (también  conocido  como  acoplamiento  bajo  o  débil)  entre  
módulos.  Eso  significa  que  debe  construir  un  sistema  en  el  que  cada  uno  de  sus  módulos  tenga,  o  haga  uso  de,  poco  o  ningún  conocimiento  
de  las  definiciones  de  otros  módulos  separados.

36
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

La  clave  para  el  acoplamiento  flojo  en  el  desarrollo  de  software  son  las  interfaces.  Una  interfaz  declara  
características  de  comportamiento  accesibles  públicamente  de  una  clase  sin  comprometerse  con  una  implementación  
particular  de  esa  clase.  Una  interfaz  es  como  un  contrato.  Las  clases  que  implementan  una  interfaz  se  comprometen  a  
cumplir  el  contrato,  es  decir,  estas  clases  deben  proporcionar  implementaciones  para  las  firmas  de  métodos  de  la  interfaz.
En  C++,  las  interfaces  se  implementan  mediante  clases  abstractas,  como  esta:

Listado  3­6.  La  interfaz  conmutable

class  Switchable  { public:  
virtual  
void  on()  =  0;  vacío  virtual  
apagado()  =  0; };

La  clase  Switch  ya  no  contiene  una  referencia  a  la  Lámpara.  En  su  lugar,  contiene  una  referencia  a  nuestro
nueva  clase  de  interfaz  Conmutable.

Listado  3­7.  La  clase  Switch  modificada,  donde  Lamp  se  ha  ido

Interruptor  de  clase  
{ privado:
Conmutable  y  conmutable;  
estado  booleano  {falso};

público:
Interruptor  (conmutable  y  conmutable):  conmutable  (conmutable)  {}

void  toggle()  { if  
(estado)  { estado  
=  falso;  
conmutable.off(); }  más  
{ estado  =  
verdadero;  
conmutable.on(); }

} };

La  clase  Lamp  implementa  nuestra  nueva  interfaz.

Listado  3­8.  La  clase  'Lámpara'  implementa  la  interfaz  'Conmutable'

Lámpara  de  clase :  conmutable  pública  
{ pública:  
invalidar  ()  anular  { // ...

anular  off()  invalidar  { // ...

} };

37
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

Expresado  en  UML,  nuestro  nuevo  diseño  se  parece  al  de  la  figura  3­5.

Figura  3­5.  Interruptor  y  lámpara  acoplados  libremente  a  través  de  una  interfaz

Las  ventajas  de  tal  diseño  son  obvias.  Switch  es  completamente  independiente  de  las  clases  concretas  que  debe  controlar.  
Además,  Switch  se  puede  probar  de  forma  independiente  al  proporcionar  una  prueba  doble  que  implementa  la  interfaz  Switchable.  
¿Quieres  controlar  un  ventilador  en  lugar  de  una  lámpara?  No  hay  problema:  este  diseño  está  abierto  para  la  extensión.  
Simplemente  cree  una  clase  Ventilador  u  otras  clases  que  representen  dispositivos  eléctricos  que  implementen  la  interfaz  
Conmutable,  como  se  muestra  en  la  Figura  3­6.

Figura  3­6.  A  través  de  una  interfaz,  un  interruptor  puede  controlar  diferentes  clases  de  dispositivos  eléctricos

La  atención  al  acoplamiento  flojo  puede  proporcionar  un  alto  grado  de  autonomía  para  los  módulos  individuales  de  un  sistema.
El  principio  puede  ser  efectivo  en  diferentes  niveles:  tanto  en  los  módulos  más  pequeños  como  en  el  nivel  de  arquitectura  del  
sistema  para  componentes  grandes.  La  alta  cohesión  fomenta  el  acoplamiento  flexible,  porque  un  módulo  con  una  responsabilidad  
claramente  definida  generalmente  depende  de  menos  colaboradores.

38

www.allitebooks.com
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

Tenga  cuidado  con  las  optimizaciones

La  optimización  prematura  es  la  raíz  de  todos  los  males  (o  al  menos  de  la  mayor  parte)  en  la  programación.

—Donald  E.  Knuth,  informático  estadounidense  [Knuth74]

He  visto  desarrolladores  que  inician  optimizaciones  que  desperdician  el  tiempo  solo  con  vagas  ideas  de  gastos  generales,  pero  
sin  saber  realmente  dónde  se  pierde  el  rendimiento.  A  menudo  manipulaban  las  instrucciones  individuales;  o  trató  de  optimizar  
pequeños  bucles  locales  para  exprimir  hasta  la  última  gota  de  rendimiento.  Solo  como  nota  al  pie,  uno  de  estos  
programadores  de  los  que  estoy  hablando  era  yo.
El  éxito  de  estas  actividades  fue  generalmente  marginal.  Las  ventajas  de  rendimiento  esperadas  por  lo  general
no  surgió.  Al  final  fue  sólo  una  pérdida  de  tiempo  precioso.  Por  el  contrario,  a  menudo  la  capacidad  de  comprensión  y  
mantenimiento  del  código  supuestamente  optimizado  sufre  drásticamente.  Particularmente  malo:  a  veces  incluso  sucede  que  
sutilmente  se  deslizan  errores  en  el  código  durante  tales  medidas  de  optimización.  Mi  consejo  es  el  siguiente:  siempre  que  no  haya  
requisitos  de  rendimiento  explícitos  que  satisfacer,  manténgase  alejado  de  las  optimizaciones.
La  comprensibilidad  y  mantenibilidad  de  nuestro  código  debe  ser  nuestro  primer  objetivo.  Y  como  explico  en  la  sección  “¡Pero  
el  Call  Time  Overhead!”  En  el  capítulo  4,  los  compiladores  son  muy  buenos  hoy  en  día  para  optimizar  el  código.
Siempre  que  sienta  el  deseo  de  optimizar  algo,  piense  en  YAGNI.
Solo  cuando  los  requisitos  de  desempeño  explícitos,  que  son  solicitados  expresamente  por  una  parte  interesada,  no  son
satisfecho,  si  entras  en  acción.  Pero  primero  debe  analizar  cuidadosamente  dónde  se  pierde  el  rendimiento.  No  haga  ninguna  
optimización  solo  sobre  la  base  de  una  intuición.  Por  ejemplo,  puede  usar  un  generador  de  perfiles  para  averiguar  dónde  están  los  
cuellos  de  botella.  Después  del  uso  de  una  herramienta  de  este  tipo,  los  desarrolladores  suelen  sorprenderse  de  que  el  rendimiento  
se  pierda  en  una  ubicación  completamente  diferente  a  la  que  se  suponía  originalmente.

■  Nota  Un  Profiler  es  una  herramienta  para  el  análisis  dinámico  de  programas.  Mide,  entre  otras  métricas,  la  frecuencia  y  duración  

de  las  llamadas  a  funciones.  La  información  de  perfil  recopilada  se  puede  utilizar  para  ayudar  a  la  optimización  del  programa.

Principio  de  menor  asombro  (PLA)
El  Principio  de  menor  asombro  (POLA/PLA),  también  conocido  como  Principio  de  menor  sorpresa  (POLS),  es  bien  conocido  en  el  
diseño  y  la  ergonomía  de  la  interfaz  de  usuario.  El  principio  establece  que  el  usuario  no  debe  sorprenderse  por  las  respuestas  
inesperadas  de  la  interfaz  de  usuario.  El  usuario  no  debe  confundirse  con  controles  que  aparecen  o  desaparecen,  mensajes  de  error  
confusos,  reacciones  inusuales  en  secuencias  de  pulsaciones  de  teclas  establecidas  (recuerde:  Ctrl  +  C  es  el  estándar  de  facto  
para  copiar  aplicaciones  en  sistemas  operativos  Windows,  y  no  para  salir  de  un  programa),  o  otro  comportamiento  inesperado.

Este  principio  también  puede  trasladarse  bien  al  diseño  de  API  en  el  desarrollo  de  software.  Llamar  a  una  función  no  debería  
sorprender  a  la  persona  que  llama  con  un  comportamiento  inesperado  o  efectos  secundarios  misteriosos.  Una  función  debe  hacer  
exactamente  lo  que  implica  su  nombre  de  función  (consulte  la  sección  sobre  "Nombramiento  de  funciones"  en  el  Capítulo  4).  Por  
ejemplo,  llamar  a  un  getter  en  una  instancia  de  una  clase  no  debería  modificar  el  estado  interno  de  ese  objeto.

La  regla  de  los  boy  scouts
Este  principio  es  acerca  de  usted  y  su  comportamiento.  Dice  lo  siguiente:  Siempre  deje  el  campamento  más  limpio  de  lo  que  lo  
encontró.

39
Machine Translated by Google

Capítulo  3  ■  Tenga  principios

Los  boy  scouts  tienen  muchos  principios.  Uno  de  sus  principios  establece  que  deben  limpiar  un  desorden  o  contaminación  
en  el  medio  ambiente  de  inmediato,  una  vez  que  han  encontrado  cosas  tan  malas.  Como  artesanos  de  software  responsables,  
debemos  aplicar  este  principio  a  nuestro  trabajo  diario.  Siempre  que  encontremos  algo  en  un  fragmento  de  código  que  deba  
mejorarse,  o  que  sea  un  mal  olor  de  código,  debemos  corregirlo  de  inmediato.  Y  no  importa  quién  fue  el  autor  original  de  este  código.

La  ventaja  de  este  comportamiento  es  que  evitamos  continuamente  el  deterioro  de  nuestro  código.  Si  todos  nos  comportamos  
de  esta  manera,  el  código  simplemente  no  podría  pudrirse.  La  tendencia  de  la  creciente  entropía  del  software  tiene  pocas  posibilidades  
de  dominar  nuestro  sistema.  Y  la  mejora  no  tiene  por  qué  ser  gran  cosa.  Puede  ser  una  limpieza  muy  pequeña,  por  ejemplo:

•Renombrar  una  clase,  variable,  función  o  método  con  un  nombre  incorrecto  (consulte  la  sección
“Buenos  nombres  y  denominación  de  funciones”  en  el  Capítulo  4).

•Descomponer  las  entrañas  de  una  función  grande  en  partes  más  pequeñas  (ver  la  sección  “Que  sean  
pequeños”  en  el  Capítulo  4).

•  Eliminar  un  comentario  haciendo  que  el  fragmento  de  código  comentado  se  explique  por  sí  mismo
(ver  apartado  “Evitar  Comentarios”  en  el  Capítulo  4).

•  Limpiar  un  complejo  y  desconcertante  complejo  if­else.

•Eliminar  un  poco  de  código  duplicado  (consulte  la  sección  sobre  el  principio  SECO  en  este
capítulo).

Dado  que  la  mayoría  de  estas  mejoras  son  refactorizaciones  de  código,  es  esencial  contar  con  una  sólida  red  de  seguridad  que  
consista  en  buenas  pruebas  unitarias,  como  se  describe  en  el  Capítulo  2.  Sin  pruebas  unitarias  implementadas,  no  puede  estar  seguro  
de  no  romper  algo.
Además  de  una  buena  cobertura  de  pruebas  unitarias,  todavía  necesitamos  una  cultura  especial  en  nuestro  equipo:  propiedad  colectiva  del  código.
La  propiedad  colectiva  del  código  significa  que  realmente  debemos  trabajar  como  comunidad.  Cada  miembro  del  equipo,  en  
cualquier  momento,  puede  realizar  un  cambio  o  una  extensión  en  cualquier  pieza  de  código.  No  debe  haber  una  actitud  como  “Este  
es  el  código  de  Peter,  y  ese  es  el  módulo  de  Fred.  ¡Yo  no  los  toco!”  Debe  considerarse  un  valor  alto  que  otras  personas  puedan  
hacerse  cargo  del  código  que  escribimos.  Nadie  en  un  equipo  real  debería  tener  miedo  o  tener  que  obtener  permiso  para  limpiar  el  
código  o  agregar  nuevas  funciones.  Con  una  cultura  de  propiedad  colectiva  del  código,  la  Regla  Boy  Scout  funcionará  bien.

40
Machine Translated by Google

CAPÍTULO  4

Conceptos  básicos  de  C++  limpio

Como  ya  expliqué  en  la  Introducción  de  este  libro  (ver  Capítulo  1),  mucho  código  C++  no  está  limpio.  En  muchos  proyectos,  
la  entropía  del  software  ha  tomado  la  delantera.  Incluso  si  se  trata  de  un  proyecto  de  desarrollo  en  curso,  por  ejemplo,  con  una  pieza  
de  software  en  mantenimiento,  gran  parte  del  código  base  suele  ser  muy  antiguo.  El  código  se  ve  como  fue  escrito  en  el  siglo  
pasado.  ¡Esto  no  es  sorprendente,  porque  la  mayor  parte  de  ese  código  fue  escrito  en  el  siglo  pasado!  Hay  muchos  proyectos  
con  un  largo  ciclo  de  vida,  que  tienen  sus  raíces  en  los  años  90  o  incluso  en  los  80.  Además,  muchos  programadores  simplemente  
copian  fragmentos  de  código  de  proyectos  heredados  y  los  modifican  para  hacer  las  cosas.

Algunos  programadores  tratan  el  lenguaje  como  una  de  muchas  herramientas.  No  ven  ninguna  razón  para  mejorar  
algo,  porque  lo  que  improvisan  funciona  de  alguna  manera.  No  debería  ser  así  porque  conducirá  rápidamente  a  una  mayor  
entropía  del  software  y  el  proyecto  se  convertirá  en  un  gran  desastre  más  rápido  de  lo  que  piensas.
En  este  capítulo  describo  los  conceptos  básicos  generales  de  C++  limpio.  Estas  son  a  veces  cosas  universales  que  a  menudo  
son  independientes  del  lenguaje  de  programación.  Por  ejemplo,  dar  un  buen  nombre  es  fundamental  en  todos  los  lenguajes  de  
programación.  Varios  otros  aspectos,  como  la  corrección  de  constantes,  el  uso  de  punteros  inteligentes  o  las  grandes  ventajas  de  
la  semántica  de  movimiento,  son  específicos  de  C++.
Pero  antes  de  discutir  temas  específicos,  quiero  señalar  un  consejo  general:

Si  aún  no  lo  está  haciendo,  ¡comience  a  usar  C++  11  (o  superior)  ahora!

Con  el  nuevo  estándar  que  surgió  en  2011,  C++  se  ha  mejorado  de  muchas  maneras  y  algunas  características
de  C++11,  pero  también  de  los  siguientes  estándares  C++14  y  C++17,  son  demasiado  útiles  para  ignorarlos.  Y  no  se  trata  sólo  
de  rendimiento.  El  lenguaje  definitivamente  se  ha  vuelto  mucho  más  fácil  de  usar  e  incluso  se  ha  vuelto  más  poderoso.  C++11  no  
solo  puede  hacer  que  su  código  sea  más  corto,  más  claro  y  más  fácil  de  leer:  puede  aumentar  su  productividad.  Además,  
las  características  de  este  estándar  de  lenguaje  y  sus  sucesores  le  permiten  escribir  un  código  más  correcto  y  seguro  para  las  
excepciones.
Pero  ahora  exploremos  los  elementos  clave  de  C++  limpio  y  moderno  paso  a  paso...

buenos  nombres

Los  programas  deben  estar  escritos  para  que  la  gente  los  lea,  y  solo  de  manera  incidental  para  que  las  máquinas  los  ejecuten.

—Hal  Abelson  y  Gerald  Jay  Sussman,  1984

©  Stephan  Roth  2017   41
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0_4
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

El  siguiente  fragmento  de  código  fuente  está  tomado  del  conocido  Apache  OpenOffice  versión  3.4.1,  un  paquete  de  software  
de  oficina  de  código  abierto.  Apache  OpenOffice  tiene  una  larga  historia,  que  se  remonta  al  año  1984.
Desciende  de  Oracles  OpenOffice.org  (OOo),  que  era  una  versión  de  código  abierto  del  anterior  StarOffice.
En  2011,  Oracle  detuvo  el  desarrollo  de  OpenOffice.org,  despidió  a  todos  los  desarrolladores  y  aportó  el  código  y  las  marcas  
registradas  a  Apache  Software  Foundation.  Por  lo  tanto,  sea  tolerante  y  tenga  en  cuenta  que  Apache  Software  Foundation  ha  
heredado  una  bestia  antigua  de  casi  30  años  y  una  gran  deuda  técnica.

Listado  4­1.  Un  extracto  del  código  fuente  de  OpenOffice  3.4.1  de  Apache

//  Construyendo  la  estructura  de  información  para  elementos  individuales
SbxInfo*  ProcessWrapper::GetInfo( abreviado  nIdx )  {

Métodos*  p  =  &pMetodos[ nIdx ];
//  Wenn  mal  eine  Hilfedatei  zur  Verfuegung  steht:
//  SbxInfo*  pResultInfo  =  new  SbxInfo( Hilfedateiname,  p­>nHelpId );
SbxInfo*  pResultInfo  =  nuevo  SbxInfo;  corto  nPar  
=  p­>nArgs  &  _ARGSMASK;  for( corto  i  =  0;  i  <  
nPar;  i++ )  {

p++;
String  aMethodName( p­>pName,  RTL_TEXTENCODING_ASCII_US );  sal_uInt16  
nInfoFlags  =  (p­>nArgs  >>  8)  &  0x03;  if( p­>nArgs  &  _OPT )  
nInfoFlags  |=  SBX_OPCIONAL;  
pResultInfo­>AddParam( aMethodName,  
p­>eType,  nInfoFlags );

}  devuelve  pResultInfo;
}

Tengo  una  pregunta  simple  para  usted:  ¿ Qué  hace  esta  función?
Parece  fácil  dar  una  respuesta  a  primera  vista,  porque  el  fragmento  de  código  es  pequeño  (menos  de  20  LOC)  y  la  
sangría  está  bien.  Pero,  de  hecho,  no  es  posible  decir  de  un  vistazo  lo  que  realmente  hace  esta  función,  y  la  razón  de  esto  
radica  no  solo  en  el  dominio  que  puede  ser  desconocido  para  usted.
Este  fragmento  de  código  abreviado  tiene  muchos  malos  olores  (p.  ej.,  código  comentado,  comentarios  en  alemán,  
literales  mágicos  como  0x03,  etc.),  pero  un  problema  importante  es  la  mala  denominación.  El  nombre  de  la  función  GetInfo()  
es  muy  abstracto  y  nos  da  una  vaga  idea  de  lo  que  realmente  hace  esta  función.  Además,  el  nombre  del  espacio  de  
nombres  ProcessWrapper  no  es  muy  útil.  ¿Quizás  pueda  usar  esta  función  para  recuperar  información  sobre  un  proceso  en  
ejecución?  Bueno,  ¿no  sería  RetrieveProcessInformation()  un  nombre  mucho  mejor?
Después  de  un  análisis  de  la  implementación  de  la  función,  también  notará  que  el  nombre  es  engañoso,  porque  
GetInfo()  no  es  solo  un  captador  simple  como  podría  sospechar.  También  hay  algo  creado  con  el  nuevo  operador.  Tal  vez  
también  notó  el  comentario  sobre  la  función  que  habla  sobre  construir,  y  no  solo  obtener  algo.  En  otras  palabras,  el  sitio  de  la  
llamada  recibirá  un  recurso  que  se  asignó  en  el  montón  y  debe  cuidarlo.  Para  enfatizar  este  hecho,  ¿no  sería  mucho  mejor  un  
nombre  como  CreateProcessInformation()?

A  continuación,  observe  el  argumento  y  el  valor  de  retorno  de  la  función.  ¿Qué  es  SbxInfo?  ¿Qué  es  nIdx?
Tal  vez  el  argumento  nIdx  tenga  un  valor  que  se  usa  para  acceder  a  un  elemento  en  una  estructura  de  datos  (es  decir,  un  
índice),  pero  eso  sería  solo  una  suposición.  De  hecho,  no  lo  sabemos  exactamente.

42
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Los  desarrolladores  leen  el  código  fuente  mucho  más  a  menudo  que  lo  traduce  un  compilador.  Por  lo  tanto,  el  código  fuente  debe  ser  legible  y  
los  buenos  nombres  son  un  factor  clave  para  aumentar  su  legibilidad.  Si  está  trabajando  en  un  proyecto  con  varias  personas,  una  buena  denominación  es  
esencial  para  que  usted  y  sus  compañeros  de  equipo  puedan  entender  su  código  rápidamente.  E  incluso  si  tiene  que  editar  o  leer  un  fragmento  de  código  
escrito  por  usted  mismo  después  de  algunas  semanas  o  algunos  meses,  los  buenos  nombres  de  clases,  métodos  y  variables  lo  ayudarán  a  recordar  lo  
que  pretendía.
Entonces,  aquí  está  mi  consejo  básico:

Los  archivos  de  código  fuente,  los  espacios  de  nombres,  las  clases,  las  plantillas,  las  funciones,  los  argumentos,  las  variables  y  las  constantes  deben  

tener  nombres  significativos  y  expresivos.

Cuando  diseño  software  o  escribo  código,  paso  mucho  tiempo  pensando  en  nombres.  Creo  que  es  tiempo  bien  invertido  para  pensar  en  buenos  
nombres,  incluso  si  a  veces  no  es  fácil  y  toma  5  minutos  o  más.  Rara  vez  encuentro  el  nombre  perfecto  para  una  cosa  inmediatamente.  Por  lo  tanto,  
cambio  el  nombre  a  menudo,  lo  cual  es  fácil  con  un  buen  editor  o  un  entorno  de  desarrollo  integrado  (IDE)  con  capacidades  de  refactorización.

Si  encontrar  un  nombre  adecuado  para  una  variable,  función  o  clase  parece  ser  difícil  o  casi  imposible,  potencialmente  indica  que  algo  más  
podría  estar  mal.  Tal  vez  exista  un  problema  de  diseño  y  deba  encontrar  y  resolver  la  causa  raíz  de  su  problema  de  nombres.

Aquí  hay  algunos  consejos  para  encontrar  buenos  nombres.

Los  nombres  deben  ser  autoexplicativos  Me  he  comprometido  con  el  concepto  

de  código  autodocumentado.  El  código  autodocumentado  es  un  código  en  el  que  no  se  requieren  comentarios  para  explicar  su  propósito  (consulte  
también  la  siguiente  sección  sobre  comentarios  y  cómo  evitarlos).  Y  el  código  autodocumentado  requiere  nombres  que  se  expliquen  por  sí  mismos  para  sus  
espacios  de  nombres,  clases,  variables,  constantes  y  funciones.

Use  nombres  simples  pero  descriptivos  y  que  se  expliquen  por  sí  mismos.

Listado  4­2.  Algunos  ejemplos  de  malos  nombres

número  entero  sin  signo ;  
bandera  
booleana ;  std::vector<Cliente>  lista;
Datos  del  producto;

Las  convenciones  de  nomenclatura  de  variables  a  menudo  pueden  convertirse  en  una  guerra  religiosa,  pero  estoy  muy  seguro  de  que  existe  un  
amplio  acuerdo  en  que  num,  flag,  list  y  data  son  realmente  malos  nombres.  ¿Qué  son  los  datos?  Todo  son  datos.  Este  nombre  no  tiene  absolutamente  
ninguna  semántica.  Es  como  si  envolviera  sus  bienes  y  muebles  en  cajas  de  mudanza  y  en  lugar  de  escribir  en  ellas  lo  que  realmente  contienen,  
por  ejemplo,  "  Utensilios  de  cocina",  escribiría  la  palabra  "Cosas"  en  cada  caja.  En  la  casa  nueva  cuando  llegan  las  cajas,  esta  información  es  
completamente  inútil.

43
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Aquí  hay  un  ejemplo  de  cómo  podríamos  nombrar  mejor  las  cuatro  variables  del  ejemplo  de  código  anterior:

Listado  4­3.  Algunos  ejemplos  de  buenos  nombres

número  int  sin  firmar  de  artículos;  bool  ha  
cambiado;  
std::vector<Cliente>  clientes;
Producto  pedidoProducto;

Ahora  se  puede  argumentar  que  los  nombres  son  mejores  cuanto  más  largos  son.  Considere  el  siguiente  ejemplo:

Listado  4­4.  Un  nombre  de  variable  muy  exhaustivo.

unsigned  int  totalNumberOfCustomerEntriesWithMangledAddressInformation;

Sin  duda,  este  nombre  es  extremadamente  expresivo.  Incluso  sin  saber  de  dónde  viene  este  código,  el  lector  sabe  muy  bien  para  qué  
se  usa  esta  variable.  Sin  embargo,  hay  problemas  con  nombres  como  este.  Por  ejemplo,  no  puede  recordar  fácilmente  nombres  tan  largos.  Y  
son  difíciles  de  escribir.  Si  se  utilizan  nombres  tan  detallados  en  las  expresiones,  la  legibilidad  del  código  puede  incluso  verse  afectada:

Listado  4­5.  Un  caos  de  nombres,  causado  por  nombres  demasiado  detallados.

totalNumberOfCustomerEntriesWithMangledAddressInformation  =  
cantidadDeCustomerEntriesWithIncompleteOrMissingZipCode  +  
cantidadDeCustomerEntriesWithoutCityInformation  +  
cantidadDeCustomerEntriesWithoutStreetInformation;

Los  nombres  demasiado  largos  y  detallados  no  son  apropiados  ni  deseables  cuando  se  trata  de  limpiar  nuestro  código.  Si  el
el  contexto  es  claro  en  el  que  se  usa  una  variable,  son  posibles  nombres  más  cortos  y  menos  descriptivos.  Si  la  variable  es  un  miembro  
(atributo)  de  una  clase,  por  ejemplo,  el  nombre  de  la  clase  suele  proporcionar  suficiente  contexto  para  la  variable:

Listado  4­6.  El  nombre  de  la  clase  proporciona  suficiente  información  de  contexto  para  el  atributo.

class  CustomerRepository  { private:  
unsigned  
int  numberOfMangledEntries; // ... };

Usar  nombres  del  dominio
Es  posible  que  ya  haya  oído  hablar  del  diseño  basado  en  dominios  (DDD)  antes  de  ahora.  El  término  "Diseño  impulsado  por  el  dominio"  
fue  acuñado  por  Eric  Evans  en  su  libro  homónimo  de  2004  [Evans04].  DDD  es  un  enfoque  en  el  desarrollo  de  software  complejo  orientado  a  
objetos  que  se  centra  principalmente  en  el  dominio  central  y  la  lógica  del  dominio.
En  otras  palabras,  DDD  trata  de  hacer  que  su  software  sea  un  modelo  de  un  sistema  de  la  vida  real  mediante  la  asignación  de  cosas  y  conceptos  
del  dominio  comercial  en  el  código.  Por  ejemplo,  si  el  software  que  se  desarrollará  respaldará  los  procesos  comerciales  en  un  alquiler  de  
automóviles,  entonces  las  cosas  y  los  conceptos  de  alquiler  de  automóviles  (por  ejemplo,  automóvil  de  alquiler,  grupo  de  automóviles,  
arrendatario,  período  de  alquiler,  confirmación  de  alquiler,  contabilidad,  etc.)  deben  ser  detectable  en  el  diseño  de  este  software.
Si,  por  el  contrario,  el  software  se  desarrolla  en  la  industria  aeroespacial,  el  dominio  aeroespacial  debería  reflejarse  en  él.

44
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Las  ventajas  de  tal  enfoque  son  obvias:  el  uso  de  términos  del  dominio  facilita,  sobre  todo,
la  comunicación  entre  los  desarrolladores  y  otras  partes  interesadas.  DDD  ayuda  al  equipo  de  desarrollo  de  software  a  crear  un  
modelo  común  entre  el  negocio  y  las  partes  interesadas  de  TI  en  la  empresa  que  el  equipo  puede  usar  para  comunicarse  sobre  los  
requisitos  comerciales,  las  entidades  de  datos  y  los  modelos  de  proceso.
Una  introducción  detallada  al  Diseño  Dirigido  por  Dominio  está  más  allá  del  alcance  de  este  libro.  Sin  embargo,  
básicamente  siempre  es  una  muy  buena  idea  nombrar  componentes,  clases  y  funciones  de  manera  que  los  elementos  y  conceptos  
del  dominio  de  la  aplicación  puedan  redescubrirse.  Esto  nos  permite  comunicar  los  diseños  de  software  con  la  mayor  naturalidad  
posible.  Hará  que  el  código  sea  más  comprensible  para  cualquier  persona  involucrada  en  la  resolución  de  un  problema,  por  
ejemplo,  un  probador  o  un  experto  en  negocios.
Tomemos,  por  ejemplo,  el  alquiler  de  coches  mencionado  anteriormente.  La  clase  responsable  del  caso  de  uso  de  la  reserva  
de  un  automóvil  para  un  determinado  cliente  podría  ser  la  siguiente:

Listado  4­7.  La  interfaz  de  una  clase  de  controlador  de  caso  de  uso  para  reservar  un  automóvil

clase  ReserveCarUseCaseController  { público:

Identificación  del  clienteCliente(const  UniqueIdentifier&  customerId);
CarList  getListOfAvailableCars(const  Station&  atStation,  const  RentalPeriod&  deseadoRentalPeriod)  const;

ConfirmationOfReservation  reserveCar(const  UniqueIdentifier&  carId,  const  RentalPeriod&  rentalPeriod)  const;

privado:
Cliente&  inquisitivoCliente; };

Ahora  eche  un  vistazo  a  todos  esos  nombres  usados  para  la  clase,  los  métodos  y  los  argumentos  y  tipos  de  retorno.
Representan  cosas  que  son  típicas  del  dominio  de  alquiler  de  automóviles.  Si  lee  los  métodos  de  arriba  a  abajo,  estos  son  los  pasos  
individuales  que  se  requieren  para  alquilar  un  automóvil.  Este  es  código  C++,  pero  existe  una  gran  posibilidad  de  que  también  las  
partes  interesadas  no  técnicas  con  conocimiento  del  dominio  puedan  entenderlo.

Elija  nombres  en  un  nivel  apropiado  de  abstracción
Para  mantener  bajo  control  la  complejidad  de  los  sistemas  de  software  actuales,  estos  sistemas  suelen  descomponerse  
jerárquicamente.  La  descomposición  jerárquica  de  un  sistema  de  software  significa  que  todo  el  problema  se  divide  en  partes  más  
pequeñas,  respectivamente,  como  subtareas  hasta  que  los  desarrolladores  tengan  la  confianza  de  que  pueden  administrar  estas  
partes  más  pequeñas.  Existen  diferentes  métodos  y  criterios  para  realizar  este  tipo  de  descomposición.  El  Diseño  Dirigido  por  
Dominio  que  se  mencionó  en  la  sección  anterior,  y  también  el  Análisis  y  Diseño  Orientado  a  Objetos  (OOAD)  son  dos  métodos  para  
dicha  descomposición,  donde  el  criterio  básico  para  la  creación  de  componentes  y  clases  en  ambos  métodos  es  el  dominio  comercial. .

Con  tal  descomposición,  los  módulos  de  software  se  crean  en  diferentes  niveles  de  abstracción:  desde  grandes  componentes  o  
subsistemas  hasta  bloques  de  construcción  muy  pequeños  como  clases.  La  tarea,  que  cumple  un  bloque  de  construcción  en  un  nivel  
de  abstracción  más  alto,  debe  cumplirse  mediante  la  interacción  de  los  bloques  de  construcción  en  el  siguiente  nivel  de  abstracción  
más  bajo.
Los  niveles  de  abstracción  introducidos  por  este  enfoque  también  tienen  un  impacto  en  la  denominación.  cada  vez  que  vamos
un  paso  más  abajo  en  la  jerarquía,  los  nombres  de  los  elementos  se  vuelven  más  concretos.
Imagina  una  tienda  online.  En  el  nivel  superior  puede  existir  un  gran  componente  cuya  única  responsabilidad  sea  crear  
facturas.  Este  componente  podría  tener  un  nombre  corto  y  descriptivo  como  Facturación.  Por  lo  general,  este  componente  consta  
de  otros  componentes  o  clases  más  pequeños.  Por  ejemplo,  uno  de  estos  módulos  más  pequeños  podría  ser  responsable  del  
cálculo  de  un  descuento.  Otro  módulo  podría  ser  responsable  de  la  creación  de  partidas  de  factura.  Por  lo  tanto,  buenos  
nombres  para  estos  módulos  podrían  ser  DiscountCalculator  y  LineItemFactory.  Si  ahora  profundizamos  en  la  jerarquía  de  
descomposición,  los  identificadores  de  los  componentes,

45
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

clases,  y  también  funciones  o  métodos  se  vuelven  cada  vez  más  concretos,  detallados  y,  por  lo  tanto,  también  más  largos.  Por  
ejemplo,  un  método  pequeño  en  una  clase  en  el  nivel  más  profundo  podría  tener  un  nombre  muy  detallado  y  alargado,  como  
calcularReducidoValorImpuestoAñadido().

Evite  la  redundancia  al  elegir  un  nombre
Es  redundante  elegir  un  nombre  de  clase  u  otros  nombres  que  proporcionen  un  contexto  claro  y  usarlos  como  parte  para  construir  
el  nombre  de  una  variable  miembro,  por  ejemplo,  así:

Listado  4­8.  No  repita  el  nombre  de  la  clase  en  sus  atributos

#incluir  <cadena>

class  Movie  
{ private:  
std::string  movieTitle; // ...

¡No  hagas  eso!  Es  una  violación,  aunque  muy  pequeña,  del  principio  DRY.  En  su  lugar,  asígnele  el  nombre  Título.  La  variable  
miembro  está  en  el  espacio  de  nombres  de  la  clase  Película,  por  lo  que  queda  claro  sin  ambigüedad  a  quién  se  refiere  el  título:  ¡el  
título  de  la  película!
Aquí  hay  otro  ejemplo  de  redundancia:

Listado  4­9.  No  incluya  el  tipo  de  atributo  en  su  nombre

#incluir  <cadena>

class  Película  
{ // ...  
privado:  
std::string  stringTitle; };

Es  el  título  de  una  película,  ¡así  que  obviamente  es  una  cadena  y  no  un  número  entero!  No  incluya  el  tipo  de
variable  o  constante  en  su  nombre.

Evite  las  abreviaturas  crípticas  Al  elegir  un  nombre  

para  sus  variables  o  constantes,  utilice  palabras  completas  en  lugar  de  abreviaturas  crípticas.  La  razón  es  obvia:  las  abreviaturas  
crípticas  reducen  significativamente  la  legibilidad  de  su  código.  Además,  cuando  los  desarrolladores  hablan  sobre  su  código,  los  
nombres  de  las  variables  deben  ser  fáciles  de  pronunciar.
Recuerde  la  variable  denominada  nPar  en  la  línea  8  de  nuestro  fragmento  de  código  de  Open  Office.  Tampoco  su  significado
claro,  ni  se  puede  pronunciar  de  buena  manera.
Aquí  hay  algunos  ejemplos  más  de  lo  que  se  debe  y  no  se  debe  hacer:

Listado  4­10.  Algunos  ejemplos  de  buenos  y  malos  nombres

estándar::tamaño_t  idx; //  ¡Malo!  índice  
std::size_t; //  Bien;  podría  ser  suficiente  en  algunos  casos  std::size_t  customerIndex; //  Se  prefiere,  
especialmente  en  situaciones  donde //  se  indexan  varios  objetos

46
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Ctw  del   //  ¡Malo!
coche;  coche  coche  para  lavar; //  Bien

capa  de  polígono1; //  ¡Malo!
Polígono  primerPolígono; //  Bien

sin  firmar  int  nBottles;  cantidad   //  ¡Malo!
de  botella  int  sin  firmar ; //  Mejor  int  sin  firmar  botellas  
por  hora; //  Ah,  la  variable  tiene  un  valor  de  trabajo,
//  y  no  un  número  absoluto.  ¡Excelente!

const  doble  GOE  =  9.80665; //  ¡Malo!  const  
doble  gravedad  de  la  Tierra  =  9.80665; //  Más  expresivo,  pero  engañoso.  la  constante  es
//  no  una  gravitación,  que  sería  una  fuerza  en  física.  const  double  
gravitationalAccelerationOnEarth  =  9.80665; //  Bien.  constexpr  Aceleración  
gravitacionalAccelerationOnEarth  =  9.80665_ms2; //  ¡Guau!

Fíjate  en  la  última  línea,  que  he  comentado  con  “¡Guau!”  Eso  parece  bastante  conveniente,  porque  es  una  notación  
familiar  para  los  científicos.  Parece  casi  como  enseñar  física  en  la  escuela.  Y  sí,  eso  es  realmente  posible  en  C++,  como  
aprenderá  en  una  de  las  siguientes  secciones  sobre  programación  rica  en  tipos  en  el  Capítulo  5.

Evite  la  notación  y  los  prefijos  húngaros  ¿Conoce  a  Charles  Simonyi?  

Charles  Simonyi  es  un  experto  en  software  informático  húngaro­estadounidense  que  trabajó  como  arquitecto  jefe  en  
Microsoft  en  la  década  de  1980.  Tal  vez  recuerdes  su  nombre  en  un  contexto  diferente.
Charles  Simonyi  es  un  turista  espacial  y  ha  realizado  dos  viajes  al  espacio,  uno  de  ellos  a  la  Estación  Espacial  Internacional  
(ISS).
Pero  también  desarrolló  una  convención  de  notación  para  nombrar  variables  en  software  de  computadora,  
denominada  notación  húngara,  que  ha  sido  ampliamente  utilizada  dentro  de  Microsoft  y  más  tarde,  también,  por  otros  
fabricantes  de  software.
Cuando  se  usa  la  notación  húngara,  el  tipo  y,  a  veces,  también  el  alcance  de  una  variable  se  usan  como  prefijo  de  
nombre  para  esa  variable.  Aquí  están  algunos  ejemplos:

Listado  4­11.  Algunos  ejemplos  de  notación  húngara  con  explicaciones

bool  fEnabled;  int   //  f  =  una  bandera  booleana //  
nContador;  char*   n  =  tipo  de  número  (int,  corto,  sin  signo, ...) //  psz  =  un  puntero  
pszNombre;   a  una  cadena  terminada  en  cero
std::string  strNombre; //  str  =  una  cadena  stdlib  de  C++  int  
m_nCounter; //  El  prefijo  'm_'  indica  que  es  una  variable  miembro, //  es  decir,  tiene  ámbito  de  clase.

char*  g_pszAviso; //  Esa  es  una  variable  global  (!).  Créeme,  he  visto //  tal  cosa. //  d  =  coma  flotante  
de  precisión  doble.  
rango  int ; ¡En  este  caso  es //  una  mentira  fría  como  una  piedra!

Mi  consejo  en  el  siglo  XXI  es  este:

¡No  utilice  la  notación  húngara,  ni  ninguna  otra  notación  basada  en  prefijos,  codificando  el  tipo  de  una  variable  en  su  nombre!

47
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

La  notación  húngara  fue  potencialmente  útil  en  un  lenguaje  débilmente  tipificado  como  C.  Puede  haber  sido  útil  en  un  momento  
en  que  los  desarrolladores  usaban  editores  simples  para  programar,  y  no  IDE  que  tienen  una  función  como  "  IntelliSense".

Hoy  en  día,  las  herramientas  de  desarrollo  modernas  y  sofisticadas  brindan  un  excelente  soporte  al  desarrollador  y  muestran  el  tipo
y  alcance  de  una  variable.  No  hay  más  buenas  razones  para  codificar  el  tipo  de  una  variable  en  su  nombre.  Lejos  de  ello,  tales  
prefijos  pueden  dificultar  el  tren  de  legibilidad  del  código.
En  el  peor  de  los  casos,  incluso  puede  suceder  que  durante  el  desarrollo  se  cambie  el  tipo  de  una  variable  sin  adaptar  el  
prefijo  de  su  nombre.  En  otras  palabras:  los  prefijos  tienden  a  convertirse  en  mentiras,  como  puede  ver  en  la  última  variable  del  ejemplo  
anterior.  ¡Es  realmente  malo!
Y  otro  problema  es  que  en  lenguajes  orientados  a  objetos  que  soportan  polimorfismo,  el  prefijo  no  puede
especificarse  fácilmente,  o  un  prefijo  puede  incluso  resultar  desconcertante.  ¿Qué  prefijo  húngaro  es  adecuado  para  una  variable  
polimórfica  que  puede  ser  un  número  entero  o  un  doble?  idX?  diX?  ¿Cómo  determinar  un  prefijo  adecuado  e  inconfundible  para  una  
plantilla  de  C++  instanciada?
Por  cierto,  incluso  las  llamadas  Convenciones  generales  de  nomenclatura  de  Microsoft  enfatizan  que  no  debe  usar  la  notación  
húngara.

Evite  usar  el  mismo  nombre  para  diferentes  propósitos  Una  vez  que  haya  introducido  un  nombre  

significativo  y  expresivo  para  cualquier  tipo  de  entidad  de  software  (por  ejemplo,  una  clase  o  componente),  una  función  o  una  variable,  
debe  tener  cuidado  de  que  su  nombre  nunca  sea  utilizado  para  cualquier  otro  propósito.

Creo  que  es  bastante  obvio  que  usar  el  mismo  nombre  para  diferentes  propósitos  puede  ser  desconcertante  y  puede
engañar  a  los  lectores  del  código.  No  hagas  eso.  Eso  es  todo  lo  que  tengo  que  decir  sobre  ese  tema.

Comentarios

La  verdad  solo  se  puede  encontrar  en  un  lugar:  el  código.

—Robert  C.  Martin,  Código  limpio  [Martin09]

¿Recuerdas  tus  inicios  como  desarrollador  de  software  profesional?  ¿Todavía  recuerda  los  estándares  de  codificación  en  su  empresa  
durante  esos  días?  Tal  vez  aún  sea  joven  y  no  tenga  mucho  tiempo  en  el  negocio,  pero  los  mayores  confirmarán  que  la  mayoría  de  
esos  estándares  contenían  una  regla  de  que  el  código  profesional  adecuado  siempre  debe  comentarse  adecuadamente.  El  
razonamiento  absolutamente  comprensible  de  esta  regla  era  que  cualquier  otro  desarrollador,  o  un  nuevo  miembro  del  equipo,  podía  
comprender  fácilmente  la  intención  del  código.
A  primera  vista,  esta  regla  parece  una  buena  idea.  En  muchas  empresas,  por  lo  tanto,  el  código  fue  comentado  extensamente.  En  
algunos  proyectos,  la  relación  entre  las  líneas  de  código  productivas  y  los  comentarios  era  de  casi  50:50.
Desafortunadamente,  no  fue  una  buena  idea.  Al  contrario:  ¡ Esta  regla  fue  una  idea  absolutamente  mala!  Fue,  y  es  completamente  
erróneo  en  varios  aspectos,  porque  en  la  mayoría  de  los  casos,  los  comentarios  son  un  olor  a  código.  Los  comentarios  son  necesarios  
cuando  hay  necesidad  de  explicación  y  aclaración.  Y  eso  a  menudo  significa  que  el  desarrollador  no  pudo  escribir  un  código  simple  y  
que  se  explicara  por  sí  mismo.
Por  favor,  no  lo  malinterprete:  hay  algunos  casos  de  uso  razonables  para  los  comentarios.  En  algunas  situaciones  un
comentario  podría  ser  realmente  útil.  Presentaré  algunos  de  estos  casos  bastante  raros  al  final  de  esta  sección.
Pero  para  cualquier  otro  caso,  esta  regla  debe  aplicarse,  y  ese  es  también  el  título  de  la  siguiente  sección:  "¡Deje  que  el  código  cuente  
una  historia!"

48
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Deja  que  el  código  cuente  una  historia
Imagínese  una  película  en  el  cine,  que  solo  sería  comprensible  si  las  escenas  individuales  se  explican  mediante  una  descripción  
textual  debajo  de  la  imagen.  Esta  película  ciertamente  no  sería  un  éxito.  Al  contrario,  sería  despedazado  por  la  crítica.  Nadie  
vería  una  película  tan  mala.  Las  buenas  películas  son,  por  lo  tanto,  muy  exitosas,  porque  pueden  contar  principalmente  una  
historia  apasionante,  solo  a  través  de  las  imágenes  y  los  diálogos  de  los  actores.

La  narración  es  básicamente  un  concepto  exitoso  en  muchos  dominios,  no  solo  en  la  producción  cinematográfica.  
Cuando  piense  en  crear  un  gran  producto  de  software,  debe  pensar  en  ello  de  la  misma  manera  que  le  contaría  al  mundo  una  
gran  y  apasionante  historia.  No  sorprende  que  los  marcos  de  gestión  de  proyectos  ágiles  como  Scrum  utilicen  cosas  llamadas  
"historias  de  usuario"  como  una  forma  de  capturar  los  requisitos  desde  la  perspectiva  del  usuario.  Como  ya  expliqué  en  
una  sección  sobre  la  preferencia  de  nombres  de  dominios  específicos,  debe  hablar  con  las  partes  interesadas  en  su  propio  
idioma.
Entonces,  aquí  está  mi  consejo:

El  código  debe  contar  una  historia  y  explicarse  por  sí  mismo.  Los  comentarios  deben  evitarse  siempre  que  sea  posible.

Los  comentarios  no  son  subtítulos.  Siempre  que  sienta  el  deseo  de  escribir  un  comentario  en  su  código  porque  
quiere  explicar  algo,  debe  pensar  en  cómo  puede  escribir  mejor  el  código  para  que  se  explique  por  sí  mismo  y  el  comentario  
se  vuelva  superfluo.  Los  lenguajes  de  programación  modernos  como  C++  tienen  todo  lo  necesario  para  escribir  código  
claro  y  expresivo.  Los  buenos  programadores  aprovechan  esa  expresividad  para  contar  historias.

Cualquier  tonto  puede  escribir  un  código  que  una  computadora  pueda  entender.  Los  buenos  programadores  escriben  
código  que  los  humanos  pueden  entender.

—Martin  Fowler,  1999

No  Comentar  Cosas  Obvias
Una  vez  más,  echamos  un  vistazo  a  una  pequeña  y  típica  pieza  de  código  fuente  que  se  comentó  extensamente.

Listado  4­12.  ¿Son  útiles  estos  comentarios?

índicecliente++; //  Incrementar  índice
Cliente*  cliente  =  getCustomerByIndex(customerIndex); //  Recuperar  el  cliente  en  el //  índice  dado

CuentaCliente*  cuenta  =  cliente­>obtenerCuenta();  cuenta­ //  Recuperar  la  cuenta  del  cliente
>establecerDescuentoDeFidelidadEnPorcentaje(descuento); //  Otorgar  un  10%  de  descuento

¡Por  favor,  no  insultes  la  inteligencia  del  lector!  Es  obvio  que  estos  comentarios  son  totalmente  inútiles.
El  código  en  sí  es  en  gran  parte  autoexplicativo.  Y  no  solo  no  agregan  absolutamente  ninguna  información  nueva  
o  relevante.  Mucho  peor  es  que  estos  comentarios  inútiles  son  una  especie  de  duplicación  del  código.  Violan  el  principio  DRY  
que  hemos  discutido  en  el  Capítulo  3.
Quizás  hayas  notado  otro  detalle.  Echa  un  vistazo  a  la  última  línea.  El  comentario  habla  literalmente  de  un  10  %  de  
descuento,  pero  en  el  código  hay  una  variable  o  constante  con  nombre  de  descuento  que  se  pasa  a  la  función  o  método  
setLoyaltyDiscountInPercent().  ¿Qué  ha  pasado  aquí?  Una  sospecha  razonable  es  que  este  comentario  se  ha  convertido  
en  una  mentira  porque  se  cambió  el  código,  pero  el  comentario  no  se  adaptó.  Eso  es  realmente  malo  y  engañoso.

49
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

No  deshabilite  el  código  con  comentarios
A  veces,  los  comentarios  se  utilizan  para  deshabilitar  un  montón  de  código  que  el  compilador  no  traducirá.  Un  razonamiento  
a  menudo  entregado  por  algunos  desarrolladores  para  esta  práctica  es  que  uno  podría  usar  este  fragmento  de  código  
nuevamente  más  tarde.  Piensan:  "Tal  vez  algún  día...  lo  necesitaremos  de  nuevo".

Listado  4­13.  Un  ejemplo  de  código  comentado

//  Esta  función  ya  no  se  usa  (John  Doe,  2013­10­25): /*  double  

calcDisplacement(double  t)  { const  double  goe  
=  9.81; //  gravedad  de  la  tierra  double  d  =  0.5  *  pow(t,  2); //  
*
cálculo  de  la  distancia  
dvamos
e  retorno  d;

}  */

Un  problema  importante  con  el  código  comentado  es  que  agrega  confusión  sin  ningún  beneficio  real.  Solo  imagine  que  
la  función  deshabilitada  en  el  ejemplo  anterior  no  es  la  única,  sino  uno  de  muchos  lugares  donde  el  código  ha  sido  comentado.  
El  código  pronto  se  convertirá  en  un  gran  lío  y  los  fragmentos  de  código  comentados  agregarán  mucho  ruido  que  dificultará  
la  legibilidad.  Además,  los  fragmentos  de  código  comentados  no  tienen  garantía  de  calidad,  es  decir,  el  compilador  no  los  
traduce,  no  los  prueba  ni  los  mantiene.  Mi  consejo  es  este:

Excepto  con  el  propósito  de  probar  algo  rápidamente,  no  use  comentarios  para  deshabilitar  el  código.  ¡Hay  un  sistema  de  
control  de  versiones!

Si  el  código  ya  no  se  usa,  simplemente  elimínelo.  Déjalo  ir.  Tienes  una  “máquina  del  tiempo”  para  recuperarlo,  
si  es  necesario:  tu  sistema  de  control  de  versiones.  Sin  embargo,  a  menudo  resulta  que  este  caso  es  muy  raro.  Solo  eche  un  
vistazo  a  la  marca  de  tiempo  que  el  desarrollador  agregó  en  el  ejemplo  anterior.  Este  fragmento  de  código  es  antiguo.  
¿Cuál  es  la  probabilidad  de  que  se  vuelva  a  necesitar?
Para  probar  algo  rápidamente  durante  el  desarrollo,  por  ejemplo,  mientras  busca  la  causa  de  un  error,  es  útil  comentar  
una  sección  de  código  temporalmente.  Pero  debe  asegurarse  de  que  dicho  código  modificado  no  se  registre  en  el  sistema  
de  control  de  versiones.

No  escribir  comentarios  de  bloque
Comentarios  como  los  siguientes  se  pueden  encontrar  en  muchos  proyectos.

Listado  4­14.  Un  ejemplo  de  bloque  de  comentarios

#ifndef  _STUFF_H_  
#define  _STUFF_H_

//  ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
//  stuff.h:  la  interfaz  de  la  clase  Stuff
//  John  Doe,  creado:  2007­09­21 //  
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­  ­

50
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

cosas  de  clase  

{ público: //  ­­­­­­­­­­­­­­­­
//  Interfaz  pública //  
­­­­­­­­­­­­­­­­

// ...

protegido: //  
­­­­­­­­­­­­­
//  Anulables //  
­­­­­­­­­­­­­

// ...

privado: //  
­­­­­­­­­­­­­­­­­­­­­­­­
//  Funciones  miembro  privadas //  
­­­­­­­­­­­­­­­­­­­­­­­­

// ...

//  ­­­­­­­­­­­­­­­­­­
//  Atributos  privados //  
­­­­­­­­­­­­­­­­­­

// ...

#terminara  si

Este  tipo  de  comentarios  (y  no  me  refiero  a  los  que  usé  para  ocultar  partes  irrelevantes)  se  denominan  "Bloquear  
comentarios"  o  "Banners".  A  menudo  se  usan  para  poner  un  resumen  sobre  el  contenido  en  la  parte  superior  de  un  archivo  
de  código  fuente.  O  se  utilizan  para  marcar  una  posición  especial  en  el  código.  Por  ejemplo,  están  introduciendo  una  sección  
de  código  donde  se  pueden  encontrar  todas  las  funciones  de  miembros  privados  de  una  clase.
Este  tipo  de  comentarios  son  en  su  mayoría  puro  desorden  y  deben  eliminarse  de  inmediato.
Hay  muy  pocas  excepciones  en  las  que  tales  comentarios  podrían  tener  un  beneficio.  En  algunos  casos  raros,  un  grupo
de  funciones  de  una  categoría  especial  se  pueden  agrupar  debajo  de  dicho  comentario.  Pero  entonces  no  debe  
usar  trenes  de  caracteres  ruidosos  que  consisten  en  guiones  (­),  barras  inclinadas  (/),  signos  de  número  (#)  o  asteriscos  
(*)  para  envolverlo.  Un  comentario  como  el  siguiente  es  absolutamente  suficiente  para  presentar  tal  región:

Listado  4­15.  A  veces  útil:  un  comentario  para  introducir  una  categoría  de  funciones

privado:
//  Controladores  de  eventos:

void  onUndoButtonClick();  void  
onRedoButtonClick();  vacío  
onCopyButtonClick(); // ...

51
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

En  algunos  proyectos,  los  estándares  de  codificación  dicen  que  son  obligatorios  los  encabezados  grandes  con  texto  de  copyright  y  
licencia  en  la  parte  superior  de  cualquier  archivo  de  código  fuente.  Pueden  verse  así:

Listado  4­16.  El  encabezado  de  la  licencia  en  cualquier  archivo  de  código  fuente  de  Apache  OpenOffice  3.4.1

/****************************************************  ************
*

*  Con  licencia  de  Apache  Software  Foundation  (ASF)  bajo  una
*
o  más  acuerdos  de  licencia  de  colaborador.  Ver  el  archivo  AVISO
*  distribuido  con  este  trabajo  para  obtener  información  adicional  *  sobre  la  
propiedad  de  los  derechos  de  autor.  La  ASF  le  otorga  la  licencia  de  este  archivo  *  
bajo  la  Licencia  Apache,  Versión  2.0  (la  *  "Licencia");  no  puede  usar  
este  archivo  excepto  de  conformidad  *  con  la  Licencia.  Puede  obtener  una  copia  
de  la  Licencia  en
*

*  http://www.apache.org/licenses/LICENSE­2.0
*

*  A  menos  que  lo  exija  la  ley  aplicable  o  se  acuerde  por  escrito,  *  el  software  
distribuido  bajo  la  Licencia  se  distribuye  en  un
*  BASE  "TAL  CUAL",  SIN  GARANTÍAS  O  CONDICIONES  DE  CUALQUIER
*
AMABLE,  ya  sea  expresa  o  implícita.  Vea  la  Licencia  para  el
*  lenguaje  específico  que  rige  los  permisos  y  limitaciones  *  bajo  la  Licencia.

*
****************************************************  ***********/

Primero  quiero  decir  algo  fundamental  sobre  los  derechos  de  autor.  No  necesita  agregar  comentarios  sobre  los  derechos  
de  autor,  ni  hacer  nada  más,  para  tener  derechos  de  autor  sobre  sus  obras.  De  acuerdo  con  el  Convenio  de  Berna  para  la  
Protección  de  las  Obras  Literarias  y  Artísticas  [Wipo1886]  (o  el  Convenio  de  Berna  para  abreviar),  tales  comentarios  no  tienen  
significado  legal.
Hubo  momentos  en  que  tales  comentarios  fueron  necesarios.  Antes  de  que  Estados  Unidos  firmara  la  Convención  de  
Berna  en  1989,  dichos  avisos  de  derechos  de  autor  eran  obligatorios  si  deseaba  hacer  cumplir  sus  derechos  de  autor  en  
Estados  Unidos.  Pero  eso  es  cosa  del  pasado.  Hoy  en  día  estos  comentarios  ya  no  son  necesarios.
Mi  consejo  es  simplemente  omitirlos.  Son  solo  equipaje  engorroso  e  inútil.  Sin  embargo,  si  quieres
o  incluso  necesita  ofrecer  información  de  derechos  de  autor  y  licencia  en  su  proyecto,  entonces  es  mejor  que  los  escriba  
en  archivos  separados,  como  licencia.txt  y  derechos  de  autor.txt.  Si  una  licencia  de  software  requiere  en  todas  las  
circunstancias  que  la  información  de  la  licencia  se  incluya  en  el  área  principal  de  cada  archivo  de  código  fuente,  entonces  
puede  ocultar  estos  comentarios  si  su  IDE  tiene  el  llamado  editor  plegable.

No  use  comentarios  para  sustituir  el  control  de  versiones

A  veces,  y  esto  es  extremadamente  malo,  los  comentarios  de  banner  se  usan  para  un  registro  de  cambios,  como  en  el  
siguiente  ejemplo.

Listado  4­17.  Administrar  el  historial  de  cambios  en  el  archivo  de  código  fuente

//  #############################################  ###########################
//  Registro  de  cambios:
//  2016­06­14  (John  Smith)  Método  de  cambio  rebuildProductList  para  corregir  el  error  #275
//  2015­11­07  (Bob  Jones)  Extrajo  cuatro  métodos  a  la  nueva  clase  ProductListSorter
//  2015­09­23  (Ninja  Dev)  Arregló  el  error  más  estúpido  de  una  manera  muy  inteligente //  
#########################  ##############################################

52
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

¡No  hagas  esto!  Rastrear  el  historial  de  cambios  de  cada  archivo  en  su  proyecto  es  una  de  las  principales  tareas  de  su
sistema  de  control  de  versiones.  Si  está  usando  Git,  por  ejemplo,  puede  usar  git  log  ­­  [nombre  de  archivo]  para  obtener  el  
historial  de  cambios  de  un  archivo.  Los  programadores  que  han  escrito  los  comentarios  anteriores  son  más  que  probables  aquellos  
que  siempre  dejan  el  cuadro  Comentarios  de  registro  vacío  en  sus  confirmaciones.

Los  raros  casos  en  los  que  los  comentarios  son  útiles
Por  supuesto,  no  todos  los  comentarios  del  código  fuente  son  básicamente  inútiles,  falsos  o  malos.  Hay  algunos  casos  en  los  
que  los  comentarios  son  importantes  o  incluso  indispensables.
En  algunos  casos  muy  concretos  puede  ocurrir  que,  aunque  hayas  utilizado  nombres  perfectos  para  todas  las  variables
y  funciones,  algunas  secciones  de  su  código  necesitan  más  explicaciones  para  ayudar  al  lector.  Por  ejemplo,  un  comentario  
está  justificado  si  una  sección  de  código  tiene  un  alto  grado  de  complejidad  inherente,  de  modo  que  no  puede  ser  entendida  
fácilmente  por  todos  los  que  no  tienen  un  conocimiento  experto  profundo.  Este  puede  ser  el  caso,  por  ejemplo,  con  un  sofisticado  
algoritmo  o  fórmula  matemática.  O  el  sistema  de  software  se  ocupa  de  un  dominio  (comercial)  no  cotidiano,  es  decir,  un  área  o  
campo  de  aplicación  que  no  es  fácilmente  comprensible  para  todos,  por  ejemplo,  física  experimental,  simulaciones  complejas  de  
fenómenos  naturales  o  métodos  de  cifrado  ambiciosos.  En  tales  casos,  algunos  comentarios  bien  escritos  que  expliquen  las  cosas  
pueden  ser  muy  valiosos.
Otra  buena  razón  para  escribir  un  comentario  por  una  vez  es  una  situación  en  la  que  debe  desviarse  deliberadamente  de  un  
buen  principio  de  diseño.  Por  ejemplo,  el  principio  DRY  (consulte  el  Capítulo  3)  es,  por  supuesto,  válido  en  la  mayoría  de  las  
circunstancias,  pero  puede  haber  algunos  casos  muy  raros  en  los  que  deba  duplicar  intencionalmente  una  pieza  de  código,  por  
ejemplo,  para  cumplir  requisitos  de  calidad  ambiciosos  con  respecto  al  rendimiento.  Esto  justifica  un  comentario  explicando  por  
qué  ha  violado  el  principio;  de  lo  contrario,  es  posible  que  sus  compañeros  de  equipo  no  puedan  comprender  su  decisión.

El  desafío  es  este:  los  comentarios  buenos  y  significativos  son  difíciles  de  escribir.  Puede  ser  más  difícil  que  escribir  el  
código.  Así  como  no  todos  los  miembros  de  un  equipo  de  desarrollo  son  buenos  para  diseñar  una  interfaz  de  usuario,  tampoco  
todos  son  buenos  para  escribir.  La  redacción  técnica  es  una  habilidad  para  la  que  suele  haber  especialistas.
Entonces,  aquí  hay  algunos  consejos  para  escribir  comentarios  que  son  inevitables  por  las  razones  
mencionadas  anteriormente:

•  Asegúrese  de  que  sus  comentarios  agreguen  valor  al  código.  El  valor  en  este  contexto  significa  que  los  
comentarios  agregan  información  importante  para  otros  seres  humanos  (generalmente  otros  
desarrolladores)  que  no  son  evidentes  en  el  código  en  sí.

•  Explique  siempre  el  Por  qué,  no  el  Cómo.  El  funcionamiento  de  una  parte  del  código  debe  quedar  bastante  
claro  a  partir  del  propio  código,  y  la  denominación  significativa  de  variables  y  funciones  es  la  clave  para  
lograr  este  objetivo.  Use  comentarios  únicamente  para  explicar  por  qué  existe  una  determinada  pieza  
de  código.  Por  ejemplo,  puede  proporcionar  una  justificación  de  por  qué  eligió  un  algoritmo  o  método  
en  particular.

•  Trate  de  ser  lo  más  breve  y  expresivo  posible.  Prefiere  comentarios  breves  y  concisos,  idealmente  de  una  
sola  línea,  y  evita  textos  largos  y  parlanchines.  Siempre  tenga  en  cuenta  que  los  comentarios  
también  deben  mantenerse.  En  realidad,  es  mucho  más  fácil  mantener  comentarios  breves  que  
explicaciones  extensas  y  prolijas.

■  Sugerencia  En  entornos  de  desarrollo  integrados  (IDE)  con  colores  de  sintaxis,  el  color  de  los  comentarios  suele  
estar  preconfigurado  en  verde  o  verde  azulado.  ¡Deberías  cambiar  este  color  a  rojo!  Un  comentario  en  el  código  
fuente  debe  ser  algo  especial  que  atraiga  la  atención  del  desarrollador.

53
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Generación  de  Documentación  a  partir  de  Código  Fuente

Una  forma  especial  de  comentarios  son  las  anotaciones  que  puede  extraer  un  generador  de  documentación.  Un  ejemplo  de  tal  herramienta  
es  Doxygen  (http://doxygen.org)  que  está  muy  extendido  en  el  mundo  de  C++  y  se  publica  bajo  una  Licencia  Pública  General  GNU  
(GPLv2).  Dicha  herramienta  analiza  el  código  fuente  C++  anotado  y  puede  crear  una  documentación  en  forma  de  un  documento  
legible  e  imprimible  (por  ejemplo,  PDF)  o  un  conjunto  de  documentos  web  interrelacionados  (HTML)  que  se  pueden  ver  con  un  
navegador.  Y  en  combinación  con  una  herramienta  de  visualización,  Doxygen  puede  incluso  generar  diagramas  de  clases,  incluir  
gráficos  de  dependencia  y  gráficos  de  llamadas.  Por  lo  tanto,  Doxygen  también  se  puede  utilizar  para  el  análisis  de  código.

Para  que  una  documentación  significativa  salga  de  dicha  herramienta,  el  código  fuente  debe  estar  anotado
intensamente  con  comentarios  específicos.  Aquí  hay  un  ejemplo  no  tan  bueno  con  anotaciones  en  estilo  Doxygen:

Listado  4­18.  Una  clase  anotada  con  comentarios  de  documentación  para  Doxygen

//!  Los  objetos  de  esta  clase  representan  una  cuenta  de  cliente  en  nuestro  sistema.  clase  CuentaCliente  
{ // ...

//!  Concede  un  descuento  de  fidelidad. //!  
@param  discount  es  el  valor  del  descuento  en  porcentaje.  void  
grantLoyaltyDiscount  ( descuento  corto  sin  firmar);

// ... };

¿Qué?  ¿Los  objetos  de  clase  CustomerAccount  representan  cuentas  de  clientes?  ¡¿Ah,  de  verdad?!  ¿Y  
grantLoyaltyDiscount  otorga  un  descuento  por  fidelidad?  ¡Eh!
Pero  en  serio  amigos!  Para  mí,  esta  forma  de  documentación  funciona  en  ambos  sentidos.
Por  un  lado,  puede  ser  muy  útil  para  anotar,  especialmente  la  interfaz  pública  (API)  de  una  biblioteca  o  un  marco  con  este  tipo  
de  comentarios,  y  generar  documentación  a  partir  de  ella.  En  particular,  si  los  clientes  del  software  son  desconocidos  (el  caso  
típico  con  bibliotecas  y  marcos  disponibles  públicamente),  dicha  documentación  puede  ser  muy  útil  si  desean  utilizar  el  software  en  sus  
proyectos.
Por  otro  lado,  tales  comentarios  agregan  una  gran  cantidad  de  ruido  a  su  código.  La  relación  entre  el  código  y  las  líneas  de  
comentarios  puede  llegar  rápidamente  a  50:50.  Y  como  se  puede  ver  en  el  ejemplo  anterior,  dichos  comentarios  también  tienden  a  explicar  
cosas  obvias  (recuerde  la  sección  de  este  capítulo,  “No  comente  cosas  obvias”).  Finalmente,  la  mejor  documentación  que  existe,  
una  "documentación  ejecutable",  es  un  conjunto  de  pruebas  unitarias  bien  diseñadas  (consulte  la  sección  sobre  pruebas  unitarias  en  
el  Capítulo  2 ).  y  Capítulo  8  sección  sobre  Desarrollo  basado  en  pruebas),  que  puede  mostrar  exactamente  cómo  se  debe  usar  la  API  de  
la  biblioteca.
De  todos  modos,  no  tengo  una  opinión  final  sobre  este  tema.  Si  quiere,  o  tiene  que,  anotar  la  API  pública  de  sus  componentes  
de  software  con  comentarios  estilo  Doxygen  a  toda  costa,  entonces,  por  el  amor  de  Dios,  hágalo.  Si  está  bien  hecho,  puede  ser  muy  
útil.  ¡Le  recomiendo  encarecidamente  que  preste  atención  exclusiva  a  los  encabezados  de  su  API  pública!  Para  todas  las  demás  partes  
de  su  software,  por  ejemplo,  módulos  de  uso  interno  o  funciones  privadas,  le  recomiendo  que  no  los  equipe  con  anotaciones  de  
Doxygen.
El  ejemplo  anterior  se  puede  mejorar  significativamente  si  se  utilizan  términos  y  explicaciones  del  dominio  de  las  aplicaciones.

54
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Listado  4­19.  Una  clase  anotada  con  comentarios  desde  una  perspectiva  comercial  para  Doxygen

//!  Cada  cliente  debe  tener  una  cuenta,  por  lo  que  se  pueden  hacer  reservas.  La  cuenta //!  también  es  
necesario  para  la  creación  de  facturas  mensuales. //!  entidades  @ingroup //!  
@ingroup  clase  de  
contabilidad  CustomerAccount  
{ // ...

//!  Los  clientes  regulares  ocasionalmente  reciben  un  descuento  regular  en  su //!  compras  void  
grantDiscount(const  
PorcentajeValor&descuento);

// ...

Tal  vez  haya  notado  que  ya  no  he  comentado  sobre  el  parámetro  del  método  con  la  etiqueta  @param  de  Dogygen.  
En  cambio,  cambié  su  tipo  de  un  corto  sin  firmar  sin  sentido  a  una  referencia  constante  de  un  tipo  personalizado  llamado  
PercentageValue.  Debido  a  esto,  el  parámetro  se  ha  vuelto  autoexplicativo.  Por  qué  este  es  un  enfoque  mucho  mejor  que  
cualquier  comentario,  puede  leerlo  en  una  sección  sobre  Programación  rica  en  tipos  en  el  Capítulo  5.

Aquí  hay  algunos  consejos  finales  para  las  anotaciones  de  estilo  Doxygen  en  el  código  fuente:

•  No  use  la  etiqueta  @file  [<name>]  de  Doxygen  para  escribir  el  nombre  del  archivo  en  alguna  parte
en  el  propio  archivo.  Por  un  lado,  esto  es  inútil,  porque  Dogygen  lee  el  nombre  del  archivo  de  
todos  modos  y  automáticamente.  Por  otro  lado,  viola  el  principio  DRY  (ver  Capítulo  3).  Es  
información  redundante,  y  si  tiene  que  cambiar  el  nombre  del  archivo,  también  debe  recordar  
cambiar  el  nombre  de  la  etiqueta  @file.

•  No  edite  las  etiquetas  @version,  @author  y  @date  manualmente,  ya  que  su  sistema  de  control  de  
versiones  puede  administrar  y  realizar  un  seguimiento  de  esta  información  mucho  mejor  que  
cualquier  desarrollador  que  deba  editarlas  manualmente.  Si  dicha  información  de  gestión  debe  
aparecer  en  el  archivo  de  código  fuente  en  todas  las  circunstancias,  el  sistema  de  control  de  
versiones  debe  completar  automáticamente  estas  etiquetas.  En  todos  los  demás  casos  
prescindiría  por  completo  de  ellos.

•No  use  las  etiquetas  @bug  o  @todo.  En  su  lugar,  debería  corregir  el  error
inmediatamente,  o  use  un  software  de  seguimiento  de  problemas  para  archivar  errores  para  una  posterior  resolución  de  
problemas,  respectivamente,  administre  los  puntos  abiertos.

• Se  recomienda  encarecidamente  proporcionar  una  página  de  inicio  descriptiva  del  proyecto  utilizando  
la  etiqueta  @mainpage  (idealmente  en  un  archivo  de  encabezado  separado  solo  para  este  propósito),  
ya  que  dicha  página  de  inicio  sirve  como  guía  de  inicio  y  ayuda  de  orientación  para  los  desarrolladores  
que  actualmente  no  están  familiarizados.  con  el  proyecto  entre  manos.

• No  usaría  la  etiqueta  @example  para  proporcionar  un  bloque  de  comentarios  que  contenga  un  
ejemplo  de  código  fuente  sobre  cómo  usar  una  API.  Como  ya  se  mencionó,  tales  comentarios  
agregan  mucho  ruido  al  código.  En  su  lugar,  ofrecería  un  conjunto  de  pruebas  unitarias  bien  
diseñadas  (consulte  el  Capítulo  2  sobre  las  pruebas  unitarias  y  el  capítulo  8  sobre  el  desarrollo  
basado  en  pruebas),  ya  que  estos  son  los  mejores  ejemplos  de  uso:  ¡ejemplos  ejecutables!  
Además,  las  pruebas  unitarias  siempre  son  correctas  y  están  actualizadas,  ya  que  deben  
ajustarse  cuando  cambia  la  API  (de  lo  contrario,  las  pruebas  fallarán).  Un  comentario  con  un  
ejemplo  de  uso,  por  otro  lado,  puede  resultar  erróneo  sin  que  nadie  lo  note.

55
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

•Una  vez  que  un  proyecto  ha  crecido  a  un  tamaño  particular,  es  recomendable  poner  en  común  ciertos
categorías  de  unidades  de  software  con  la  ayuda  del  mecanismo  de  agrupación  de  Dogygen  
(Etiquetas:  @defgroup  <nombre>,  @addtogroup  <nombre>  y  @ingroup  <nombre>).  Esto  es,  
por  ejemplo,  muy  útil  cuando  desea  expresar  el  hecho  de  que  ciertas  unidades  de  software  
pertenecen  a  un  módulo  cohesivo  en  un  nivel  superior  de  abstracción  (por  ejemplo,  un  componente  
o  subsistema).  Este  mecanismo  también  permite  agrupar  ciertas  categorías  de  clases,  por  ejemplo,  
todas  las  entidades,  todos  los  adaptadores  (consulte  Patrón  de  adaptador  en  el  Capítulo  9)  o  todas  
las  fábricas  de  objetos  (consulte  Patrón  de  fábrica  en  el  Capítulo  9).  La  clase  CustomerAccount  
del  ejemplo  de  código  anterior  está,  por  ejemplo,  en  el  grupo  de  entidades  (un  grupo  que  contiene  
todos  los  objetos  comerciales),  pero  también  forma  parte  del  componente  de  contabilidad.

Funciones
Las  funciones  (sinónimos:  métodos,  procedimientos,  servicios,  operaciones)  son  el  corazón  de  cualquier  sistema  de  software.  
Representan  la  primera  unidad  organizativa  por  encima  de  las  líneas  de  código.  Las  funciones  bien  escritas  fomentan  
considerablemente  la  legibilidad  y  la  mantenibilidad  de  un  programa.  Por  esta  razón,  deben  elaborarse  bien  y  con  cuidado.  En  
esta  sección  doy  varias  pistas  importantes  para  escribir  buenas  funciones.
Sin  embargo,  antes  de  explicar  las  cosas  que  considero  importantes  para  funciones  bien  diseñadas,  vamos  a
examine  de  nuevo  un  ejemplo  disuasorio,  tomado  de  OpenOffice  3.4.1  de  Apache.

Listado  4­20.  Otro  extracto  del  código  fuente  de  OpenOffice  3.4.1  de  Apache

1780  sal_Bool  BasicFrame::QueryFileName(String&  rName,  FileType  nFileType,  sal_Bool  bSave )  1781  { 1782  1783  1784  1785  
1786  
1787   NewFileDialog  aDlg( esto,  bSave ?  WinBits( WB_SAVEAS ) :
1788   WinBits(WB_OPEN));  
1789 aDlg.SetText( String( SttResId( bSave ?  IDS_SAVEDLG :  IDS_LOADDLG ) ) );

if  ( nFileType  &  FT_RESULT_FILE )  

{ aDlg.SetDefaultExt( String( SttResId( IDS_RESFILE ) ) );  
aDlg.AddFilter( String( SttResId( IDS_RESFILTER ) ),
1790 Cadena  (SttResId  (IDS_RESFILE))));  
1791 aDlg.AddFilter( String( SttResId( IDS_TXTFILTER ) ),  
1792 String( SttResId( IDS_TXTFILE ) ) );  
1793 aDlg.SetCurFilter( SttResId( IDS_RESFILTER ) );
1794 }
1795  
1796 si  ( nFileType  &  FT_BASIC_SOURCE )  {
1797  
1798 aDlg.SetDefaultExt( String( SttResId( IDS_NONAMEFILE ) ) );  
1799   aDlg.AddFilter( String( SttResId( IDS_BASFILTER ) ),  
1800 String( SttResId( IDS_NONAMEFILE ) ) );  
1801 aDlg.AddFilter( String( SttResId( IDS_INCFILTER ) ),  
1802 String( SttResId( IDS_INCFILE ) ) );  
1803   aDlg.SetCurFilter( SttResId( IDS_BASFILTER ) );
1804   }
1805  
1806 si  (nFileType  &  FT_BASIC_LIBRARY)  {
1807  
1808 aDlg.SetDefaultExt( String( SttResId( IDS_LIBFILE ) ) );

56
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

1809   aDlg.AddFilter( String( SttResId( IDS_LIBFILTER ) ),  
1810   String( SttResId( IDS_LIBFILE ) ) );  
1811   aDlg.SetCurFilter( SttResId( IDS_LIBFILTER ) );
1812   }
1813  
1814   Config  aConf(Config::GetConfigName( Config::GetDefDirectory(),  CUniString("testtool") ));  
1815   aConf.SetGroup( "Misc" );  ByteString  
1816   aCurrentProfile  =  
1817   aConf.ReadKey( "CurrentProfile",  "Path" );  aConf.SetGroup( aCurrentProfile );  ByteString  
1818   aFilter( aConf.ReadKey( "LastFilterName") );  if  
1819   ( aFilter.Len() )  aDlg.SetCurFilter( String( aFilter,  
1820  
1821   RTL_TEXTENCODING_UTF8 ) );  else  aDlg.SetCurFilter( String( SttResId( IDS_BASFILTER ) ) );
1822  
1823  
1824  
1825  aDlg.FilterSelect(); //  Selecciona  la  última  ruta  utilizada  1826 //  if  ( bSave )  
1827  if  ( rName.Len()  >  0 )  
1828  aDlg.SetPath( rName );  1829  1830  
1831  1832  1833 /*  1834  1835  1836  1837  1838  */  
1839  
1840   if( aDlg.Execute() )  {
1841 }
rName  =  aDlg.GetPath();  
rExtensión  =  aDlg.GetCurrentFilter();  var  i:  entero;  
for  ( i  =  0 ;  i  <  
aDlg.GetFilterCount() ;  i++ )  if  ( rExtension  ==  
aDlg.GetFilterName( i ) )  rExtension  =  aDlg.GetFilterType( i );

volver  sal_True; }  
más  devuelve  sal_False;

Pregunta:  ¿Qué  esperaba  cuando  vio  una  función  miembro  llamada  QueryFileName()  por  primera  vez?

¿Esperaría  que  se  abriera  un  cuadro  de  diálogo  de  selección  de  archivos  (recuerde  el  Principio  de  menor  
asombro  discutido  en  el  Capítulo  3)?  Probablemente  no,  pero  eso  es  exactamente  lo  que  se  hace  aquí.  Obviamente,  
se  le  pide  al  usuario  que  interactúe  con  la  aplicación,  por  lo  que  un  mejor  nombre  para  esta  función  miembro  sería  
AskUserForFilename().
Pero  eso  no  es  suficiente.  Si  observa  las  primeras  líneas  en  detalle,  verá  que  hay  un  parámetro  booleano  
bSave  que  se  usa  para  distinguir  entre  un  cuadro  de  diálogo  de  archivo  para  abrir  y  un  cuadro  de  diálogo  de  archivo  para  
guardar  archivos.  ¿Esperabas  eso?  ¿Y  cómo  coincide  el  término  Consulta...  en  el  nombre  de  la  función  con  ese  hecho?  
Por  lo  tanto,  un  mejor  nombre  para  esta  función  miembro  podría  ser  AskUserForFilenameToOpenOrSave().
Las  siguientes  líneas  tratan  del  argumento  de  la  función  nFileType.  Aparentemente,  se  distinguen  tres  tipos  de  
archivos  diferentes.  El  parámetro  nFileType  está  enmascarado  con  algo  llamado  FT_RESULT_FILE,  FT_BASIC_SOURCE  
y  FT_BASIC_LIBRARY.  Dependiendo  del  resultado  de  esta  operación  AND  bit  a  bit,  el  cuadro  de  diálogo  del  archivo  se  
configura  de  manera  diferente,  por  ejemplo,  se  establecen  filtros.  Como  ya  lo  ha  hecho  antes  el  parámetro  booleano  bSave,  
las  tres  sentencias  if  introducen  caminos  alternativos.  Eso  aumenta  lo  que  se  conoce  como  la  complejidad  ciclomática  de  la  
función.

57
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

COMPLEJIDAD  CICLOMÁTICA

La  complejidad  ciclomática  métrica  cuantitativa  del  software  fue  desarrollada  por  Thomas  J.  McCabe,  un  
matemático  estadounidense,  en  1976.

La  métrica  es  un  recuento  directo  del  número  de  rutas  linealmente  independientes  a  través  de  una  sección  de  
código  fuente,  por  ejemplo,  una  función.  Si  una  función  no  contiene  sentencias  if  o  switch,  ni  bucles  for  o  
while,  solo  hay  un  único  camino  a  través  de  la  función  y  su  complejidad  ciclomática  es  1.  Si  la  función  contiene  
una  sentencia  if  que  representa  un  único  punto  de  decisión ,  hay  dos  caminos  a  través  de  la  función  y  la  
complejidad  ciclomática  es  2.

Si  la  complejidad  ciclomática  es  alta,  la  pieza  de  código  afectada  suele  ser  más  difícil  de  entender,  probar  y  
modificar  y,  por  lo  tanto,  es  propensa  a  errores.

Los  tres  si  plantean  otra  pregunta:  ¿Es  esta  función  el  lugar  adecuado  para  realizar  este  tipo  de  configuración?
¡Definitivamente  no!  Esto  no  pertenece  aquí.
Las  siguientes  líneas  (a  partir  de  1814)  tienen  acceso  a  datos  de  configuración  adicionales.  No  se  puede  determinar  con  
exactitud,  pero  parece  que  el  último  filtro  de  archivo  utilizado  ("LastFilterName")  se  carga  desde  una  fuente  que  contiene  datos  
de  configuración,  ya  sea  un  archivo  de  configuración  o  el  registro  de  Windows.  Especialmente  confuso  es  que  el  filtro  ya  definido,  
que  se  estableció  en  los  tres  bloques  if  anteriores  (aDlg.SetCurFilter(...)),  siempre  se  sobrescribirá  en  este  lugar  (ver  líneas  
1820­1823).  Entonces,  ¿cuál  es  el  sentido  de  configurar  este  filtro  en  los  tres  bloques  if  anteriores?

Poco  antes  del  final,  entra  en  juego  el  parámetro  de  referencia  rName.  Espera...  ¿nombre  de  qué,  por  favor?
Probablemente  sea  el  nombre  del  archivo,  sí,  pero  ¿por  qué  no  se  llama  filename  para  excluir  todas  las  posibilidades  de  duda?  
¿Y  por  qué  el  nombre  del  archivo  no  es  el  valor  de  retorno  de  esta  función?  (La  razón  por  la  que  debe  evitar  los  llamados  argumentos  
de  salida  es  un  tema  que  se  analiza  más  adelante  en  este  capítulo).
Como  si  esto  no  fuera  suficientemente  malo,  la  función  también  contiene  código  comentado.
Bueno,  esta  función  consta  de  unas  50  líneas  solamente,  pero  tiene  muchos  malos  olores  a  código.  La  función  es  
demasiado  larga,  tiene  una  alta  complejidad  ciclomática,  mezcla  diferentes  preocupaciones,  tiene  muchos  argumentos  y  
contiene  código  muerto.  El  nombre  de  la  función  QueryFileName()  no  es  específico  y  puede  resultar  engañoso.  ¿Quién  es  consultado?
¿Una  base  de  datos?  AskUserForFilename()  sería  mucho  mejor,  porque  enfatiza  la  interacción  con  el  usuario.  La  mayor  
parte  del  código  es  difícil  de  leer  y  difícil  de  entender.  ¿Qué  significa  nFileType  y  FT_BASIC_  LIBRARY?

Pero  el  punto  esencial  es  que  la  tarea  a  realizar  por  esta  función  (selección  de  nombre  de  archivo)  justifica  una
propia  clase,  porque  la  clase  BasicFrame,  que  es  parte  de  la  interfaz  de  usuario  de  la  aplicación,  definitivamente  no  es  
responsable  de  tales  cosas.
Suficiente  de  eso.  Echemos  un  vistazo  a  lo  que  debe  tener  en  cuenta  un  creador  de  software  al  diseñar  buenas  funciones.

¡Una  cosa,  no  más!
Una  función  debe  tener  una  tarea  definida  muy  precisa  que  debe  estar  representada  por  su  nombre  significativo.  En  su  brillante  
libro  Clean  Code,  el  desarrollador  de  software  estadounidense  Robert  C.  Martin  lo  formula  de  la  siguiente  manera:

Las  funciones  deben  hacer  una  cosa.  Deberían  hacerlo  bien.  Deberían  hacerlo  solo.

—Robert  C.  Martin,  Código  limpio  [Martin09]

58
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Puede  preguntarse  ahora:  ¿Pero  cómo  sé  cuándo  una  función  hace  demasiadas  cosas?  Aquí  hay  algunas  indicaciones  posibles:

1.  La  función  es  grande,  es  decir,  contiene  muchas  líneas  de  código  (ver  la  siguiente  sección  sobre  funciones  pequeñas).

2.  Intenta  encontrar  un  nombre  significativo  y  expresivo  para  la  función  que  exactamente
describe  su  propósito,  pero  no  puede  evitar  el  uso  de  conjunciones,  como  "y"  o  "o",  para  construir  el  nombre.  
(Consulte  también  una  de  las  siguientes  secciones  sobre  nombres).

3.  El  cuerpo  de  una  función  se  separa  verticalmente  usando  líneas  vacías  en  grupos
que  representan  pasos  posteriores.  A  menudo,  estos  grupos  también  se  presentan  con  comentarios  que  
son  como  titulares.

4.  La  complejidad  ciclomática  es  alta.  La  función  contiene  muchos  'if',  'else'  o
declaraciones  de  'cambio  de  caso'.

5.  La  función  tiene  muchos  argumentos  (ver  la  sección  sobre  Argumentos  y  Retorno).
Valores  más  adelante  en  este  capítulo),  especialmente  uno  o  más  argumentos  de  bandera  de  tipo  bool.

Déjalos  ser  pequeños

Una  pregunta  central  con  respecto  a  las  funciones  es  esta:  ¿Cuál  debería  ser  la  longitud  máxima  de  una  función?
Hay  muchas  reglas  generales  y  heurísticas  para  la  longitud  de  una  función.  Por  ejemplo,  algunos  dicen  que  una  función  debe  caber  en  la  
pantalla  verticalmente.  Bien,  a  primera  vista  parece  una  regla  no  tan  mala.  Si  una  función  cabe  en  la  pantalla,  no  es  necesario  que  el  desarrollador  se  
desplace.  Por  otro  lado,  ¿la  altura  de  mi  pantalla  debería  realmente  determinar  el  tamaño  máximo  de  una  función?  Las  alturas  de  las  pantallas  no  
son  todas  iguales.  Por  lo  tanto,  personalmente  no  creo  que  sea  una  buena  regla.  Aquí  está  mi  consejo  sobre  este  tema:

Las  funciones  deben  ser  bastante  pequeñas.  Idealmente  4–5  líneas,  máximo  12–15  líneas,  pero  no  más.

¡Pánico!  Ya  puedo  escuchar  el  clamor:  “¿Muchas  funciones  diminutas?  ¡¿HABLAS  EN  SERIO?!"
Sí,  en  serio.  Como  ya  escribió  Robert  C.  Martin  en  su  libro  Clean  Code  [Martin09]:  Las  funciones  deberían  ser  pequeñas,  y  deberían  ser  
más  pequeñas  que  eso.
Las  funciones  grandes  suelen  tener  una  alta  complejidad.  Los  desarrolladores  a  menudo  no  son  capaces  de  decir  de  un  vistazo  qué
tal  función  lo  hace.  Si  una  función  es  demasiado  grande,  normalmente  tiene  demasiadas  responsabilidades  (consulte  la  sección  anterior)  y  
no  hace  exactamente  una  sola  cosa.  Cuanto  más  grande  es  una  función,  más  difícil  es  entenderla  y  mantenerla.  Tales  funciones  a  menudo  
contienen  muchas  decisiones,  en  su  mayoría  anidadas  (if,  else,  switch)  y  bucles.  Esto  también  se  conoce  como  alta  complejidad  ciclomática.

Por  supuesto,  como  con  cualquier  regla,  puede  haber  pocas  excepciones  justificadas.  Por  ejemplo,  una  función  que  contiene
una  sola  declaración  de  cambio  grande  podría  ser  aceptable  si  es  extremadamente  limpia  y  fácil  de  leer.
Puede  tener  una  declaración  de  cambio  de  400  líneas  en  una  función  (a  veces  necesaria  para  manejar  diferentes  tipos  de  datos  entrantes  en  los  
sistemas  de  telecomunicaciones),  y  está  perfectamente  bien.

“¡Pero  el  tiempo  de  llamada  por  encima!”

La  gente  ahora  podría  objetar  que  muchas  funciones  pequeñas  reducen  la  velocidad  de  ejecución  de  un  programa.
Podrían  argumentar  que  cualquier  llamada  de  función  es  costosa.
Permítanme  explicar  por  qué  creo  que  estos  temores  son  infundados  en  la  mayoría  de  los  casos.

59
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Sí,  hubo  momentos  en  que  los  compiladores  de  C++  no  eran  muy  buenos  para  optimizar  y  las  CPU  eran  comparativamente  lentas.  
Fue  en  un  momento  en  que  se  difundió  el  mito  de  que  C  ++  es  generalmente  más  lento  que  C.  Tales  mitos  son  propagados  por  personas  que  no  
conocen  muy  bien  el  lenguaje.  Y  los  tiempos  han  cambiado.
Hoy  en  día,  los  compiladores  modernos  de  C++  son  muy  buenos  para  optimizar.  Por  ejemplo,  pueden  realizar  múltiples
optimizaciones  de  aceleración  locales  y  globales.  Pueden  reducir  muchas  construcciones  de  C++,  como  bucles  o  declaraciones  condicionales,  a  
secuencias  funcionalmente  similares  de  código  de  máquina  muy  eficiente.  Y  ahora  son  lo  suficientemente  inteligentes  para  funciones  en  línea  
automáticamente,  si  esas  funciones  pueden  estar  básicamente  en  línea  (...  por  supuesto,  a  veces  no  es  posible  hacer  eso).

E  incluso  el  Linker  puede  realizar  optimizaciones.  Por  ejemplo,  Visual­Studio  Compiler/  Linker  de  Microsoft  proporciona  una  función  llamada  
Optimización  de  todo  el  programa  que  permite  que  el  compilador  y  el  vinculador  realicen  optimizaciones  globales  con  información  sobre  todos  los  
módulos  del  programa.  Y  con  otra  función  de  Visual­Studio  llamada  Optimizaciones  guiadas  por  perfiles,  el  compilador  optimiza  un  programa  
utilizando  datos  recopilados  de  las  ejecuciones  de  prueba  de  creación  de  perfiles  del  archivo .exe  o .dll.

Incluso  si  no  queremos  usar  las  opciones  de  optimización  del  compilador,  ¿de  qué  estamos  hablando  cuando
consideramos  una  llamada  de  función?

Una  CPU  Intel  Core  i7  2600K  es  capaz  de  realizar  128.300  millones  de  instrucciones  por  segundo  (MIPS)  a  una  velocidad  de  reloj  de  3,4  GHz.  
Damas  y  caballeros,  cuando  hablamos  de  llamadas  de  función,  ¡estamos  hablando  de  unos  pocos  nanosegundos!  La  luz  viaja  aproximadamente  30  cm  
en  un  nanosegundo  (0,000000001  seg).  En  comparación  con  otras  operaciones  en  una  computadora,  como  el  acceso  a  la  memoria  fuera  del  caché  
o  el  acceso  al  disco  duro,  una  llamada  de  función  es  mucho  más  rápida.

Los  desarrolladores  deberían  invertir  su  precioso  tiempo  en  problemas  reales  de  rendimiento,  que  generalmente  tienen  su  origen  en  una  
mala  arquitectura  y  diseño.  Solo  en  circunstancias  muy  especiales  tiene  que  preocuparse  por  la  sobrecarga  de  llamadas  a  funciones.

Nomenclatura  de  funciones  En  general,  

se  puede  decir  que  las  mismas  reglas  de  nomenclatura  que  las  de  las  variables  y  constantes  son,  en  la  medida  de  lo  posible,  aplicables  también  a  las  
funciones,  respectivamente,  a  los  métodos.  Los  nombres  de  las  funciones  deben  ser  claros,  expresivos  y  autoexplicativos.  No  debería  tener  que  
leer  el  cuerpo  de  una  función  para  saber  lo  que  hace.  Debido  a  que  las  funciones  definen  el  comportamiento  de  un  programa,  normalmente  tienen  
un  verbo  en  su  nombre.  Se  utiliza  algún  tipo  especial  de  funciones  para  proporcionar  información  sobre  un  estado.  Sus  nombres  a  menudo  comienzan  
con  "is..."  o  "has...".

El  nombre  de  una  función  debe  comenzar  con  un  verbo.  Los  predicados,  es  decir,  declaraciones  sobre  un  objeto  que  pueden  ser  
verdaderos  o  falsos,  deben  comenzar  con  "is"  o  "has".

Estos  son  algunos  ejemplos  de  nombres  de  métodos  expresivos:

Listado  4­21.  Solo  algunos  ejemplos  de  nombres  expresivos  y  autoexplicativos  para  funciones  miembro

void  CustomerAccount::grantDiscount(DiscountValue  descuento);  void  Asunto::adjuntarObservador(const  
Observador&  observador);  void  Asunto::notificar  a  Todos  los  Observadores()  const;  int  
Embotellado::getTotalAmountOfFilledBottles()  const;  bool  
PuertaAutomática::isOpen()  const;  bool  CardReader::isEnabled()  const;  bool  
DoubleLinkedList::hasMoreElements()  const;

60
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Use  nombres  reveladores  de  intenciones
Eche  un  vistazo  a  la  siguiente  línea  de  código,  que  es,  por  supuesto,  solo  un  pequeño  extracto  de  un  programa  más  grande:

std::string  head  =  html.substr(startOfHeader,  lengthOfHeader);

Esta  línea  de  código  se  ve  bien  en  principio.  Hay  una  cadena  de  C++  (encabezado  <cadena>)  llamada  html,  
que  contiene  una  parte  de  HTML  (lenguaje  de  marcado  de  hipertexto),  obviamente.  Cuando  se  ejecuta  esta  línea,  se  
recupera  una  copia  de  una  subcadena  de  html  y  se  asigna  a  una  nueva  cadena  llamada  head.  La  subcadena  se  define  
mediante  dos  parámetros:  uno  que  establece  el  índice  inicial  de  la  subcadena  y  otro  que  define  la  cantidad  de  caracteres  
que  se  incluirán  en  la  subcadena.
Bien,  acabo  de  explicar  en  detalle  cómo  se  extrae  el  encabezado  de  un  fragmento  de  HTML.  Deja  que  te  enseñe
otra  versión  del  mismo  código:

Listado  4­22.  Después  de  introducir  un  nombre  que  revele  la  intención,  el  código  es  mejor  comprensible.

std::string  ReportRenderer::extractHtmlHeader(const  std::string&  html)  {
return  html.substr(startOfHeader,  lengthOfHeader); }

// ...

std::string  head  =  extractHtmlHeader(html);

¿Puedes  ver  cuánta  claridad  podría  aportar  un  pequeño  cambio  como  este  a  tu  código?  Hemos  introducido  una  
pequeña  función  miembro  que  explica  su  intención  por  su  nombre  semántico.  Y  en  el  lugar  donde  originalmente  se  podía  
encontrar  la  operación  de  cadena,  hemos  reemplazado  la  invocación  directa  de  std::string::substr()  por  una  llamada  de  
nuestra  nueva  función.

El  nombre  de  una  función  debe  expresar  su  intención/propósito  y  no  explicar  cómo  funciona.

Cómo  se  realiza  el  trabajo,  eso  es  lo  que  debería  ver  en  el  código  del  cuerpo  de  la  función.  No  expliques  el
Cómo  en  un  nombre  de  funciones.  En  su  lugar,  exprese  el  propósito  de  la  función  desde  una  perspectiva  comercial.
Además,  tenemos  otra  ventaja.  La  funcionalidad  parcial  de  cómo  se  extrae  el  encabezado  de
la  página  HTML  ha  sido  casi  aislada  y  ahora  es  más  fácil  de  reemplazar  sin  andar  a  tientas  en  aquellos  lugares  donde  se  
llama  a  la  función.

Argumentos  y  valores  devueltos  Después  de  
haber  discutido  los  nombres  de  funciones  en  detalle,  hay  otro  aspecto  que  es  importante  para  funciones  buenas  
y  limpias:  los  argumentos  de  la  función  y  los  valores  devueltos.  Ambos  también  contribuyen  significativamente  al  
hecho  de  que  una  función  o  método  se  puede  entender  bien  y  es  fácil  de  usar  para  los  clientes.

61
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Número  de  argumentos  ¿Cuántos  

argumentos  (también  conocidos  como  parámetros)  debe  tener  una  función  (sinónimos:  método,  operación)  como  máximo?
En  Clean  Code  encontramos  la  siguiente  recomendación:

El  número  ideal  de  argumentos  para  una  función  es  cero  (niládico).  Luego  viene  uno  (monádico),  
seguido  de  cerca  por  dos  (diádico).  Deben  evitarse  tres  argumentos  (triádicos)  siempre  que  sea  
posible.  Más  de  tres  (poliádico)  requiere  una  justificación  muy  especial,  y  de  todos  modos  no  
debería  usarse.
—  Robert  C.  Martin,  Código  limpio  [Martin09]

Este  consejo  es  por  tanto  muy  interesante  ya  que  Martin  recomienda  que  una  función  ideal  no  debe  tener  argumentos.  Esto  es  
un  poco  extraño  porque  una  función  en  el  sentido  matemático  puro  (y  =  f(x))  siempre  tiene  al  menos  un  argumento  (vea  también  el  
capítulo  sobre  Programación  Funcional).  Esto  significa  que  una  “función  sin  argumentos”  por  lo  general  debe  tener  algún  tipo  de  efecto  
secundario.
Tenga  en  cuenta  que  Martin  usa  ejemplos  de  código  escritos  en  Java  en  su  libro,  por  lo  que  en  realidad  se  refiere  a  los  métodos  
de  una  clase  cuando  habla  de  funciones.  Tenemos  que  considerar  que  hay  un  “argumento”  implícito  adicional  disponible  para  los  
métodos  de  un  objeto:  ¡este!  El  puntero  this  representa  el  contexto  de  ejecución.  Con  la  ayuda  de  esto,  una  función  miembro  puede  
acceder  a  los  atributos  de  su  clase,  leerlos  o  manipularlos.  En  otras  palabras:  desde  la  perspectiva  de  una  función  miembro,  los  
atributos  de  una  clase  no  son  más  que  variables  globales.  Entonces,  la  regla  de  Martin  parece  ser  una  guía  adecuada,  pero  creo  
que  es  principalmente  apropiada  para  diseños  orientados  a  objetos.
Pero,  ¿por  qué  demasiados  argumentos  son  malos?
En  primer  lugar,  todos  los  argumentos  de  la  lista  de  argumentos  de  una  función  pueden  conducir  a  una  dependencia,  con  
la  excepción  de  los  argumentos  de  tipos  integrados  estándar  como  int  o  double.  Si  usa  un  tipo  complejo  (por  ejemplo,  una  clase)  en  la  
lista  de  argumentos  de  una  función,  su  código  depende  de  ese  tipo.  Se  debe  incluir  el  archivo  de  cabecera  que  contiene  el  tipo  utilizado.
Además,  cada  argumento  debe  procesarse  en  algún  lugar  dentro  de  una  función  (si  no,  el  argumento
es  innecesario  y  debe  eliminarse  inmediatamente).  Tres  argumentos  pueden  llevar  a  una  función  relativamente  compleja,  como  
hemos  visto  en  el  ejemplo  de  la  función  miembro  BasicFrame::QueryFileName()  de  OpenOffice  de  Apache.

En  la  programación  de  procedimientos,  a  veces  puede  ser  muy  difícil  no  exceder  los  tres  argumentos.  En  C,  por  ejemplo,  a  
menudo  verá  funciones  con  más  argumentos.  Un  ejemplo  disuasorio  es  la  anticuada  Win32­API  de  Windows.

Listado  4­23.  La  función  Win32  CreateWindowEx  para  crear  ventanas

HWND  CreateWindowEx  (

DWORD  dwExStyle,
LPCTSTR  lpClassName,
LPCTSTR  lpWindowName,
DWORD  dwStyle,  
int  x,  int  
y,  int  
nWidth,  int  
nHeight,
HWND  hWndPadre,
HMENÚ  hMenú,
HINSTANCIA  hInstancia,
LPVOID  lpParam);

62
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Bueno,  este  feo  código  viene  de  la  antigüedad,  obviamente.  Estoy  bastante  seguro  de  que  si  se  diseñara  hoy  en  día,  la  API  de  
Windows  ya  no  se  vería  así.  No  sin  razón,  existen  numerosos  marcos,  como  Microsoft  Foundation  Classes  (MFC),  Qt  (https://www.qt.io),  
o  wxWidgets  (https://www.wxwidgets.org ),  que  envuelve  esta  espeluznante  interfaz  y  ofrece  formas  más  simples  y  más  orientadas  a  objetos  
para  crear  una  interfaz  gráfica  de  usuario  (UI).

Y  hay  pocas  posibilidades  de  reducir  el  número  de  argumentos.  Puede  combinar  x,  y,  nWidth  y  nHeight  en  una  nueva  estructura  llamada  
Rectangle,  pero  todavía  hay  nueve  argumentos.  Un  agravante  es  que  algunos  de  los  argumentos  de  esta  función  son  punteros  a  otras  
estructuras  complejas,  que  a  su  vez  están  compuestas  por  muchos  atributos.

En  buenos  diseños  orientados  a  objetos,  por  lo  general  no  se  requieren  listas  de  argumentos  tan  largas.  Pero  C++  no  es  un  
lenguaje  orientado  a  objetos  puro,  como  Java  o  C#.  En  Java,  todo  debe  estar  incrustado  en  una  clase,  lo  que  a  veces  conduce  a  mucho  
código  repetitivo.  En  C++  esto  no  es  necesario.  Se  le  permite  implementar  funciones  independientes  en  C++,  es  decir,  funciones  que  no  
son  miembros  de  una  clase.  Y  eso  está  bastante  bien.
Así  que  aquí  está  mi  consejo  sobre  este  tema:

Las  funciones  reales  deben  tener  la  menor  cantidad  de  argumentos  posible.  Un  argumento  es  el  número  ideal.  Las  
funciones  miembro  (métodos)  de  una  clase  a  menudo  no  tienen  argumentos.  Por  lo  general,  esas  funciones  manipulan  
el  estado  interno  del  objeto,  o  se  usan  para  consultar  algo  del  objeto.

Evite  los  argumentos  de  bandera

Un  argumento  de  bandera  es  un  tipo  de  argumento  que  le  dice  a  una  función  que  realice  una  operación  diferente  dependiendo  de  su  valor.  
Los  argumentos  de  marca  son  en  su  mayoría  de  tipo  bool  y,  a  veces,  incluso  una  enumeración.

Listado  4­24.  Un  argumento  de  bandera  para  controlar  el  nivel  de  detalle  de  una  factura

Facturación  de  facturas::createInvoice(const  BookingItems&  items,  const  bool  withDetails)  { if  (withDetails)  { //...

}  más  { //...

} }

El  problema  básico  con  los  argumentos  de  bandera  es  que  introduces  dos  (oa  veces  incluso  más)  caminos  a  través  de  tu  
función.  Dicho  argumento  generalmente  se  evalúa  en  algún  lugar  dentro  de  la  función  en  una  declaración  if­  o  switch/case.  Se  utiliza  para  
determinar  si  se  debe  o  no  realizar  una  determinada  acción.  Significa  que  la  función  no  está  haciendo  una  cosa  exactamente  como  
debería  (consulte  la  sección  "Una  cosa,  no  más",  anteriormente  en  este  capítulo).  Es  un  caso  de  cohesión  débil  (ver  Capítulo  3)  y  viola  el  
Principio  de  Responsabilidad  Única  (ver  Capítulo  6  sobre  la  Orientación  a  Objetos).

Y  si  ve  la  llamada  a  la  función  en  algún  lugar  del  código,  no  sabe  exactamente  qué  significa  verdadero  o  falso  sin  analizar  la  función  
Billing::createInvoice()  en  detalle:

Listado  4­25.  Desconcertante:  ¿Qué  significa  el  'verdadero'  en  la  lista  de  argumentos?

Facturación  facturación;  
Factura  factura  =  facturación.createInvoice(bookingItems,  true);

Mi  consejo  es  que  simplemente  evites  los  argumentos  de  bandera.  Este  tipo  de  argumentos  son  siempre
necesario  si  la  preocupación  de  realizar  una  acción  no  está  separada  de  su  configuración.

63
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Una  solución  podría  ser  proporcionar  funciones  separadas  y  bien  nombradas  en  su  lugar:

Listado  4­26.  Más  fácil  de  comprender:  dos  funciones  miembro  con  nombres  reveladores  de  intenciones

Facturación  Factura::createSimpleInvoice(const  BookingItems&  items)  {
//...
}

Facturación  de  facturas::createInvoiceWithDetails(const  BookingItems&  items)  
{ Factura  de  factura  =  createSimpleInvoice(items); //...añadir  
detalles  a  la  factura...
}

Otra  solución  sería  una  jerarquía  de  especialización  de  facturación:

Listado  4­27.  Diferentes  niveles  de  detalles  para  facturas,  realizados  de  forma  orientada  a  objetos.

class  Billing  
{ public:  
virtual  Invoice  createInvoice(const  BookingItems&  items)  =  0; // ... };

class  SimpleBilling :  public  Billing  { public:  
virtual  
Invoice  createInvoice(const  BookingItems&  items)  override; // ... };

clase  FacturaciónDetallada:  facturación  pública  
{ público:  
factura  virtual  createInvoice(const  BookingItems&  items)  override; // ...  privado:  

SimpleBilling  simpleBilling; };

La  variable  de  miembro  privado  de  tipo  SimpleBilling  se  requiere  en  la  clase  DetailBilling  para  poder  
realizar  primero  una  creación  de  factura  simple  sin  duplicación  de  código  y  luego  agregar  los  detalles  a  la  
factura.

ANULAR  ESPECIFICADOR  [C++11]

Desde  C  ++  11,  se  puede  especificar  explícitamente  que  una  función  virtual  anulará  una  función  virtual  de  clase  
base.  Para  este  propósito,  se  ha  introducido  el  identificador  de  anulación .

Si  override  aparece  inmediatamente  después  de  la  declaración  de  una  función  miembro,  el  compilador  comprobará  
que  la  función  es  virtual  y  está  anulando  una  función  virtual  de  una  clase  base.  Por  lo  tanto,  los  desarrolladores  
están  protegidos  de  errores  sutiles  que  pueden  surgir  cuando  simplemente  piensan  que  han  anulado  una  función  
virtual,  pero  en  realidad  han  alterado/añadido  una  nueva  función,  por  ejemplo,  debido  a  un  error  tipográfico.

64
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Evite  los  argumentos  de  salida
Un  parámetro  de  salida,  a  veces  también  llamado  parámetro  de  resultado,  es  un  argumento  de  función  que  se  usa  para  el  valor  de  
retorno  de  la  función.
Uno  de  los  beneficios  mencionados  con  frecuencia  del  uso  de  argumentos  de  salida  es  que  las  funciones  que  los  usan  pueden
devolver  más  de  un  valor  a  la  vez.  Aquí  está  un  ejemplo  típico:

bool  ScriptInterpreter::executeCommand(const  std::string&  nombre,
const  std::vector<std::string>&  argumentos,
resultado&  resultado);

Esta  función  miembro  de  la  clase  ScriptInterpreter  devuelve  no  solo  un  bool.  El  tercer  argumento  es  una  referencia  no  
constante  a  un  objeto  de  tipo  Result,  que  representa  el  resultado  real  de  la  función.  El  valor  de  retorno  booleano  es  para  determinar  
si  la  ejecución  del  comando  fue  exitosa  por  parte  del  intérprete.  Una  llamada  típica  de  esta  función  miembro  podría  verse  así:

Intérprete  de  ScriptInterpreter; //  Muchas  
otras  preparaciones...
resultado  resultado;

if  (interpreter.executeCommand(commandName,  argumentList,  result))  {
//  Continuar  normalmente... }  else  
{ //  Manejar  
la  ejecución  fallida  del  comando...
}

Mi  simple  consejo  es  este:

Evite  los  argumentos  de  salida  a  toda  costa.

Los  argumentos  de  salida  no  son  intuitivos  y  pueden  generar  confusión.  A  veces,  la  persona  que  llama  no  puede  averiguar  
fácilmente  si  un  objeto  pasado  se  trata  como  un  parámetro  de  salida  y  posiblemente  la  función  lo  modifique.
Además,  los  parámetros  de  salida  complican  la  fácil  composición  de  expresiones.  Si  las  funciones  sólo  tienen
un  valor  de  retorno,  se  pueden  interconectar  con  bastante  facilidad  a  llamadas  de  función  encadenadas.  Por  el  contrario,  si  las  funciones  
tienen  múltiples  parámetros  de  salida,  los  desarrolladores  se  ven  obligados  a  preparar  y  manejar  todas  las  variables  que  contendrán  los  
valores  de  los  resultados.  Por  lo  tanto,  el  código  que  llama  a  estas  funciones  puede  convertirse  rápidamente  en  un  desastre.
Especialmente  si  se  debe  fomentar  la  inmutabilidad  y  se  deben  reducir  los  efectos  secundarios,  entonces  los  parámetros  de  
salida  son  una  idea  absolutamente  terrible.  Como  era  de  esperar,  todavía  es  imposible  pasar  un  objeto  inmutable  (consulte  el  Capítulo  9)  
como  un  parámetro  de  salida.
Si  un  método  debe  devolver  algo  a  sus  llamadores,  deje  que  el  método  lo  devuelva  como  el  valor  devuelto  por  los  métodos.
Si  el  método  debe  devolver  varios  valores,  rediseñarlo  para  que  devuelva  una  única  instancia  de  un  objeto  que  contenga  los  valores.  
Alternativamente,  se  puede  usar  una  std::tuple  (ver  Barra  lateral)  o  un  std::pair.

sesenta  y  cinco
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

std::tupla  Y  std::make_tuple  [C++11]

Una  plantilla  de  clase  a  veces  útil  está  disponible  desde  C  ++  11  que  puede  contener  una  colección  de  tamaño  fijo  de  
valores  heterogéneos  resp.  objetos:  std::tuple.  Se  define  en  el  encabezado  <tuple>  de  la  siguiente  manera:

plantilla<  clase...  Tipos  >  
clase  tupla;

Es  una  plantilla  llamada  variádica,  es  decir,  es  una  plantilla  que  puede  tomar  un  número  variable  de  argumentos  de  
plantilla.  Por  ejemplo,  si  debe  contener  varios  valores  diferentes  de  diferentes  tipos  como  un  solo  objeto,  puede  escribir  lo  
siguiente:

usando  Cliente  =  std::tuple<std::string,  std::string,  std::string,  Money,  unsigned  int>; // ...

Cliente  unCliente  =  std::make_tuple("Stephan",  "Roth",  "Bad  Schwartau",
saldo  pendiente,  timeForPaymentInDays);

std::make_tuple  crea  el  objeto  tupla,  deduciendo  el  tipo  objetivo  de  los  tipos  de  argumentos.  Con  la  palabra  clave  auto  puede  
dejar  que  el  compilador  deduzca  el  tipo  de  aCustomer  de  su  inicializador:

auto  aCustomer  =  std::make_tuple("Stephan",  "Roth",  "Bad  Schwartau",  saldo  
pendiente,  timeForPaymentInDays);

Desafortunadamente,  el  acceso  a  elementos  individuales  de  una  instancia  de  std::tuple  solo  es  posible  a  través  de  su  índice.
Por  ejemplo,  para  recuperar  la  ciudad  de  un  Cliente,  debe  escribir  el  siguiente  código:

auto  city  =  std::get<2>(unCliente);

Esto  es  contrario  a  la  intuición  y  puede  reducir  la  legibilidad  del  código.

Mi  consejo  es  usar  la  plantilla  de  clase  std::tuple  solo  en  casos  excepcionales.  Solo  debe  usarse  para  combinar  cosas  
temporalmente,  que  de  todos  modos  no  van  juntas.  Una  vez  que  los  datos  (atributos,  objetos)  deben  mantenerse  juntos,  
debido  a  que  su  cohesión  es  alta,  generalmente  justifica  la  introducción  de  un  tipo  explícito  para  este  conjunto  de  datos:  ¡una  
clase!

Si  también  debe  distinguir  básicamente  entre  el  éxito  y  el  fracaso,  entonces  puede  usar  el  llamado
Patrón  de  objeto  de  caso  especial  (consulte  el  Capítulo  9  sobre  patrones  de  diseño)  para  devolver  un  objeto  que  representa  un  
resultado  no  válido.

66
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

No  pase  ni  devuelva  0  (NULL,  nullptr)

EL  ERROR  DEL  MIL  MILLÓN  DE  DÓLARES

Sir  Charles  Antony  Richard  Hoare,  comúnmente  conocido  como  Tony  Hoare  o  CAR  Hoare,  es  un  famoso  
científico  informático  británico.  Es  conocido  principalmente  por  el  algoritmo  Quick  Sort.  En  1965,  Tony  Hoare  
trabajó  junto  con  el  informático  suizo  Niklaus  E.  Wirth  en  el  desarrollo  del  lenguaje  de  programación  ALGOL.  
Introdujo  referencias  nulas  en  el  lenguaje  de  programación  ALGOL  W,  que  fue  el  predecesor  de  PASCAL.

Más  de  40  años  después,  Tony  Hoare  lamenta  esta  decisión.  En  una  charla  en  la  Conferencia  QCon  2009  en  
Londres,  dijo  que  la  introducción  de  referencias  nulas  probablemente  había  sido  un  error  histórico  de  mil  
millones  de  dólares.  Argumentó  que  las  referencias  nulas  ya  habían  causado  tantos  problemas  durante  los  siglos  
pasados,  que  el  costo  probablemente  podría  ser  de  aproximadamente  mil  millones  de  dólares.

En  C++,  los  punteros  pueden  apuntar  a  NULL  o  0.  Concretamente,  esto  significa  que  el  puntero  apunta  a  la  memoria
dirección  0.  NULL  es  solo  una  definición  de  macro:

#define  NULL 0

Desde  C++11,  el  lenguaje  proporciona  la  nueva  palabra  clave  nullptr,  que  es  del  tipo  std::nullptr_t.
A  veces  veo  funciones  como  esta:

Customer*  findCustomerByName(const  std::string&  name)  const  { //  Código  que  busca  
al  cliente  por  nombre... // ...y  si  no  se  encuentra  el  cliente:  return  
nullptr; // ...o  NULL; }

Recibiendo  NULL  o  nullptr  (A  partir  de  aquí,  solo  usaré  nullptr  en  el  siguiente  texto  por  el  bien
de  simplicidad)  como  valor  de  retorno  de  una  función  puede  ser  confuso.  ¿Qué  debe  hacer  la  persona  que  llama  con  él?  
¿Qué  significa?  En  el  ejemplo  anterior,  podría  ser  que  no  exista  un  cliente  con  el  nombre  dado.  Pero  también  puede  significar  
que  podría  haber  habido  un  error  crítico.  Un  nullptr  puede  significar  fracaso,  puede  significar  éxito  y  puede  significar  casi  
cualquier  cosa.
Mi  consejo  es  este:

Si  es  inevitable  devolver  un  puntero  regular  como  resultado  de  una  función  o  método,  ¡no  devuelva  nullptr!

En  otras  palabras:  si  se  ve  obligado  a  devolver  un  puntero  normal  como  resultado  de  una  función  (veremos  más  adelante
en  que  puede  haber  mejores  alternativas),  asegúrese  de  que  el  puntero  que  está  devolviendo  siempre  apunte  a  una  dirección  
válida.  Estas  son  mis  razones  por  las  que  creo  que  esto  es  importante.

67
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

La  razón  principal  por  la  que  no  debe  devolver  nullptr  de  una  función  es  que  transfiere  la  responsabilidad  
de  decidir  qué  hacer  a  sus  llamadores.  Tienen  que  comprobarlo.  Tienen  que  lidiar  con  eso.  Si  las  funciones  pueden  
potencialmente  devolver  nullptr,  esto  lleva  a  muchas  verificaciones  nulas,  como  esta:

Cliente*  cliente  =  findCustomerByName("Stephan");

if  (cliente !=  nullptr)  
{ ProductosPedidos*  productospedidos  =  cliente­>getAllOrderedProducts();  if  (productospedidos !
=  nullptr)  {
//  Hacer  algo  con  productospedidos... }  else  { //  ¿Y  
qué  
debemos  hacer  aquí?

} }  else  { //  
¿Y  qué  debemos  hacer  aquí?
}

Muchas  comprobaciones  nulas  reducen  la  legibilidad  del  código  y  aumentan  su  complejidad.  Y  hay  otro  problema  visible  
que  nos  lleva  directamente  al  siguiente  punto.
Si  una  función  puede  devolver  un  puntero  válido  o  nullptr,  introduce  una  ruta  de  flujo  alternativa  que  debe  continuar  la  
persona  que  llama.  Y  debería  conducir  a  una  reacción  razonable  y  sensata.  Esto  es  a  veces  bastante  problemático.  ¿Cuál  sería  
la  respuesta  correcta  e  intuitiva  en  nuestro  programa  cuando  nuestro  puntero  a  Customer  no  apunta  a  una  instancia  válida,  
sino  a  nullptr?  ¿Debe  el  programa  cancelar  la  operación  en  ejecución  con  un  mensaje?  ¿Existe  algún  requisito  de  que  
cierto  tipo  de  continuación  del  programa  sea  obligatoria  en  tales  casos?  Estas  preguntas  a  veces  no  se  pueden  responder  
bien.  La  experiencia  ha  demostrado  que  a  menudo  es  relativamente  fácil  para  las  partes  interesadas  describir  todos  los  llamados  
Happy  Day  Cases  de  su  software,  que  son  los  casos  positivos  durante  el  funcionamiento  normal.  Es  mucho  más  difícil  describir  
el  comportamiento  deseado  del  software  para  las  excepciones,  errores  y  casos  especiales.

La  peor  consecuencia  puede  ser  esta:  si  se  olvida  cualquier  verificación  nula,  esto  puede  conducir  a  un  tiempo  de  ejecución  crítico
errores  Eliminar  la  referencia  de  un  puntero  nulo  provocará  un  error  de  segmentación  y  la  aplicación  se  bloqueará.
En  C++  todavía  hay  otro  problema  a  considerar:  la  propiedad  del  objeto.
Para  la  persona  que  llama  a  la  función,  es  vago  qué  hacer  con  el  recurso  señalado  por  el  puntero  después  de  su  uso.  
¿Quién  es  su  dueño?  ¿Es  necesario  eliminar  el  objeto?  En  caso  afirmativo:  ¿Cómo  se  desechará  el  recurso?  ¿Se  debe  
eliminar  el  objeto  con  eliminar,  porque  se  asignó  con  el  operador  nuevo  en  algún  lugar  dentro  de  la  función?  ¿O  la  
propiedad  del  objeto  de  recurso  se  administra  de  manera  diferente,  por  lo  que  se  prohíbe  una  eliminación  y  dará  como  
resultado  un  comportamiento  indefinido  (consulte  la  sección  "No  permitir  un  comportamiento  indefinido"  en  el  Capítulo  5 )?  ¿Es  
quizás  incluso  un  recurso  del  sistema  operativo  que  debe  manejarse  de  una  manera  muy  especial?
De  acuerdo  con  el  Principio  de  ocultación  de  información  (consulte  el  Capítulo  3),  esto  no  debería  tener  relevancia  
para  la  persona  que  llama,  pero  de  hecho  le  hemos  impuesto  la  responsabilidad  del  recurso.  Y  si  la  persona  que  llama  no  
maneja  el  puntero  correctamente,  puede  provocar  errores  graves,  por  ejemplo,  fugas  de  memoria,  eliminación  doble,  
comportamiento  indefinido  y,  en  ocasiones,  vulnerabilidades  de  seguridad.

Estrategias  para  evitar  punteros  regulares

Prefiere  la  construcción  de  objetos  simples  en  la  pila  en  lugar  de  en  el  montón
La  forma  más  sencilla  de  crear  un  nuevo  objeto  es  simplemente  creándolo  en  la  pila,  de  esta  manera:

#include  "Cliente.h" // ...

cliente  cliente;

68
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

En  el  ejemplo  anterior,  se  crea  una  instancia  de  la  clase  Cliente  (definida  en  el  encabezado  Cliente.h)  en  el
pila.  La  línea  de  código  que  crea  la  instancia  generalmente  se  puede  encontrar  en  algún  lugar  dentro  del  cuerpo  de  una  
función  o  método.  Eso  significa  que  la  instancia  se  destruye  automáticamente  si  la  función  o  el  método  se  queda  fuera  del  
alcance,  lo  que  sucede  cuando  regresamos  de  la  función  o  el  método  respectivo.
Hasta  ahora,  todo  bien.  Pero,  ¿qué  haremos  si  un  objeto  que  se  creó  en  una  función  o  método  debe  devolverse  a  la  
persona  que  llama?
En  el  estilo  antiguo  de  C++,  este  desafío  a  menudo  se  enfrentaba  de  tal  manera  que  el  objeto  se  creaba  en  el
montón  (usando  el  operador  nuevo)  y  luego  devuelto  desde  la  función  como  un  puntero  a  este  recurso  asignado.

Cliente*  createDefaultCustomer()  {
Cliente*  cliente  =  nuevo  Cliente(); //  Hacer  algo  
más  con  el  cliente,  por  ejemplo,  configurarlo,  y  al  final...  volver  al  cliente;

La  razón  comprensible  de  este  enfoque  es  que,  si  estamos  tratando  con  un  objeto  grande,  un  costoso
La  construcción  de  copias  se  puede  evitar  de  esta  manera.  Pero  ya  hemos  discutido  los  inconvenientes  de  esta  solución  en  la  
sección  anterior.  Por  ejemplo,  ¿qué  debe  hacer  la  persona  que  llama  si  el  puntero  devuelto  es  nullptr?  Además,  la  persona  que  
llama  a  la  función  se  ve  obligada  a  estar  a  cargo  de  la  gestión  de  recursos  (por  ejemplo,  eliminar  el  puntero  devuelto  de  la  
manera  correcta).
Buenas  noticias:  desde  C++  11,  podemos  simplemente  devolver  objetos  grandes  como  valores  sin  preocuparnos  por  una  
construcción  de  copia  costosa.

Cliente  createDefaultCustomer()  {
cliente  cliente; //  Hacer  
algo  con  el  cliente,  y  al  final...  devolver  al  cliente;

La  razón  por  la  que  ya  no  tenemos  que  preocuparnos  por  la  gestión  de  recursos  en  este  caso  es  la  llamada  semántica  de  
movimiento,  que  se  admite  desde  C++  11.  En  pocas  palabras,  el  concepto  de  semántica  de  movimiento  permite  que  los  recursos  
se  "muevan"  de  un  objeto  a  otro  en  lugar  de  copiarlos.  El  término  "mover"  significa  en  este  contexto  que  los  datos  internos  de  un  
objeto  se  eliminan  del  antiguo  objeto  de  origen  y  se  colocan  en  un  nuevo  objeto.  Es  una  transferencia  de  propiedad  de  los  datos  
de  un  objeto  a  otro  objeto,  y  esto  se  puede  realizar  extremadamente  rápido  (la  semántica  de  movimiento  de  C++  11  se  analiza  en  
detalle  en  el  siguiente  Capítulo  5 ).
Con  C++11,  todas  las  clases  de  contenedores  de  la  biblioteca  estándar  se  han  ampliado  para  admitir  la  semántica  de  movimiento.
Esto  no  solo  los  ha  hecho  muy  eficientes,  sino  también  mucho  más  fáciles  de  manejar.  Por  ejemplo,  para  devolver  un  vector  
grande  que  contiene  cadenas  de  una  función  de  una  manera  muy  eficiente,  puede  hacerlo  como  se  muestra  en  el  siguiente  
ejemplo:

Listado  4­28.  Desde  C++  11,  un  objeto  grande  instanciado  localmente  puede  devolverse  fácilmente  por  valor

#include  <vector>  
#include  <cadena>

utilizando  StringVector  =  std::vector<std::string>;  const  
StringVector::size_type  CANTIDAD_DE_CADENAS  =  10000;

StringVector  crearLargeVectorOfStrings()  {
StringVector  theVector(CANTIDAD_DE_CADENAS,  "Prueba");  
devuelve  el  Vector; //  ¡No  se  garantiza  la  construcción  de  copias  aquí! }

69
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

La  explotación  de  la  semántica  de  movimiento  es  una  muy  buena  manera  de  deshacerse  de  muchos  punteros  regulares.  Pero  podemos  
hacer  mucho  más…

En  la  lista  de  argumentos  de  una  función,  use  referencias  (const)  en  lugar  de  punteros
En  vez  de  escribir...

función  vacía  (tipo  *  argumento);

…deberías  usar  referencias  de  C++,  así:

función  vacía  (tipo  y  argumento);

La  principal  ventaja  de  usar  referencias  en  lugar  de  punteros  para  los  argumentos  es  que  no  es  necesario  verificar  que  la  
referencia  no  sea  nullptr.  La  razón  simple  de  esto  es  que  las  referencias  nunca  son  "NULAS".  (Está  bien,  sé  que  hay  algunas  posibilidades  
sutiles  en  las  que  aún  puede  terminar  con  una  referencia  nula,  pero  esto  presupone  un  estilo  de  programación  muy  tonto  o  amateur).

Y  otra  ventaja  es  que  no  necesita  desreferenciar  nada  dentro  de  la  función  con  la  ayuda  del  operador  de  desreferencia  (*).  Eso  
conducirá  a  un  código  más  limpio.  La  referencia  se  puede  usar  dentro  de  la  función,  ya  que  se  ha  creado  localmente  en  la  pila.  Por  supuesto,  
si  no  desea  tener  ningún  efecto  secundario,  debe  convertirlo  en  una  referencia  constante  (consulte  la  próxima  sección  sobre  Corrección  
constante).

Si  es  inevitable  lidiar  con  un  puntero  a  un  recurso,  use  uno  inteligente
Si  es  inevitable  usar  un  puntero  porque  el  recurso  debe  crearse  en  el  montón  de  manera  obligatoria,  debe  envolverlo  de  inmediato  y  
aprovechar  el  llamado  lenguaje  RAII  (Resource  Acquisition  is  Initialization).  Eso  significa  que  debe  usar  un  puntero  inteligente  
para  ello.  Dado  que  los  punteros  inteligentes  y  el  lenguaje  RAII  juegan  un  papel  importante  en  el  C++  moderno,  hay  una  sección  
dedicada  a  este  tema  en  el  Capítulo  5.

Si  una  API  devuelve  un  puntero  sin  formato...
…,  bueno,  entonces  tenemos  un  “problema­depende”.
A  menudo,  las  API  devuelven  punteros  que  están  más  o  menos  fuera  de  nuestras  manos.  Los  ejemplos  típicos  son  las  
bibliotecas  de  terceros.
En  el  caso  afortunado  de  que  nos  enfrentemos  a  una  API  bien  diseñada  que  proporcione  métodos  de  fábrica  para  crear  
recursos  y  también  métodos  para  devolverlos  a  la  biblioteca  para  su  eliminación  segura  y  adecuada,  hemos  ganado.  En  este  caso,  una  
vez  más  podemos  aprovechar  el  lenguaje  RAII  (Resource  Acquisition  Is  Initialization;  consulte  el  Capítulo  5).  Podemos  crear  un  
puntero  inteligente  personalizado  para  envolver  el  puntero  normal,  cuyo  asignador  resp.  deallocator  podría  manejar  el  recurso  
administrado  como  lo  esperaba  la  biblioteca  de  terceros.

El  poder  de  la  corrección  constante
La  corrección  constante  es  un  enfoque  poderoso  para  un  código  mejor  y  más  seguro  en  C++.  El  uso  de  const  puede  ahorrar  muchos  
problemas  y  tiempo  de  depuración,  porque  las  violaciones  de  const  provocan  errores  en  tiempo  de  compilación.  Y  como  una  especie  de  
efecto  secundario,  el  uso  de  const  también  puede  ayudar  al  compilador  a  aplicar  algunos  de  sus  algoritmos  de  optimización.  Eso  significa  
que  el  uso  adecuado  de  este  calificador  también  es  una  manera  fácil  de  aumentar  un  poco  el  rendimiento  de  ejecución  del  programa.

70
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Desafortunadamente,  muchos  desarrolladores  subestiman  los  beneficios  de  un  uso  intensivo  de  const.  Mi  consejo  es  este:

Preste  atención  a  la  corrección  constante.  Use  const  tanto  como  sea  posible  y  elija  siempre  una  declaración  adecuada  
de  variables  u  objetos  como  mutables  o  inmutables.

En  general,  la  palabra  clave  const  en  C++  evita  que  el  programa  pueda  mutar  los  objetos.  Pero  const  se  puede  usar  en  
diferentes  contextos.  La  palabra  clave  tiene  muchas  caras.
Su  uso  más  simple  es  definir  una  variable  como  una  constante:

const  largo  doble  PI  =  3.141592653589794;

Otro  uso  es  evitar  que  se  modifiquen  los  parámetros  que  se  pasan  a  una  función.  Dado  que  hay  varias  variaciones,  a  
menudo  genera  confusión.  Aquí  hay  unos  ejemplos:

unsigned  int  determineWeightOfCar(Car  const*  car); //  1  void  lacarCoche(Coche*  
const  coche); //  2  unsigned  int  determineWeightOfCar(Car  
const*  const  car); //  3  void  imprimirMensaje(const  std::string&  mensaje); //  4  void  
imprimirMensaje(std::string  const&  mensaje); //  5

1.  El  coche  puntero  apunta  a  un  objeto  constante  de  tipo  Coche,  es  decir,  el  objeto  Coche  (el  
“apuntado”)  no  se  puede  modificar.

2.  El  puntero  coche  es  un  puntero  constante  de  tipo  Coche,  es  decir,  puede  modificar  el  objeto  Coche,  
pero  no  puede  modificar  el  puntero  (por  ejemplo,  asignándole  una  nueva  instancia  de  Coche).

3.  En  este  caso,  tanto  el  puntero  como  la  punta  (el  objeto  Coche)  no  se  pueden
modificado.

4.  El  mensaje  de  argumento  se  pasa  por  referencia  constante  a  la  función,  es  decir,  el
La  variable  de  cadena  a  la  que  se  hace  referencia  no  se  puede  cambiar  dentro  de  la  función.

5.  Esta  es  solo  una  notación  alternativa  para  un  argumento  de  referencia  const.  Es  
funcionalmente  equivalente  a  la  línea  4  (…que  por  cierto  prefiero).

■  Sugerencia  Hay  una  regla  general  simple  para  leer  los  calificadores  const  de  la  manera  correcta.  Si  los  lee  de  
derecha  a  izquierda,  entonces  cualquier  calificador  const  que  aparezca  modifica  lo  que  está  a  la  izquierda.  Excepción:  si  
no  hay  nada  a  la  izquierda,  por  ejemplo,  al  principio  de  una  declaración,  entonces  const  modifica  la  cosa  a  su  derecha.

71
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Otro  uso  de  la  palabra  clave  const  es  declarar  una  función  miembro  (no  estática)  de  una  clase  como  const,  como  en  este  
ejemplo  en  la  línea  5:

#incluir  <cadena>

clase  Coche  
{ public:  
std::string  getRegistrationCode()  const;  void  
setRegistrationCode(const  std::string&  registrationCode); // ...

privado:  
std::string  _registrationCode; // ... };

A  diferencia  del  setter  de  la  línea  6,  la  función  miembro  getRegistrationCode  de  la  línea  5  no  puede  modificar  las  variables  
miembro  de  la  clase  Car.  La  siguiente  implementación  de  getRegistrationCode  provocará  un  error  de  compilación,  porque  la  función  
intenta  asignar  una  nueva  cadena  a  _registrationCode:

std::string  Coche::getRegistrationCode()  {
std::string  toBeReturned  =  código  de  registro;  código  de  
registro  =  "foo"; //  ¡Error  en  tiempo  de  compilación!  volver  a  ser  
devuelto;
}

Acerca  del  estilo  antiguo  de  C  en  proyectos  de  C++
Si  observa  programas  C++  relativamente  nuevos  (por  ejemplo,  en  GitHub  o  Sourceforge),  se  sorprenderá  de  cuántos  de  
estos  programas  supuestamente  "nuevos"  todavía  contienen  innumerables  líneas  de  código  C  antiguo.
Bueno,  C  sigue  siendo  un  subconjunto  del  lenguaje  C++.  Esto  significa  que  los  elementos  de  lenguaje  de  C  todavía  están  disponibles.
Desafortunadamente,  muchas  de  estas  antiguas  construcciones  de  C  tienen  importantes  inconvenientes  cuando  se  trata  de  
escribir  código  limpio,  seguro  y  moderno.  Y  claramente  hay  mejores  alternativas.
Por  lo  tanto,  un  consejo  básico  es  dejar  de  usar  esas  construcciones  de  C  antiguas  y  propensas  a  errores  siempre  
que  existan  mejores  alternativas  de  C++.  Y  hay  muchas  de  estas  posibilidades.  Hoy  en  día,  puede  prescindir  casi  por  completo  de  
la  programación  en  C  en  C++  moderno.

Preferir  cadenas  y  flujos  de  C++  a  caracteres  antiguos  de  estilo  C*  Una  cadena  denominada  C++  

forma  parte  de  la  biblioteca  estándar  de  C++  y  es  de  tipo  std::string  o  std::wstring  (ambas  definidas  en  el  encabezado  
<string>).  De  hecho,  ambas  son  definiciones  de  tipo  en  la  plantilla  de  clase  std::basic_string<T>  y  se  definen  de  esta  manera:

typedef  basic_string<char>  cadena;  typedef  
basic_string<wchar_t>  wstring;

72
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Para  crear  una  cadena  de  este  tipo,  se  debe  crear  una  instancia  de  un  objeto  de  una  de  estas  dos  plantillas,  por  ejemplo,  con  su  
constructor  de  inicialización:

std::string  nombre("Stephan");

En  comparación  con  esto,  una  cadena  de  estilo  C  es  simplemente  una  matriz  de  caracteres  (tipo  char  o  wchar_t)  que  termina  con  
un  terminador  cero  (a  veces  también  llamado  terminador  nulo).  Un  terminador  cero  es  un  carácter  especial  ('\0',  código  ASCII  0)  que  se  
utiliza  para  indicar  el  final  de  la  cadena.  Una  cadena  de  estilo  C  se  puede  definir  de  esta  manera:

char  nombre[]  =  "Stephan";

En  este  caso,  el  terminador  cero  se  agrega  automáticamente  al  final  de  la  cadena,  es  decir,  la  longitud  de  la  cadena  es  de  8  
caracteres.  Un  punto  importante  es  que  debemos  tener  en  cuenta  que  todavía  estamos  tratando  con  una  variedad  de  personajes.  
Esto  significa,  por  ejemplo,  que  tiene  un  tamaño  fijo.  Puede  cambiar  el  contenido  de  la  matriz  utilizando  el  operador  de  índice,  pero  
no  se  pueden  agregar  más  caracteres  al  final  de  la  matriz.  Y  si  el  terminador  cero  al  final  se  sobrescribe  accidentalmente,  esto  puede  
causar  varios  problemas.
La  matriz  de  caracteres  se  usa  a  menudo  con  la  ayuda  de  un  puntero  que  apunta  al  primer  elemento,  por  ejemplo,  cuando  se  
pasa  como  argumento  de  función:

char*  pointerToName  =  nombre;

función  vacía  (char*  pointerToCharacterArray)  {
//...
}

Sin  embargo,  en  muchos  programas  de  C++,  así  como  en  los  libros  de  texto,  las  cadenas  C  todavía  se  usan  con  frecuencia.  
¿Hay  alguna  buena  razón  para  usar  cadenas  de  estilo  C  en  C++  hoy  en  día?
Sí,  hay  algunas  situaciones  en  las  que  aún  puede  usar  cadenas  de  estilo  C.  Presentaré  algunas  de  estas  excepciones.  
Pero  para  la  abrumadora  cantidad  de  cadenas  en  un  programa  C++  moderno,  deben  implementarse  usando  cadenas  C++.  Los  objetos  
de  tipo  std::string  respectivamente  std::wstring  brindan  numerosas  ventajas  en  comparación  con  las  antiguas  cadenas  de  estilo  C:

•Los  objetos  de  cadena  C++  administran  su  memoria  por  sí  mismos,  de  modo  que  puede  copiarlos,  crearlos  y  
destruirlos  fácilmente.  Eso  significa  que  lo  liberan  de  la  administración  de  la  vida  útil  de  los  datos  de  la  
cadena,  lo  que  puede  ser  una  tarea  difícil  y  desalentadora  al  usar  matrices  de  caracteres  de  estilo  C.

•Son  mutables.  La  cadena  se  puede  manipular  fácilmente  de  varias  maneras:  agregando  cadenas  o  caracteres  
individuales,  concatenando  cadenas,  reemplazando  partes  de  la  cadena,  etc.

•Las  cadenas  C++  proporcionan  una  interfaz  iteradora  conveniente.  Al  igual  que  con  todos  los  demás  Estándar
Tipos  de  contenedores  de  biblioteca,  std::string  respectivamente  std::wstring  le  permite  iterar  sobre  sus  
elementos  (es  decir,  sobre  sus  caracteres).  Esto  también  significa  que  todos  los  algoritmos  adecuados  
que  se  definen  en  el  encabezado  <algoritmo>  se  pueden  aplicar  a  la  cadena.

•Las  cadenas  de  C++  funcionan  perfectamente  junto  con  flujos  de  E/S  de  C++  (p.  ej.,  ostream,
stringstream,  fstream,  etc.)  para  que  pueda  aprovechar  fácilmente  todas  esas  útiles  funciones  de  
transmisión.

•Desde  C++11,  la  biblioteca  estándar  utiliza  ampliamente  la  semántica  de  movimiento.  Muchos
los  algoritmos  y  contenedores  ahora  están  optimizados  para  movimiento.  Esto  también  se  aplica  a  las  cadenas  de  C++.
Por  ejemplo,  una  instancia  de  std::string  a  menudo  se  puede  devolver  simplemente  como  el  valor  
de  retorno  de  una  función.  Los  enfoques  que  antes  eran  todavía  necesarios  con  punteros  o  referencias  
para  devolver  de  manera  eficiente  objetos  de  cadena  grandes  desde  una  función,  es  decir,  sin  la  costosa  
copia  de  los  datos  de  la  cadena,  ahora  ya  no  son  necesarios.

73
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

En  resumen,  se  pueden  dar  los  siguientes  consejos:

Aparte  de  unas  pocas  excepciones,  las  cadenas  en  un  programa  C++  moderno  deben  estar  representadas  por  cadenas  C++  
tomadas  de  la  Biblioteca  estándar.

Bueno,  pero  ¿cuáles  son  las  pocas  excepciones  que  justifican  el  uso  de  cadenas  antiguas  de  estilo  C?
Por  un  lado,  están  las  constantes  de  cadena,  es  decir,  cadenas  inmutables.  Si  solo  necesita  una  matriz  fija  de  caracteres  fijos,  
entonces  std::string  ofrece  poca  ventaja.  Por  ejemplo,  puede  definir  una  constante  de  cadena  de  este  tipo  de  esta  manera:

const  char*  const  EDITOR  =  "Apress  Media  LLC";

En  este  caso,  no  se  puede  cambiar  el  valor  al  que  se  apunta,  ni  se  puede  modificar  el  puntero  en  sí.
(ver  también  la  sección  sobre  Corrección  de  constantes).
Otra  razón  para  trabajar  con  cadenas  C  es  la  compatibilidad  con  las  bibliotecas  de  API  de  estilo  C,  respectivamente.  Muchas  
bibliotecas  de  terceros  suelen  tener  interfaces  de  bajo  nivel  para  garantizar  la  compatibilidad  con  versiones  anteriores  y  mantener  su  área  de  
uso  lo  más  amplia  posible.  Las  cadenas  a  menudo  se  esperan  como  cadenas  de  estilo  C  en  dicha  API.  Sin  embargo,  incluso  en  este  
caso,  el  uso  de  cadenas  de  estilo  C  debe  limitarse  localmente  al  manejo  de  esta  interfaz.  Lejos  del  intercambio  de  datos  con  una  API  de  
este  tipo,  las  cadenas  C++  mucho  más  cómodas  deben  usarse  siempre  que  sea  posible.

Evite  el  uso  de  printf(),  sprintf(),  gets(),  etc.  printf(),  que  forma  parte  de  la  biblioteca  

C  para  realizar  operaciones  de  entrada/salida  (definidas  en  el  encabezado  <cstdio>),  imprime  datos  formateados  en  la  salida  estándar  
(stdout ).  Algunos  desarrolladores  todavía  usan  muchos  printfs  con  fines  de  seguimiento/registro  en  su  código  C++.  A  menudo  
argumentan  que  printf  es...  no...  debe  ser  mucho  más  rápido  que  C++  I/O­Streams,  ya  que  falta  toda  la  sobrecarga  de  C++.

Primero,  la  E/S  es  un  cuello  de  botella  de  todos  modos,  sin  importar  si  está  usando  printf()  o  std::cout.  para  escribir  cualquier  cosa  en
La  salida  estándar  es  generalmente  lenta,  con  magnitudes  más  lentas  que  la  mayoría  de  las  otras  operaciones  en  un  programa.
Bajo  ciertas  circunstancias,  std::cout  puede  ser  un  poco  más  lento  que  printf(),  pero  en  relación  con  el  costo  general  de  una  operación  de  E/
S,  esos  pocos  microsegundos  suelen  ser  insignificantes.  En  este  punto  también  me  gustaría  recordarles  a  todos  que  tengan  cuidado  
con  las  optimizaciones  (prematuras)  (recuerden  la  sección  “Cuidado  con  las  optimizaciones”  en  el  Capítulo  3).

En  segundo  lugar,  printf()  es  fundamentalmente  de  tipos  inseguros  y,  por  lo  tanto,  propenso  a  errores.  La  función  espera  una  
secuencia  de  argumentos  sin  tipo  relacionados  con  una  cadena  C  llena  de  especificadores  de  formato,  que  es  el  primer  argumento.  Las  
funciones  que  no  se  pueden  usar  de  manera  segura  nunca  deben  usarse,  ya  que  esto  puede  generar  errores  sutiles,  comportamiento  
indefinido  (consulte  la  sección  sobre  Comportamiento  indefinido  en  el  Capítulo  5)  y  vulnerabilidades  de  seguridad.

74
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

estándar::to_String()  [C++11]

No  utilice  la  función  de  C  sprintf()  (encabezado  <cstdio>)  con  fines  de  conversión  en  un  programa  C++  
moderno.  Desde  C++11,  todas  las  variables  de  tipo  numérico  se  pueden  convertir  fácilmente  a  una  
cadena  de  C++  utilizando  la  función  segura  y  conveniente  std::to_string()  respectivamente  std::to_wstring() ,  
definida  en  el  encabezado  <string>.  Por  ejemplo,  un  entero  con  signo  se  puede  convertir  en  un  std::string  
que  contiene  una  representación  textual  del  valor  de  la  siguiente  manera:

valor  int  { 42 };  
std::string  valueAsString  =  std::to_string(value);

std::to_string()  respectivamente  std::to_wstring()  está  disponible  para  todos  los  tipos  integrales  o  de  punto  
flotante,  como  int,  long,  long  long,  unsigned  int,  float,  double,  etc.  Pero  uno  de  los  principales  inconvenientes  de  
este  simple  asistente  de  conversión  es  su  inexactitud  en  ciertos  casos.

doble  d  { 1e­9 };  
std::cout  <<  std::to_string(d)  <<  "\n"; //  ¡Precaución!  Salida:  0.000000

Además,  no  hay  capacidades  de  configuración  para  controlar  cómo  to_string()  da  formato  a  la  cadena  de  
salida,  por  ejemplo,  la  cantidad  de  lugares  decimales.  Eso  significa  que  esta  función  solo  se  puede  usar  
de  facto  en  una  medida  menor  en  un  programa  real.  Si  necesita  una  conversión  más  precisa  y  personalizada,  
debe  proporcionarla  usted  mismo.  En  lugar  de  usar  sprintf(),  puede  aprovechar  los  flujos  de  cadena  
(encabezado  <sstream>)  y  las  capacidades  de  configuración  de  los  manipuladores  de  E/S  definidos  en  
el  encabezado  <iomanip>,  como  en  el  siguiente  ejemplo:

#include  <iomanip>  
#include  <sstream>  
#include  <cadena>

std::string  convertDoubleToString(const  long  double  valueToConvert,  const  int  precision)  
{ std::stringstream  
stream  { };  stream  <<  std::fixed  <<  
std::setprecision(precision)  <<  valueToConvert;  volver  corriente.str(); }

En  tercer  lugar,  a  diferencia  de  printf,  los  flujos  de  E/S  de  C++  permiten  que  los  objetos  complejos  se  transmitan  fácilmente  proporcionando  
un  operador  de  inserción  personalizado  (operador<<).  Supongamos  que  tenemos  una  factura  de  clase  (definida  en  un  archivo  de  encabezado  
llamado  Factura.h)  que  se  parece  a  esto:

Listado  4­29.  Un  extracto  del  archivo  Invoice.h  con  números  de  línea

01  #ifndef  INVOICE_H_  02  
#define  INVOICE_H_  03  04  

#include  <crono>  05  
#include  <memoria>  06  
#include  <ostream>

75
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

07  #include  <string>  08  
#include  <vector>  09  10  

#include  "Customer.h"  11  #include  
"InvoiceLineItem.h"  12  #include  "Money.h"  
13  #include  "UniqueIdentifier.h"  
14

15  usando  InvoiceLineItemPtr  =  std::shared_ptr<InvoiceLineItem>;  16  usando  
InvoiceLineItems  =  std::vector<InvoiceLineItemPtr>;  17

18  usando  InvoiceRecipient  =  Cliente;  19  usando  
InvoiceRecipientPtr  =  std::shared_ptr<InvoiceRecipient>;  20

21  usando  DateTime  =  std::chrono::system_clock::time_point;  22  23  clase  

Factura  { 24  public:  
explícito  
Factura(const  UniqueIdentifier&  númerofactura);  25  Factura()  =  borrar;  26  27  
void  setRecipient(const  
InvoiceRecipientPtr&  recipiente);  28  void  setDateTimeOfInvoicing(const  DateTime&  
dateTimeOfInvoicing);  Dinero  getSum()  const;  29  Dinero  getSumWithoutTax()  const;  30  void  
addLineItem(const  
InvoiceLineItemPtr&  lineItem);  31 // ...posiblemente  
más  funciones  miembro  aquí...  32  33  34  private:  amigo  std::ostream&  
operator<<(std::ostream&  outstream,  const  Invoice&  factura);  35  

std::string  
getDateTimeOfInvoicingAsString()  const;  36  37  38  39  40

UniqueIdentifier  número  de  factura;  
DateTime  dateTimeOfInvoicing;  
destinatario  FacturaRecipientPtr;  
Elementos  de  línea  de  factura  elementos  de  
línea  
de  factura;  41  42 };  43 //...

La  clase  tiene  dependencias  con  un  destinatario  de  la  factura  (que  en  este  caso  es  un  alias  para  el  Cliente  
definido  en  el  encabezado  Cliente.h;  consulte  la  línea  n.º  18)  y  utiliza  un  identificador  (tipo  UniqueIdentifier)  que  representa  un  
número  de  factura  que  se  garantiza  que  será  único  entre  todos  los  números  de  factura.  Además,  la  factura  utiliza  un  tipo  
de  datos  que  puede  representar  montos  de  dinero  (consulte  también  la  sección  "Clase  de  dinero"  en  el  Capítulo  9  sobre  
patrones  de  diseño),  así  como  una  dependencia  a  otro  tipo  de  datos  que  representa  un  único  elemento  de  línea  de  factura.
Este  último  se  utiliza  para  administrar  una  lista  de  artículos  de  factura  dentro  de  la  factura  usando  un  std::vector  (ver  línea  
n.  °  16  respectivamente  41).  Y  para  representar  el  momento  de  la  facturación,  usamos  el  tipo  de  datos  time_point  de  la  
biblioteca  Chrono  (definida  en  el  encabezado  <chrono>),  que  está  disponible  desde  C++11.
Ahora  imaginemos  además  que  queremos  transmitir  la  factura  completa  con  todos  sus  datos  a  la  salida  estándar.
¿No  sería  bastante  simple  y  conveniente  si  pudiéramos  escribir  algo  como...

std::cout  <<  instanciaDeFactura;

76
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Bueno,  eso  es  posible  con  C++.  El  operador  de  inserción  (<<)  para  flujos  de  salida  se  puede  sobrecargar  para  cualquier  clase.  
Solo  tenemos  que  agregar  una  función  operator<<  a  nuestra  declaración  de  clase  en  el  encabezado.  Es  importante  hacer  de  esta  
función  un  amigo  de  la  clase  (ver  línea  n.°  35)  porque  se  llamaría  sin  crear  un  objeto.

Listado  4­30.  El  operador  de  inserción  para  la  clase  Factura

43 // ...  44  
std::ostream&  operator<<(std::ostream&  outstream,  const  Factura&  factura)  { <<  factura.númeroFactura  <<  "\n";  45  46
"
outstream  <<  "Factura  No.:
"Destinatario:  "  <<   <<  *(factura.destinatario)  <<  "\n";  outstream  <<  
47   factura.getDateTimeOfInvoicingAsString()  <<  "\n";  salida  <<  "Fecha/hora:  "
48 outstream  <<  "Elementos:"  <<  "\n";  for  (const  
49 auto&  item :  factura.invoiceLineItems)  { <<  *item  <<  "\n";
" "
50 aguas  afuera  <<
51
52 }  outstream  <<  "Importe  facturado: " <<  factura.getSum()  <<  std::endl;
53 retorno  aguas  abajo;  54 }  
55 // ...

Todos  los  componentes  estructurales  de  la  clase  Factura  se  escriben  en  un  flujo  de  salida  dentro  de  la  función.
Esto  es  posible  porque  también  las  clases  UniqueIdentifier,  InvoiceRecipient  y  InvoiceLineItem  tienen  sus  propias  funciones  de  operador  
de  inserción  (que  no  se  muestran  aquí)  para  los  flujos  de  salida.  Para  imprimir  todos  los  elementos  de  línea  en  el  vector,  se  utiliza  un  
ciclo  for  basado  en  rangos  de  C++11.  Y  para  obtener  una  representación  textual  de  la  fecha  de  facturación,  usamos  un  método  de  ayuda  
interno  llamado  getDateTimeOfInvoicingAsString()  que  devuelve  una  cadena  de  fecha/hora  bien  formateada.

Entonces,  mi  consejo  para  un  programa  C++  moderno  es  este:

Evite  usar  printf(),  y  también  otras  funciones  C  inseguras,  como  sprintf(),  puts(),  etc.

Prefiere  los  contenedores  de  biblioteca  estándar  a  las  matrices  simples  de  estilo  C  En  lugar  de  usar  matrices  de  estilo  C,  

debe  usar  la  plantilla  std::array<TYPE,  N>  que  está  disponible  desde  C++11  (header  <array>).  Las  instancias  de  std::array<TYPE,  
N>  son  contenedores  de  secuencias  de  tamaño  fijo  y  son  tan  eficientes  como  las  matrices  ordinarias  de  estilo  C.

Los  problemas  con  las  matrices  de  estilo  C  son  más  o  menos  los  mismos  que  con  las  cadenas  de  estilo  C  (consulte  la  sección  anterior).
Las  matrices  C  son  malas,  porque  se  transmiten  como  un  puntero  sin  formato  a  su  primer  elemento.  Esto  podría  ser  
potencialmente  peligroso,  porque  no  hay  comprobaciones  vinculadas  que  protejan  a  los  usuarios  de  esa  matriz  para  acceder  a  
elementos  inexistentes.  Las  matrices  creadas  con  std::array  son  más  seguras  porque  no  se  degradan  a  punteros  (consulte  también  la  
sección  "Estrategias  para  evitar  punteros  regulares",  anteriormente  en  este  capítulo).

77
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Una  ventaja  de  usar  std::array  es  que  conoce  su  tamaño  (número  de  elementos).  Cuando  se  trabaja  con  arreglos,  
el  tamaño  de  ese  arreglo  es  información  importante  que  a  menudo  se  requiere.  Las  matrices  ordinarias  de  estilo  C  no  
conocen  su  propio  tamaño.  Por  lo  tanto,  el  tamaño  de  la  matriz  a  menudo  debe  manejarse  como  una  información  
adicional,  por  ejemplo,  en  una  variable  adicional.  Por  ejemplo,  el  tamaño  debe  pasarse  como  un  argumento  adicional  
a  las  llamadas  a  funciones  como  en  el  siguiente  ejemplo.

const  std::size_t  arraySize  =  10;
MiArrayType  array[arraySize];

void  function(MyArrayType  const*  array,  const  std::size_t  arraySize)  {
// ...
}

Estrictamente  hablando,  en  este  caso  la  matriz  y  su  tamaño  no  forman  una  unidad  cohesiva  (ver  la  
sección  sobre  Cohesión  Fuerte  en  el  Capítulo  3).  Además,  ya  sabemos  por  una  sección  anterior  sobre  argumentos  y  
valores  devueltos  que  el  número  de  argumentos  de  la  función  debe  ser  lo  más  pequeño  posible.
Por  el  contrario,  las  instancias  de  std::array  llevan  su  tamaño  y  se  puede  consultar  cualquier  instancia  al  respecto.  Por  lo  tanto,  la
Las  listas  de  argumentos  de  funciones  o  métodos  no  requieren  parámetros  adicionales  sobre  el  tamaño  de  la  matriz:

#incluye  <matriz>

usando  MyTypeArray  =  std::array<MyType,  10>;

función  vacía  (const  MyTypeArray&  array)  {
const  std::size_t  arraySize  =  array.size(); //...

Otra  ventaja  notable  de  std::array  es  que  tiene  una  interfaz  compatible  con  STL.  La  plantilla  de  clase  
proporciona  funciones  de  miembros  públicos  para  que  se  vea  como  cualquier  otro  contenedor  en  la  Biblioteca  estándar.
Por  ejemplo,  los  usuarios  de  una  matriz  pueden  obtener  un  iterador  que  apunte  al  comienzo  y  al  final  de  la  secuencia  
usando  std::array::begin()  respectivamente  std::array::end().  Esto  también  significa  que  los  algoritmos  del  
encabezado  <algoritmo>  se  pueden  aplicar  a  la  matriz  (consulte  también  la  sección  sobre  algoritmos  en  el  siguiente  capítulo).

#incluye  <matriz>  
#incluye  <algoritmo>

usando  MyTypeArray  =  std::array<MyType,  10>;
matriz  MiTipoArray;

void  hacerAlgoConCadaElemento(const  MiTipo&elemento)  {
// ...
}

std::for_each(std::cbegin(arreglo),  std::cend(arreglo),  hacerAlgoConCadaElemento);

78
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

NO  MIEMBRO  std::begin( )  Y  std::end( )  [C++11/14]

Cada  contenedor  de  la  biblioteca  estándar  de  C++  tiene  una  función  miembro  begin()  respectivamente  cbegin()  y  end()  
respectivamente  cend()  para  recuperar  iteradores  respectivamente  const­iteradores  para  ese  contenedor.

C++11  ha  introducido  funciones  gratuitas  para  no  miembros  con  ese  fin:  std::begin(<container>)  y  
std::end(<container>).  Con  C++14,  las  funciones  aún  faltantes  std::cbegin(<container>),  
std::cend(<container>),  std::rbegin(<container>)  y  std::rend(<container>)  tienen  sido  agregado  En  lugar  
de  usar  las  funciones  miembro,  ahora  se  recomienda  usar  estas  funciones  que  no  son  miembros  
(todas  definidas  en  el  encabezado  <iterador>)  para  obtener  iteradores  respectivamente  const­iteradores  
para  un  contenedor,  como  este:

#incluir  <vector>

std::vector<CualquierTipo>  aVector;  
iteración  automática  =  std::begin(aVector); // ...en  lugar  de  'auto  iter  =  aVector.begin();'

La  razón  es  que  esas  funciones  libres  permiten  un  estilo  de  programación  más  flexible  y  genérico.  Por  ejemplo,  
muchos  contenedores  definidos  por  el  usuario  no  tienen  una  función  de  miembro  begin()  y  end() ,  lo  que  los  hace  
imposibles  de  usar  con  los  algoritmos  de  la  biblioteca  estándar  (consulte  la  sección  sobre  algoritmos  en  el  Capítulo  
5)  o  cualquier  otra  plantilla  definida  por  el  usuario.  función  que  requiere  iteradores.  Las  funciones  que  no  son  
miembros  para  recuperar  iteradores  son  extensibles  en  el  sentido  de  que  pueden  sobrecargarse  para  cualquier  tipo  de  
secuencia,  incluidas  las  antiguas  matrices  de  estilo  C.  En  otras  palabras:  los  contenedores  no  compatibles  con  STL  
(personalizados)  se  pueden  adaptar  con  capacidades  de  iterador.

Por  ejemplo,  suponga  que  tiene  que  lidiar  con  una  matriz  de  enteros  de  estilo  C,  como  esta:

int  fibonacci[]  =  { 1,  1,  2,  3,  5,  8,  13,  21,  34,  55,  89,  144 };

Este  tipo  de  matriz  ahora  se  puede  adaptar  con  una  interfaz  de  iterador  compatible  con  la  biblioteca  estándar.
Para  las  matrices  de  estilo  C,  estas  funciones  ya  se  proporcionan  en  la  Biblioteca  estándar,  por  lo  que  no  es  necesario  
que  las  programe  usted  mismo.  Se  ven  más  o  menos  así:

template  <typename  Tipo,  std::size_t  size>
Tipo*  begin(Tipo  (&elemento)[tamaño])  
{ return  &elemento[0];
}

template  <typename  Tipo,  std::size_t  size>
Tipo*  end(Tipo  (&elemento)[tamaño])  
{ return  &elemento[0]  +  tamaño;
}

79
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Para  insertar  todos  los  elementos  de  la  matriz  en  un  flujo  de  salida,  por  ejemplo,  para  imprimirlos  en  la  salida  estándar,  ahora  
podemos  escribir:

int  main()  { for  
(auto  it  =  begin(fibonacci);  it !=  end(fibonacci);  ++it)  { std::cout  <<  *it  <<  ",  "; }  estándar::cout  
<<  estándar::endl;  devolver  0; }

Proporcionar  funciones  begin()  y  end()  sobrecargadas  para  tipos  de  contenedores  personalizados  o  matrices  antiguas  de  
estilo  C  permite  la  aplicación  de  todos  los  algoritmos  de  la  biblioteca  estándar  a  estos  tipos.

Además,  std::array  puede  acceder  a  elementos  que  incluyen  cheques  enlazados  con  la  ayuda  de  la  función  miembro  
std::array::at(size_type  n).  Si  el  índice  dado  está  fuera  de  los  límites,  se  lanza  una  excepción  de  tipo  std::out_  of_bounds.

Usar  moldes  de  C++  en  lugar  de  moldes  antiguos  de  estilo  C
Antes  de  que  surja  una  falsa  impresión,  primero  me  gustaría  hacer  una  advertencia  importante:

■  Advertencia  ¡Los  moldes  tipográficos  son  básicamente  malos  y  deben  evitarse  siempre  que  sea  posible!  Son  una  indicación  confiable  

de  que  debe  haber,  aunque  solo  sea  un  problema  de  diseño  relativamente  pequeño.

Sin  embargo,  si  no  se  puede  evitar  una  conversión  de  tipo  en  una  situación  determinada,  bajo  ninguna  circunstancia  debe  
usar  una  conversión  de  estilo  C:

doble  d  { 3.1415 };  int  i  =  
(int)d;

En  este  caso,  el  doble  se  degrada  a  un  número  entero.  Esta  conversión  explícita  va  acompañada  de  una  pérdida  de
precisión  ya  que  los  lugares  decimales  del  número  de  punto  flotante  se  desechan.  La  conversión  explícita  con  el  molde  de  estilo  C  
dice  algo  como  esto:  "El  programador  que  escribió  esta  línea  de  código  estaba  al  tanto  de  las  consecuencias".

Bueno,  esto  es  ciertamente  mejor  que  una  conversión  de  tipo  implícita.  Sin  embargo,  en  lugar  de  usar  el  antiguo  estilo  C
conversiones,  debe  usar  conversiones  de  C++  para  conversiones  de  tipo  explícitas,  como  esta:

int  i  =  static_cast<int>(d);

80
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

La  explicación  simple  para  este  consejo  es:  ¡el  compilador  verifica  las  conversiones  de  estilo  C  ++  durante  el  tiempo  de  compilación!  Las  
conversiones  de  estilo  C  no  se  verifican  de  esta  manera  y,  por  lo  tanto,  pueden  fallar  en  el  tiempo  de  ejecución,  lo  que  puede  causar  errores  
desagradables  o  fallas  en  la  aplicación.  Por  ejemplo,  una  conversión  de  estilo  C  usada  de  manera  imprevista  puede  causar  una  pila  corrupta,  
como  en  el  siguiente  caso.

int32_t  i  { 200 }; //  Reserva  y  usa  memoria  de  4  bytes  int64_t*  
pointerToI  =  (int64_t*)&i; //  El  puntero  apunta  a  8  bytes

*punteroToI  =  9223372036854775807; //  Puede  causar  un  error  en  tiempo  de  ejecución  a  través  de  la  corrupción  de  la  pila

Obviamente,  en  este  caso  es  posible  escribir  un  valor  de  64  bits  en  un  área  de  memoria  que  solo  tiene  un  tamaño  de  32  bits.
El  problema  es  que  el  compilador  no  puede  llamar  nuestra  atención  sobre  este  código  potencialmente  peligroso.  El  compilador  
traduce  este  código,  incluso  con  configuraciones  muy  conservadoras  (g++  ­std=c++17  ­pedantic  ­pedantic  errors  ­Wall  ­Wextra  
­Werror  ­Wconversion),  sin  quejas.  Esto  puede  conducir  a  errores  muy  insidiosos  durante  la  ejecución  del  programa.

Ahora  veamos  qué  sucederá  si  usamos  un  static_cast  de  C++  en  la  segunda  línea  en  lugar  del  viejo  y  malo
Reparto  estilo  C:

int64_t*  pointerToI  =  static_cast<int64_t*>(&i); //  El  puntero  apunta  a  8  bytes

El  compilador  ahora  puede  detectar  la  conversión  problemática  e  informa  el  mensaje  de  error  correspondiente:

error:  static_cast  no  válido  del  tipo  'int32_t*  {aka  int*}'  para  escribir  'int64_t*  {aka  long  int*}'

Otra  razón  por  la  que  debe  usar  conversiones  de  C++  en  lugar  de  las  antiguas  conversiones  de  estilo  C  es  que  las  
conversiones  de  estilo  C  son  muy  difíciles  de  detectar  en  un  programa.  Ni  pueden  ser  descubiertos  fácilmente  por  el  desarrollador,  
ni  pueden  ser  buscados  convenientemente  utilizando  un  editor  o  procesador  de  textos  común.  Por  el  contrario,  es  muy  fácil  
buscar  términos  como  static_cast<>,  const_cast<>  o  dynamic_cast<>.
De  un  vistazo,  aquí  están  todos  los  consejos  sobre  conversiones  de  tipos  para  un  programa  C++  moderno  y  bien  
diseñado:

1.  Trate  de  evitar  las  conversiones  de  tipos  (casts)  en  todas  las  circunstancias.  En  su  lugar,  intente  
eliminar  el  error  de  diseño  subyacente  que  lo  obliga  a  utilizar  la  conversión.

2. Si  no  se  puede  evitar  una  conversión  de  tipo  explícita,  utilice  únicamente  conversiones  de  
estilo  C++  (static_cast<>  o  const_cast<>),  ya  que  el  compilador  comprueba  estas  
conversiones.  Nunca  use  moldes  de  estilo  C  viejos  y  malos.

3.  Tenga  en  cuenta  que  dynamic_cast<>  nunca  debe  usarse  porque  se  considera  un  mal  diseño.  La  
necesidad  de  un  dynamic_cast<>  es  una  indicación  confiable  de  que  algo  anda  mal  dentro  de  una  
jerarquía  de  especialización  (este  tema  se  profundizará  en  el  Capítulo  6  sobre  la  Orientación  a  
Objetos).

4.  Bajo  ninguna  circunstancia,  nunca  use  reinterpret_cast<>.  Este  tipo  de  tipo
La  conversión  marca  una  conversión  insegura,  no  portátil  y  dependiente  de  la  implementación.
Su  nombre  largo  e  inconveniente  es  una  pista  general  para  hacerte  pensar  en  lo  que  estás  
haciendo  actualmente.

81
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

Evite  las  macros
Quizás  uno  de  los  legados  más  severos  del  lenguaje  C  son  las  macros.  Una  macro  es  una  pieza  de  código  que  se  puede  
identificar  por  un  nombre.  Si  el  llamado  preprocesador  encuentra  el  nombre  de  una  macro  en  el  código  fuente  del  programa  durante  la  
compilación,  el  nombre  se  reemplaza  por  su  fragmento  de  código  relacionado.
Un  tipo  de  macros  son  las  macros  de  tipo  objeto  que  a  menudo  se  usan  para  dar  nombres  simbólicos  a  números.
constantes,  como  en  el  siguiente  ejemplo.

Listado  4­31.  Dos  ejemplos  de  macros  similares  a  objetos

#define  BUFFER_SIZE  1024  #define  
PI  3.14159265358979

Otros  ejemplos  típicos  de  macros  son  los  siguientes:

Listado  4­32.  Dos  ejemplos  de  macros  similares  a  funciones

#define  MIN(a,b)  (((a)<(b))?(a):(b))  #define  MAX(a,b)  
(((a)>(b))?(a):  (b))

MÍN.  resp.  MAX  compara  dos  valores  y  devuelve  el  más  pequeño  respectivamente  el  más  grande.  Este  tipo  de  macros  
se  denominan  macros  similares  a  funciones.  Aunque  estas  macros  parecen  casi  funciones,  no  lo  son.  El  preprocesador  C  realiza  
simplemente  una  sustitución  del  nombre  por  el  fragmento  de  código  relacionado  (de  hecho,  es  una  operación  textual  de  buscar  y  
reemplazar).
Las  macros  son  potencialmente  peligrosas.  A  menudo  no  se  comportan  como  se  esperaba  y  pueden  tener  efectos  secundarios  no  deseados.
efectos  Por  ejemplo,  supongamos  que  ha  definido  una  macro  como  esta:

#define  PELIGROSO  1024+1024

Y  en  algún  lugar  de  tu  código  escribes  esto:

valor  int  =  PELIGROSO  *  2;

Probablemente  alguien  esperaba  que  el  valor  de  la  variable  contuviera  4096,  pero  en  realidad  sería  3072.
Recuerde  el  orden  de  las  operaciones  matemáticas  que  nos  dice  que  la  división  y  la  multiplicación,  de  izquierda  a  derecha,  deben  ocurrir  
primero.
Otro  ejemplo  de  efectos  secundarios  inesperados  debido  al  uso  de  una  macro  es  el  uso  de  'MAX'  en  el  siguiente
forma:

int  máximo  =  MAX(12,  valor++);

El  preprocesador  generará  lo  siguiente:

int  máximo  =  (((12)>(valor++))?(12):(valor++));

Como  se  puede  ver  fácilmente  ahora,  la  operación  posterior  al  incremento  en  el  valor  se  realizará  dos  veces.  Esto  era
ciertamente  no  era  la  intención  del  desarrollador  que  había  escrito  el  código  anterior.
¡No  uses  más  macros!  Al  menos  desde  C++  11,  están  casi  obsoletos.  Con  algunos  muy  raros
excepciones,  las  macros  simplemente  ya  no  son  necesarias  y  ya  no  deberían  usarse  en  un  programa  C++  moderno.
Tal  vez  la  introducción  del  llamado  Reflection  (es  decir,  la  capacidad  de  que  el  programa  pueda  examinar,  introspeccionar  y  modificar  
su  propia  estructura  y  comportamiento  en  tiempo  de  ejecución)  como  posible  parte  de  un  futuro  estándar  de  C++  pueda  ayudar  a  
deshacerse  de  las  macros  por  completo.  Pero  hasta  que  llegue  el  momento,  las  macros  todavía  se  necesitan  actualmente  para  algunos  
propósitos  especiales,  por  ejemplo,  cuando  se  usa  un  marco  de  prueba  de  unidad  o  registro.

82
Machine Translated by Google

Capítulo  4  ■  Conceptos  básicos  de  Clean  C++

En  lugar  de  macros  similares  a  objetos,  use  expresiones  constantes  para  definir  constantes:

constexpr  int  INOFENSIVO  =  1024  +  1024;

Y  en  lugar  de  macros  similares  a  funciones,  simplemente  use  funciones  verdaderas,  por  ejemplo,  las  plantillas  de  
funciones  std::min  o  std::max  que  se  definen  en  el  encabezado  <algoritmo>  (consulte  también  la  sección  sobre  el  encabezado  
<algoritmo>  en  el  siguiente  capítulo):

#incluir  <algoritmo> // ...  int  
máximo  
=  std::max(12,  value++);

83
Machine Translated by Google

CAPÍTULO  5

Conceptos  avanzados  de  C++  moderno

En  los  capítulos  3  y  4  discutimos  los  principios  y  prácticas  básicos  que  construyen  una  base  sólida  para  un  código  C++  limpio  y  
moderno.  Con  estos  principios  y  reglas  en  mente,  un  desarrollador  puede  aumentar  significativamente  la  calidad  del  código  
C++  interno  de  un  proyecto  de  software  y,  por  lo  tanto,  a  menudo,  su  calidad  externa.  El  código  se  vuelve  más  comprensible,  más  
fácil  de  mantener,  más  fácil  de  extender,  menos  susceptible  a  errores,  y  esto  conduce  a  una  vida  mejor  para  cualquier  creador  de  
software,  porque  es  más  divertido  trabajar  con  una  base  de  código  sólida  como  esa.  Y  en  el  Capítulo  2  también  aprendimos  
que,  sobre  todo,  un  conjunto  bien  mantenido  de  Pruebas  unitarias  bien  diseñadas  puede  mejorar  aún  más  la  calidad  del  
software,  así  como  la  eficiencia  del  desarrollo.
Pero,  ¿podemos  hacerlo  mejor?  Por  supuesto  que  podemos.

Como  ya  he  explicado  en  la  introducción  de  este  libro,  el  viejo  dinosaurio  C++  ha  experimentado  algunas  mejoras  
considerables  durante  los  últimos  años.  El  lenguaje  estándar  C++11  (abreviatura  de  ISO/IEC  14882:2011),  pero  también  los  
siguientes  estándares  C++14  (que  era  solo  una  pequeña  extensión  de  C++11)  y  la  versión  más  reciente  C++17  ( que  llegó  al  
proceso  de  votación  final  de  ISO  en  junio  de  2017),  han  creado  una  herramienta  de  desarrollo  moderna,  flexible  y  eficiente  a  
partir  del  lenguaje  de  programación  ya  algo  polvoriento.
Algunos  de  los  nuevos  conceptos  introducidos  a  través  de  estos  estándares,  como  la  semántica  de  movimientos,  son  prácticamente  un  
cambio  de  paradigma.
Ya  he  usado  algunas  de  las  características  de  estos  estándares  de  C++  en  los  capítulos  anteriores  y  la  mayor  parte  de  
las  expliqué  en  las  barras  laterales.  Ahora  es  el  momento  de  profundizar  en  algunos  de  ellos  y  explorar  cómo  pueden  ayudarnos  
a  escribir  código  C++  excepcionalmente  sólido  y  moderno.  Por  supuesto,  no  es  posible  discutir  aquí  todas  las  características  
del  lenguaje  de  los  nuevos  estándares  C++.  Eso  iría  mucho  más  allá  del  alcance  de  este  libro,  dejando  de  lado  el  hecho  de  
que  esto  está  cubierto  por  muchos  otros  libros.  Por  lo  tanto,  he  seleccionado  algunos  temas  que  creo  que  respaldan  muy  bien  
la  escritura  de  código  C++  limpio.

Gestión  de  recursos
La  gestión  de  recursos  es  un  negocio  básico  para  los  desarrolladores  de  software.  Una  multitud  de  recursos  misceláneos  deben  
asignarse,  usarse  y  devolverse  regularmente  después  de  su  uso.  Estos  incluyen  lo  siguiente:

•Memoria  (ya  sea  en  la  pila  o  en  el  montón);
• Identificadores  de  archivos  necesarios  para  acceder  a  los  archivos  (lectura/escritura)  en  el  disco  
duro  u  otros  medios;

•Conexiones  de  red  (por  ejemplo,  a  un  servidor,  una  base  de  datos,  etc.);

•Hilos,  bloqueos,  temporizadores  y  transacciones;

•Otros  recursos  del  sistema  operativo,  como  identificadores  GDI  en  sistemas  operativos  Windows.  
(La  abreviatura  GDI  significa  Interfaz  de  dispositivo  gráfico.  GDI  es  un  componente  central  del  
sistema  operativo  de  Microsoft  Windows  y  es  responsable  de  representar  objetos  gráficos).

©  Stephan  Roth  2017   85
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0_5
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

El  manejo  adecuado  de  los  recursos  puede  ser  una  tarea  complicada.  Considere  el  siguiente  ejemplo:

Listado  5­1.  Tratar  con  un  recurso  que  se  asignó  en  el  montón

void  hacerAlgo()  {
ResourceType*  recurso  =  new  ResourceType();  intente  

{ // ...haga  algo  con  el  recurso...  recurso­>foo(); }  
catch  (...)  { eliminar  
recurso;  tirar; }  
eliminar  recurso; }

¿Cuál  es  el  problema  aquí?  Tal  vez  haya  notado  las  dos  declaraciones  de  eliminación  idénticas.  el  catch­all
El  mecanismo  de  manejo  de  excepciones  introduce  al  menos  dos  caminos  posibles  en  nuestro  programa.  Esto  también  
significa  que  tenemos  que  asegurarnos  de  que  el  recurso  se  libere  en  dos  lugares.  En  circunstancias  normales,  estos  
manejadores  de  excepciones  catch­all  están  mal  vistos.  Pero  en  este  caso,  no  tenemos  otra  oportunidad  que  atrapar  todas  
las  posibles  excepciones  que  ocurran  aquí  solo  porque  primero  debemos  liberar  el  recurso,  antes  de  lanzar  el  objeto  de  
excepción  más  lejos  para  tratarlo  en  otro  lugar  (por  ejemplo,  en  el  sitio  de  llamada  de  la  función).
Y  en  este  ejemplo  simplificado  solo  tenemos  dos  caminos.  En  programas  reales,  pueden  existir  muchas  más  rutas  de  
ejecución.  La  probabilidad  de  que  se  olvide  una  eliminación  es  mucho  mayor.  Y  cualquier  eliminación  olvidada  resultará  en  
una  peligrosa  fuga  de  recursos.

■  Advertencia  ¡No  subestime  las  fugas  de  recursos!  Las  fugas  de  recursos  son  un  problema  grave,  especialmente  para  los  

procesos  de  larga  duración  y  para  los  procesos  que  asignan  rápidamente  muchos  recursos  sin  desasignarlos  después  del  uso.  Si  

un  sistema  operativo  tiene  una  falta  de  recursos,  esto  puede  conducir  a  estados  críticos  del  sistema.  Además,  las  fugas  de  
recursos  pueden  ser  un  problema  de  seguridad,  ya  que  los  atacantes  pueden  aprovecharlas  para  realizar  ataques  de  
denegación  de  servicio.

La  solución  más  simple  para  nuestro  pequeño  ejemplo  anterior  podría  ser  que  asignemos  el  recurso  en  la  pila,
en  lugar  de  asignarlo  en  el  montón:

Listado  5­2.  Mucho  más  fácil:  Tratar  con  un  recurso  en  la  pila

void  hacerAlgo()  {
recurso  ResourceType;

// ...hacer  algo  con  el  recurso...  resource.foo();

Con  este  cambio,  el  recurso  se  elimina  de  forma  segura  en  cualquier  caso.  Pero  a  veces  no  es  posible  asignar  todo  en  la  
pila,  como  ya  hemos  discutido  en  la  sección  "No  pase  o  devuelva  0  (NULL,  nullptr)"  en  el  Capítulo  4.  ¿Qué  pasa  con  los  
identificadores  de  archivos,  los  recursos  del  sistema  operativo,  etc. ? ?
La  pregunta  central  es  esta:  ¿ Cómo  podemos  garantizar  que  los  recursos  asignados  estén  siempre  libres?

86
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

La  adquisición  de  recursos  es  inicialización  (RAII)
La  adquisición  de  recursos  es  inicialización  (RAII)  es  una  expresión  idiomática  (consulte  el  Capítulo  9  sobre  modismos)  que  pueden  
ayudar  a  hacer  frente  a  los  recursos  de  una  manera  segura.  El  idioma  también  se  conoce  como  Constructor  Acquires,  Destructor  
Releases  (CADRe)  y  Scope­based  Resource  Management  (SBRM).
RAII  aprovecha  la  simetría  de  una  clase  por  su  constructor  y  su  correspondiente  destructor.  Nosotros
podemos  asignar  un  recurso  en  el  constructor  de  una  clase,  y  podemos  desasignarlo  en  el  destructor.  Si  creamos  dicha  clase  como  
una  plantilla,  se  puede  usar  para  diferentes  tipos  de  recursos.

Listado  5­3.  Una  plantilla  de  clase  muy  simple  que  puede  administrar  varios  tipos  de  recursos

template  <typename  RESTYPE>  class  
ScopedResource  final  { public:

ScopedResource()  { ManagedResource  =  new  RESTYPE(); }
~ScopedResource()  { eliminar  el  recurso  administrado; }

RESTYPE*  operador­>()  const  { return  recursoadministrado; }

privado:
RESTYPE*  recurso  gestionado; };

Ahora  podemos  usar  la  plantilla  de  clase  ScopedResource  de  la  siguiente  manera:

Listado  5­4.  Uso  de  ScopedResource  para  administrar  una  instancia  de  ResourceType

#incluye  "AlcanceRecurso.h"  #incluye  
"TipoRecurso.h"

void  hacerAlgo()  {

ScopedResource<ResourceType>  recurso;

intente  
{ // ...haga  algo  con  el  recurso...  recurso­>foo(); }  
atrapar  (...)  { lanzar; } }

Como  puede  verse  fácilmente,  no  es  necesario  crear  ni  eliminar.  Si  el  recurso  se  queda  fuera  del  alcance,  lo  que  puede  suceder
en  varios  puntos  de  este  método,  la  instancia  envuelta  de  tipo  ResourceType  se  elimina  automáticamente  a  través  del  destructor  
de  ScopedResource.
Pero  por  lo  general  no  hay  necesidad  de  reinventar  la  rueda  e  implementar  un  contenedor  de  este  tipo,  que  usted  también  llama  un  
puntero  inteligente.

Punteros  inteligentes
Desde  C++  11,  la  biblioteca  estándar  ofrece  implementaciones  de  puntero  inteligente  diferentes  y  eficientes  para  facilitar  su  uso.
Estos  punteros  se  han  desarrollado  durante  un  largo  período  dentro  del  conocido  proyecto  de  biblioteca  Boost  antes  de  que  se  
introdujeran  en  el  estándar  C++  y  se  pueden  considerar  tan  infalibles  como  sea  posible.  Los  punteros  inteligentes  reducen  la  
probabilidad  de  fugas  de  memoria.  Además,  están  diseñados  para  ser  seguros  para  subprocesos.

87
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Esta  sección  proporciona  una  breve  descripción  general.

Propiedad  única  con  std::unique_ptr<T>
La  plantilla  de  clase  std::unique_ptr<T>  (definida  en  el  encabezado  <memoria>)  administra  un  puntero  a  un  objeto  de  tipo  
T.  Como  sugiere  el  nombre,  este  puntero  inteligente  proporciona  propiedad  única,  es  decir,  un  objeto  puede  ser  propiedad  de  
solo  una  instancia  de  std::unique_ptr<T>  a  la  vez,  que  es  la  principal  diferencia  de  std::shared_ptr<T>,  que  se  explica  a  
continuación.  Esto  también  significa  que  la  construcción  de  copias  y  la  asignación  de  copias  no  están  permitidas.
Su  uso  es  bastante  simple:

#include  <memoria>

clase  Tipo  de  recurso  { //... };

//...  
std::unique_ptr<ResourceType>  resource1  { std::make_unique<ResourceType>() }; // ...  auto  resource2  
o  más  corto  con  tipo  de  deducción...
{ std::make_unique<ResourceType>() };

Después  de  esta  construcción,  el  recurso  se  puede  usar  como  un  puntero  normal  a  una  instancia  de  
ResourceType.  (std::make_unique<T>  se  explica  a  continuación  en  la  sección  "Evitar  nuevos  y  eliminar").  Por  ejemplo,  puede  
usar  el  operador  *  y  ­>  para  desreferenciar:

recurso­>foo();

Por  supuesto,  si  el  recurso  se  queda  fuera  del  alcance,  la  instancia  contenida  de  tipo  ResourceType  se  libera  de  forma  segura.
Pero  la  mejor  parte  es  que  el  recurso  se  puede  poner  fácilmente  en  contenedores,  por  ejemplo,  en  un  std::vector:

#include  "ResourceType.h"  
#include  <memoria>  
#include  <vector>

usando  ResourceTypePtr  =  std::unique_ptr<ResourceType>;  usando  
ResourceVector  =  std::vector<ResourceTypePtr>;

//...

ResourceTypePtr  recurso  { std::make_unique<ResourceType>() };  ResourceVector  
unaColecciónDeRecursos;  
aCollectionOfResources.push_back(std::move(recurso)); //  IMPORTANTE:  
¡En  este  punto,  la  instancia  de  'recurso'  está  vacía!

Tenga  en  cuenta  que  nos  aseguramos  de  que  std::vector::push_back()  llame  al  constructor  de  movimiento  respectivamente  al  
operador  de  asignación  de  movimiento  de  std::unique_ptr<T>  (consulte  la  sección  sobre  semántica  de  movimiento  en  el  próximo  capítulo).
Como  consecuencia,  el  recurso  ya  no  gestiona  un  objeto  y  se  indica  como  vacío.

88
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

■  Precaución  ¡No  utilice  más  std::auto_ptr<T>  en  su  código!  Con  la  publicación  del  estándar  C++11,  std::auto_ptr<T>  se  marcó  

como  "obsoleto"  y  ya  no  se  debe  usar.  ¡Con  el  estándar  C++  17  más  nuevo,  esta  plantilla  de  clase  de  puntero  inteligente  

finalmente  se  eliminó  del  lenguaje!

La  implementación  de  este  puntero  inteligente  no  admitía  referencias  de  valor  real  ni  semántica  de  movimiento  (consulte  la  

sección  sobre  semántica  de  movimiento  más  adelante  en  este  capítulo)  y  no  se  puede  almacenar  dentro  de  los  contenedores  de  la  

biblioteca  estándar.  std::unique_ptr<T>  es  el  reemplazo  apropiado.

Como  ya  se  mencionó,  la  construcción  de  copias  de  std::unique_ptr<T>  no  está  permitida.  Sin  embargo,  la  exclusiva
la  propiedad  del  recurso  administrado  se  puede  transferir  a  otra  instancia  de  std::unique_ptr<T>  usando  la  semántica  de  
movimiento  (hablaremos  de  la  semántica  de  movimiento  en  detalle  en  una  sección  posterior)  de  la  siguiente  manera:

std::unique_ptr<ResourceType>  pointer1  =  std::make_unique<ResourceType>();  
std::unique_ptr<ResourceType>  pointer2; //  pointer2  no  posee  nada  todavía

puntero2  =  std::mover(puntero1); //  Ahora  puntero1  está  vacío,  puntero2  es  el  nuevo  propietario

Propiedad  compartida  con  std::shared_ptr<T>
Las  instancias  de  la  plantilla  de  clase  std::shared_ptr<T>  (definida  en  el  encabezado  <memoria>)  pueden  tomar  posesión  
de  un  recurso  de  tipo  T  y  pueden  compartir  esta  propiedad  con  otras  instancias  de  std::shared_ptr<T>.  En  otras  palabras,  
muchos  propietarios  compartidos  pueden  asumir  la  propiedad  de  una  sola  instancia  de  tipo  T  y,  por  lo  tanto,  la  responsabilidad  
de  su  eliminación .  
std::shared_ptr<T>  proporciona  algo  así  como  una  funcionalidad  simple  de  recolección  de  basura  limitada.  El  inteligente
La  implementación  del  puntero  tiene  un  contador  de  referencia  que  supervisa  cuántas  instancias  de  puntero  que  poseen  el  
objeto  compartido  aún  existen.  Libera  el  recurso  administrado  si  se  destruye  la  última  instancia  del  puntero.
La  Figura  5­1  muestra  un  diagrama  de  objetos  UML  que  representa  una  situación  en  un  sistema  en  ejecución  
donde  tres  instancias  (cliente1,  cliente2  y  cliente3)  comparten  el  mismo  recurso  (:Recurso)  utilizando  tres  instancias  de  
puntero  inteligente.

Figura  5­1.  Un  diagrama  de  objetos  que  muestra  cómo  tres  clientes  comparten  un  recurso  a  través  de  punteros  inteligentes

89

www.allitebooks.com
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

En  contraste  con  el  std::unique_ptr<T>  discutido  anteriormente,  std::shared_ptr<T>  es,  por  supuesto,  una  copia  
construible  como  se  esperaba.  Pero  puede  exigir  que  el  recurso  administrado  se  mueva  mediante  std::move<T>:

std::shared_ptr<ResourceType>  pointer1  =  std::make_shared<ResourceType>();  
std::shared_ptr<ResourceType>  pointer2;

puntero2  =  std::mover(puntero1); //  El  conteo  de  referencias  no  se  modifica,  puntero1  está  vacío

En  este  caso  no  se  modifica  el  contador  de  referencia,  pero  hay  que  tener  cuidado  al  usar  la  variable  pointer1
después  del  movimiento,  porque  está  vacío,  es  decir,  contiene  un  nullptr.  La  semántica  de  movimiento  y  la  función  de  
utilidad  std::move<T>  se  analizan  en  una  sección  posterior.

Sin  propiedad,  pero  con  acceso  seguro  con  std::weak_ptr<T>
A  veces  es  necesario  tener  un  puntero  no  propietario  a  un  recurso  que  es  propiedad  de  uno  o  más  punteros  compartidos.  Al  
principio  podrías  decir:  “Está  bien,  pero  ¿cuál  es  el  problema?  Simplemente  puedo  obtener  el  puntero  sin  procesar  de  una  
instancia  de  std::shared_ptr<T>  en  cualquier  momento  llamando  a  su  función  miembro  get()”.

Listado  5­5.  Recuperando  el  puntero  normal  de  una  instancia  de  std::shared_ptr<T>

std::shared_ptr<ResourceType>  resource  =  std::make_shared<ResourceType>(); // ...

ResourceType*  rawPointerToResource  =  resource.get();

¡Cuida  tu  paso!  Esto  podría  ser  peligroso.  ¿Qué  pasará  si  la  última  instancia  de  std::shared_
ptr<ResourceType>  se  destruye  en  algún  lugar  de  su  programa  y  este  puntero  sin  procesar  todavía  está  en  uso  en  
alguna  parte?  El  puntero  en  bruto  apuntará  a  la  Tierra  de  nadie  y  su  uso  puede  causar  serios  problemas  (recuerde  mi  
advertencia  sobre  el  comportamiento  indefinido  en  el  capítulo  anterior).  No  tiene  absolutamente  ninguna  posibilidad  de  determinar  
que  el  puntero  sin  procesar  apunta  a  una  dirección  válida  de  un  recurso,  o  a  una  ubicación  arbitraria  en
memoria.
Si  necesita  un  puntero  al  recurso  sin  tener  propiedad,  debe  usar  std::weak_ptr<T>  (definido  en  el  encabezado  
<memoria>),  que  no  influye  en  la  duración  del  recurso.  std::weak_ptr<T>  simplemente  "observa"  el  recurso  administrado  y  puede  
ser  interrogado  si  es  válido.

Listado  5­6.  Uso  de  std::weak_ptr<T>  para  gestionar  recursos  que  no  son  de  propiedad

01  #incluir  <memoria>  02

03  void  hacerAlgo(const  std::weak_ptr<ResourceType>&  debilRecurso)  { if  (!  debilRecurso.caducado())  
{ 04
05 //  Ahora  sabemos  que  el  recurso  débil  contiene  un  puntero  a  un  objeto  válido  std::shared_ptr<Tipo  
06 de  recurso>  recurso  compartido  =  recurso  débil.lock(); //  Usar  recurso  compartido...
07  
08   }
09 }  10

11  int  principal()  { 12
auto  sharedResource(std::make_shared<ResourceType>());  
13 std::weak_ptr<ResourceType>  débilRecurso(sharedResource);

90
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

14
15   hacerAlgo(recursodébil);  recurso  
16 compartido.reset(); //  Elimina  la  instancia  administrada  de  ResourceType  doSomething(weakResource);
17  
18
19   devolver  0;
20 }

Como  puede  ver  en  la  línea  4  del  ejemplo  de  código  anterior,  podemos  interrogar  al  objeto  de  puntero  débil  si  administra  un  
recurso  válido.  Esto  se  hace  llamando  a  su  función  miembro  expired().  std::weak_ptr<T>  no  proporciona  operadores  de  desreferencia,  
como  *  o  ­>.  Si  queremos  usar  el  recurso,  primero  debemos  llamar  a  la  función  lock()  (ver  línea  n.°  6)  para  obtener  un  objeto  puntero  
compartido.
Tal  vez  ahora  se  esté  preguntando  cuáles  son  los  casos  de  uso  de  este  tipo  de  puntero  inteligente.  ¿Por  qué  es  
necesario,  porque  también  podría  tomar  un  std::shared_ptr<T>  en  cualquier  lugar  donde  se  necesite  un  recurso?
En  primer  lugar,  con  std::shared_ptr<T>  y  std::weak_ptr<T>,  puede  distinguir  entre  los  propietarios  de  un  recurso  y  los  
usuarios  de  un  recurso  en  un  diseño  de  software.  No  todas  las  unidades  de  software  que  requieren  un  recurso  solo  para  una  tarea  
determinada  y  limitada  en  el  tiempo  quieren  convertirse  en  sus  propietarios.  Como  podemos  ver  en  la  función  doSomething()  en  el  
ejemplo  anterior,  a  veces  es  suficiente  simplemente  "promover"  un  puntero  débil  a  un  puntero  fuerte  solo  por  una  cantidad  de  tiempo  
limitada.
Un  buen  ejemplo  sería  una  caché  de  objetos  que,  con  el  fin  de  mejorar  la  eficiencia  del  rendimiento,  mantiene  los  objetos  
a  los  que  se  ha  accedido  recientemente  en  la  memoria  durante  un  cierto  período  de  tiempo.  Los  objetos  en  el  caché  se  mantienen  
con  instancias  std::shared_ptr<T>,  junto  con  una  marca  de  tiempo  utilizada  por  última  vez.  Periódicamente,  se  ejecuta  una  
especie  de  proceso  de  recolección  de  basura,  que  escanea  el  caché  y  decide  destruir  aquellos  objetos  que  no  se  han  utilizado  
durante  un  período  de  tiempo  definido.
En  aquellos  lugares  donde  se  usan  los  objetos  almacenados  en  caché,  se  usan  instancias  de  std::weak_ptr<T>  para  contener
punteros  no  propietarios  a  estos  objetos.  Si  la  función  miembro  expired()  de  esas  instancias  std::weak_ptr<T>  devuelve  
verdadero,  el  proceso  del  recolector  de  elementos  no  utilizados  ya  ha  borrado  los  objetos  de  la  memoria  caché.  En  el  otro  caso,  la  
función  std::weak_ptr<T>::lock()  se  puede  usar  para  recuperar  un  std::shared_ptr<T>  de  ella.  Ahora  el  objeto  se  puede  usar  de  forma  
segura,  incluso  si  el  proceso  del  recolector  de  basura  se  activa.  El  proceso  evalúa  el  contador  de  uso  de  std::shared_ptr<T>  y  
comprueba  que  el  objeto  tiene  actualmente  al  menos  un  usuario  fuera  de  la  memoria  caché.  Como  consecuencia,  se  prolonga  la  vida  
útil  de  los  objetos.  O  el  proceso  elimina  el  objeto  del  caché,  lo  que  no  interfiere  con  sus  usuarios.

Otro  ejemplo  es  tratar  con  dependencias  circulares.  Por  ejemplo,  si  tiene  una  clase  A  que  necesita  un  puntero  a  otra  
clase  B  y  viceversa,  terminará  con  una  dependencia  circular.  Si  usa  std::shared_ptr<T>  para  apuntar  a  la  otra  clase  respectiva  como  
se  muestra  en  el  siguiente  ejemplo  de  código,  puede  terminar  con  una  pérdida  de  memoria.  La  razón  de  esto  es  que  el  contador  de  uso  
en  la  respectiva  instancia  de  puntero  compartido  nunca  contará  hasta  0.  Por  lo  tanto,  los  objetos  nunca  se  eliminarán.

Listado  5­7.  El  problema  con  las  dependencias  circulares  causado  por  un  uso  irreflexivo  de  std::shared_ptr<T>

#include  <memoria>

clase  B; //  Declaración  de  reenvío

class  A  
{ public:  
void  setB(std::shared_ptr<B>&  pointerToB)  { myPointerToB  =  
pointerToB; }

91
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

privado:  
std::shared_ptr<B>  myPointerToB; };

class  B  
{ public:  
void  setA(std::shared_ptr<A>&  pointerToA)  
{ myPointerToA  =  pointerToA; }

privado:  
std::shared_ptr<A>  myPointerToA; };

int  principal()  {
{ //  Las  llaves  crean  un  alcance  auto  
pointerToA  =  std::make_shared<A>();  auto  
pointerToB  =  std::make_shared<B>();  punteroAA­
>setB(punteroAB);  punteroAB­
>setA(punteroAA); }

//  En  este  punto,  respectivamente,  una  instancia  de  A  y  B  está  "perdida  en  el  espacio" (pérdida  de  memoria)

devolver  0;
}

Si  las  variables  miembro  std::shared_ptr<T>  en  las  clases  se  reemplazan  por  punteros  débiles  no  propietarios
(std::weak_ptr<T>)  a  la  otra  clase  respectiva,  se  resuelve  el  problema  con  la  fuga  de  memoria.

Listado  5­8.  Dependencias  circulares  implementadas  de  la  manera  correcta  con  std::weak_ptr<T>

clase  B; //  Declaración  de  reenvío

class  A  
{ public:  
void  setB(std::shared_ptr<B>&  pointerToB)  
{ myPointerToB  =  pointerToB; }

privado:  
std::weak_ptr<B>  myPointerToB; };

class  B  
{ public:  
void  setA(std::shared_ptr<A>&  pointerToA)  
{ myPointerToA  =  pointerToA; }

privado:  
std::weak_ptr<A>  myPointerToA; }; // ...

92
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Básicamente,  las  dependencias  circulares  son  un  mal  diseño  en  el  código  de  la  aplicación  y  deben  evitarse  siempre  que
posible.  Puede  haber  algunas  excepciones  en  las  bibliotecas  de  bajo  nivel,  donde  las  dependencias  circulares  no  causan  
problemas  graves.  Pero  aparte  de  eso,  debe  seguir  el  Principio  de  dependencia  acíclica  que  se  analiza  en  una  sección  dedicada  en  el  
Capítulo  6.

Evite  los  nuevos  y  borrados  explícitos  En  un  programa  C++  

moderno,  al  escribir  el  código  de  la  aplicación  debe  evitar  llamar  a  new  y  delete  explícitamente.
¿Por  qué?  Bueno,  la  explicación  simple  y  breve  es  esta:  new  y  delete  aumentan  la  complejidad.
La  respuesta  más  detallada  es  esta:  cada  vez  que  es  inevitable  llamar  a  nuevo  y  eliminar,  uno  tiene  que  lidiar
con  una  situación  excepcional,  no  morosa,  situación  que  requiere  un  tratamiento  especial.  Para  comprender  cuáles  son  estos  casos  
excepcionales,  echemos  un  vistazo  a  los  casos  predeterminados:  las  situaciones  por  las  que  cualquier  desarrollador  de  C++  debería  
esforzarse.
Las  llamadas  explícitas  de  nuevo  y/o  eliminación  se  pueden  evitar  mediante  las  siguientes  medidas:

•  Utilice  asignaciones  en  la  pila  siempre  que  sea  posible.  Las  asignaciones  en  la  pila  son  simples  (recuerde  el  
principio  KISS  discutido  en  el  Capítulo  3)  y  seguras.  Es  imposible  perder  nada  de  esa  memoria  que  se  
asignó  en  la  pila.  El  recurso  se  destruirá  una  vez  que  quede  fuera  del  alcance.  Incluso  puede  devolver  
el  objeto  de  una  función  por  valor,  transfiriendo  así  su  contenido  a  la  función  que  llama.

•  Para  asignar  un  recurso  en  el  montón,  use  "hacer  funciones".  Use  std::make_  unique<T>  o  
std::make_shared<T>  para  crear  una  instancia  del  recurso  y  envuélvalo  inmediatamente  en  
un  objeto  administrador  que  se  ocupa  del  recurso,  un  puntero  inteligente.

•  Use  contenedores  (Standard  Library,  Boost  u  otros)  donde  sea  apropiado.
Container  gestiona  el  espacio  de  almacenamiento  de  sus  elementos.  En  cambio,  en  el  caso  de  
secuencias  y  estructuras  de  datos  desarrolladas  por  uno  mismo,  se  ve  obligado  a  implementar  toda  la  
administración  de  almacenamiento  por  su  cuenta,  lo  que  puede  ser  una  tarea  compleja  y  propensa  a  errores.

•  Proporcionar  envoltorios  para  recursos  de  bibliotecas  propietarias  de  terceros  que  requieran  una  
gestión  de  memoria  específica  (consulte  la  siguiente  sección).

Gestión  de  recursos  patentados  Como  ya  se  mencionó  en  la  

introducción  a  esta  sección  sobre  la  gestión  de  recursos,  a  veces  es  necesario  gestionar  otros  recursos  que  no  están  asignados  o  
desasignados  en  el  montón  mediante  el  operador  predeterminado  nuevo  o  eliminar.  Ejemplos  de  este  tipo  de  recursos  son  
los  archivos  abiertos  de  un  sistema  de  archivos,  un  módulo  cargado  dinámicamente  (p.  ej.,  una  biblioteca  de  vínculos  dinámicos  
(DLL)  en  los  sistemas  operativos  Windows)  u  objetos  específicos  de  la  plataforma  de  una  interfaz  gráfica  de  usuario  (p.  ej.,  
Windows,  Buttons ,  campos  de  entrada  de  texto,  etc.).
A  menudo,  este  tipo  de  recursos  se  administran  a  través  de  algo  que  se  denomina  identificador .  Un  identificador  es  una  
referencia  abstracta  y  única  a  un  recurso  del  sistema  operativo.  En  Windows,  el  tipo  de  datos  HANDLE  se  usa  para  definir  dichos  
identificadores.  De  hecho,  este  tipo  de  datos  se  define  de  la  siguiente  manera  en  el  encabezado  WinNT.h,  un  archivo  de  encabezado  
de  estilo  C  que  define  varios  tipos  y  macros  de  la  API  de  Win32:

typedef  void  *HANDLE;

Por  ejemplo,  si  desea  acceder  a  un  proceso  de  Windows  en  ejecución  con  un  determinado  ID  de  proceso,  puede  recuperar  
un  identificador  de  este  proceso  mediante  la  función  OpenProcess()  de  la  API  de  Win32.

#include  <windows.h> // ...  
const  
DWORD  processId  =  4711;  HANDLE  
processHandle  =  OpenProcess(PROCESS_ALL_ACCESS,  FALSE,  processId);

93
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Una  vez  que  haya  terminado  con  el  identificador,  debe  cerrarlo  utilizando  la  función  CloseHandle():

BOOL  exito  =  CloseHandle(processHandle);

Por  lo  tanto,  tenemos  una  simetría  similar  al  operador  new  y  su  correspondiente  operador  delete.  Por  lo  tanto,  también  debería  
ser  posible  aprovechar  el  lenguaje  RAII  y  utilizar  punteros  inteligentes  para  dichos  recursos.
Primero,  solo  tenemos  que  cambiar  el  eliminador  predeterminado  (que  llama  a  eliminar)  por  un  eliminador  personalizado  que  
llame  a  CloseHandle():

#include  <windows.h> //  Declaraciones  de  la  API  de  Windows

class  Win32HandleCloser  { public:  
void  
operator()(HANDLE  handle)  const  { if  (handle !=  
INVALID_HANDLE_VALUE)  { CloseHandle(handle); }

} };

¡Ten  cuidado!  Si  ahora  define  un  alias  de  tipo  escribiendo  algo  como  lo  siguiente,  std::shared_  ptr<T>  administrará  algo  que  es  
de  tipo  void**,  porque  HANDLE  ya  está  definido  como  un  puntero  vacío:

usando  Win32SharedHandle  =  std::shared_ptr<HANDLE>; //  ¡Precaución!

Por  lo  tanto,  los  punteros  inteligentes  para  Win32  HANDLE  deben  definirse  de  la  siguiente  manera:

usando  Win32SharedHandle  =  std::shared_ptr<void>;  usando  
Win32WeakHandle  =  std::weak_ptr<void>;

■  Nota  ¡No  está  permitido  definir  un  std::unique_ptr<void>  en  C++!  Esto  se  debe  a  que  std::shared_ptr<T>  implementa  
el  borrado  de  tipos,  mientras  que  std::unique_ptr<T>  no  lo  hace.  Si  una  clase  admite  el  borrado  de  tipos,  significa  que  
puede  almacenar  objetos  de  un  tipo  arbitrario  y  destruirlos  correctamente.

Si  desea  utilizar  el  identificador  compartido,  debe  prestar  atención  a  pasar  una  instancia  del  eliminador  personalizado  
Win32HandleCloser  como  parámetro  durante  la  construcción:

const  DWORD  processId  =  4711;
Win32SharedHandle  processHandle  { OpenProcess(PROCESS_ALL_ACCESS,  FALSE,  processId),
Win32HandleCloser  () };

Nos  gusta  moverlo
Si  alguien  me  preguntara  qué  característica  de  C++  11  tiene  probablemente  el  impacto  más  profundo  en  cómo  se  escribirán  los  programas  
modernos  de  C++  ahora  y  en  el  futuro,  claramente  mencionaría  la  semántica  de  movimiento.  Ya  he  discutido  brevemente  la  semántica  
de  movimiento  de  C++  en  el  Capítulo  4,  en  la  sección  sobre  estrategias  para  evitar  punteros  regulares.
Pero  creo  que  son  tan  importantes  que  quiero  profundizar  aquí  en  esta  característica  del  lenguaje.

94
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

¿Qué  son  las  semánticas  de  movimiento?

En  muchos  casos  anteriores  en  los  que  el  antiguo  lenguaje  C++  nos  obligaba  a  usar  un  constructor  de  copias,  en  realidad  no  
queríamos  crear  una  copia  profunda  de  un  objeto.  En  cambio,  simplemente  queríamos  "mover  la  carga  útil  del  objeto".
La  carga  útil  de  un  objeto  no  es  más  que  los  datos  incrustados  que  el  objeto  lleva  consigo,  así  que  nada  más  que  otros  objetos  o  
variables  miembro  de  tipos  primitivos  como  int.
Estos  casos  de  antaño  en  los  que  teníamos  que  copiar  un  objeto  en  lugar  de  moverlo  eran,  por  ejemplo,  los  siguientes:

•La  devolución  de  una  instancia  de  objeto  local  como  un  valor  de  retorno  de  una  función  o
método.  Para  evitar  la  construcción  de  copias  en  estos  casos  anteriores  a  C++  11,  se  usaban  con  
frecuencia  punteros.
• Insertar  un  objeto  en  un  std::vector  u  otros  contenedores.

•La  implementación  de  la  función  de  plantilla  std::swap<T>.

En  muchas  de  las  situaciones  antes  mencionadas,  no  es  necesario  mantener  intacto  el  objeto  de  origen,  es  decir,  crear  una  copia  
profunda  y,  en  términos  de  eficiencia  de  tiempo  de  ejecución,  a  menudo  costosa,  para  que  los  objetos  de  origen  sigan  siendo  utilizables.
C++11  ha  introducido  una  función  de  lenguaje  que  ha  hecho  que  mover  los  datos  incrustados  de  un  objeto  sea  una  
operación  de  primera  clase.  Además  del  constructor  de  copia  y  el  operador  de  asignación  de  copia,  el  desarrollador  de  la  clase  
ahora  puede  implementar  constructores  de  movimiento  y  operadores  de  asignación  de  movimiento  (¡más  adelante  veremos  
por  qué  no  debería  hacerlo!).  Las  operaciones  de  traslado  suelen  ser  muy  eficientes.  A  diferencia  de  una  operación  de  
copia  real,  los  datos  del  objeto  de  origen  simplemente  se  transfieren  al  objeto  de  destino  y  el  argumento  (el  objeto  de  origen)  
de  la  operación  se  coloca  en  una  especie  de  estado  "vacío"  o  inicial.
El  siguiente  ejemplo  muestra  una  clase  arbitraria  que  implementa  explícitamente  ambos  tipos  de  semántica:  
constructor  de  copia  (línea  n.°  6)  y  operador  de  asignación  (línea  n.°  8),  así  como  constructor  de  movimiento  (línea  n.°  7)  y  
operador  de  asignación  (línea  n.° .  9).

Listado  5­9.  Una  clase  de  ejemplo  que  declara  explícitamente  funciones  miembro  especiales  para  copiar  y  mover

01  #incluir  <cadena>  02  03  

clase  Clazz  { 04  público:

Clazz()  no  excepto; //  Constructor  por  defecto  05
06 Clazz  (const.  Clazz  y  otros); //  Copiar  constructor
07   Clazz(Clazz&&  otros)  noexcept; //  Mover  constructor
08 Operador  Clazz&=(const  Clazz&  otro); //  Copiar  operador  de  asignación
09 Operador  Clazz&=(Clazz&&  otro)  noexcept; //  Mover  operador  de  asignación  10  virtual  ~Clazz()  
noexcept; //  Destructor  11

12  privado: // ...  
13  14 };

Como  veremos  más  adelante  en  la  sección  "La  regla  del  cero",  debería  ser  un  objetivo  principal  de  cualquier  desarrollador  de  C++
no  declarar  y  definir  dichos  constructores  y  operadores  de  asignación  explícitamente.
La  semántica  de  movimiento  está  estrechamente  relacionada  con  algo  que  se  llama  referencias  de  rvalue  (consulte  la  siguiente  sección).
El  constructor  u  operador  de  asignación  de  una  clase  se  denomina  "constructor  de  movimiento"  respectivamente  
"operador  de  asignación  de  movimiento",  cuando  toma  una  referencia  de  valor  r  como  parámetro.  Una  referencia  de  
valor  r  se  marca  mediante  el  operador  de  doble  ampersand  (&&).  Para  una  mejor  distinción,  la  referencia  ordinaria  con  su  
único  ampersand  (&)  ahora  también  se  denomina  referencia  lvalue.

95
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

El  asunto  con  esos  lvalues  y  rvalues
Los  llamados  lvalue  y  rvalue  son  históricamente  términos  (heredados  del  lenguaje  C),  porque  los  lvalues  generalmente  
pueden  aparecer  en  el  lado  izquierdo  de  una  expresión  de  asignación,  mientras  que  los  rvalues  generalmente  pueden  
aparecer  en  el  lado  derecho  de  una  expresión  de  asignación.  En  mi  opinión,  una  explicación  mucho  mejor  para  lvalue  es  
que  es  un  valor  localizador.  Esto  deja  en  claro  que  un  lvalue  representa  un  objeto  que  ocupa  una  ubicación  en  la  memoria  
(es  decir,  tiene  una  dirección  de  memoria  accesible  e  identificable).
Por  el  contrario,  los  valores  r  son  todos  aquellos  objetos  en  una  expresión  que  no  son  valores  l.  Es  un  objeto  temporal,  
o  subobjeto  del  mismo.  Por  lo  tanto,  no  es  posible  asignar  nada  a  un  valor  r.
Aunque  estas  definiciones  provienen  del  antiguo  mundo  C,  y  C++  11  aún  ha  introducido  más  categorías
(xvalue,  glvalue  y  prvalue)  para  habilitar  la  semántica  de  movimiento,  son  bastante  buenos  para  el  uso  diario.
La  forma  más  simple  de  una  expresión  lvalue  es  una  declaración  de  variable:

Escriba  var1;

La  expresión  var1  es  un  valor  l  de  tipo  Tipo.  Las  siguientes  declaraciones  también  representan  lvalues:

Tipo*  puntero;
tipo  y  referencia;
Tipo  y  función  ();

Un  lvalue  puede  ser  el  operando  izquierdo  de  una  operación  de  asignación,  como  la  variable  entera
theAnswerToAllQuestions  en  este  ejemplo:

int  theAnswerToAllQuestions  =  42;

Además,  la  asignación  de  una  dirección  de  memoria  a  un  puntero  deja  en  claro  que  el  puntero  es  un  valor  l:

Escriba*  pointerToVar1  =  &var1;

El  literal  "42"  en  cambio  es  un  valor  r.  No  representa  una  ubicación  identificable  en  la  memoria,  por  lo  que  no  es
es  posible  asignarle  cualquier  cosa  (por  supuesto,  los  valores  r  también  ocupan  memoria  en  la  sección  de  datos  de  la  
pila,  pero  esta  memoria  se  asigna  temporalmente  y  se  libera  inmediatamente  después  de  completar  la  operación  de  
asignación):

número  entero  =  23; //  Funciona,  porque  'número'  es  un  lvalue  42  =  número; //  
Error  del  compilador:  se  requiere  lvalue  como  operando  izquierdo  de  la  asignación

¿No  cree  que  la  función  ()  en  la  tercera  línea  de  los  ejemplos  genéricos  anteriores  es  un  valor  l?  ¡Es!
Puede  escribir  el  siguiente  fragmento  de  código  (sin  duda,  algo  extraño)  y  el  compilador  lo  compilará  sin  quejas:

int  theAnswerToAllQuestions  =  42;

int&  function()  
{ devuelve  la  respuesta  a  todas  las  
preguntas; }

int  principal()  
{ función()  =  23; //  ¡Obras!  devolver  
0; }

96
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Referencias  de  valor
Como  ya  se  mencionó  anteriormente,  la  semántica  de  movimiento  de  C++  11  está  estrechamente  relacionada  con  algo  que  se  
llama  referencias  de  rvalue.  Estas  referencias  de  rvalue  ahora  hacen  posible  abordar  la  ubicación  de  memoria  de  rvalues.  En  el  
siguiente  ejemplo,  la  memoria  temporal  se  asigna  a  una  referencia  de  valor  r  y,  por  lo  tanto,  la  convierte  en  "permanente".
Incluso  puede  recuperar  un  puntero  que  apunte  a  esta  ubicación  y  manipular  la  memoria  a  la  que  hace  referencia  la  referencia  
rvalue  usando  este  puntero.

int&&  rvalueReference  =  25  +  17;  int*  
pointerToRvalueReference  =  &rvalueReference;  
*pointerToRvalueReference  =  23;

Al  introducir  referencias  de  valor  r,  estas  pueden,  por  supuesto,  aparecer  también  como  parámetros  en  funciones  o
métodos.  La  Tabla  5­1  muestra  las  posibilidades.

Tabla  5­1.  Diferentes  funciones  respectivamente  firmas  de  métodos  y  sus  tipos  de  parámetros  permitidos

Firma  de  función/método Tipos  de  parámetros  permitidos

void  function(Type  param)  void   Tanto  lvalues  como  rvalues  pueden  pasarse  como  parámetros.
X::method(Type  param)  void  

function(Type&  param)  void   Solo  se  pueden  pasar  lvalues  como  parámetros.
function(const  Type&  param)  void  
X::method(Type&  param)  void  
X::method(const  Type&  param)  void  

function( Tipo&&  parámetro)  void   Solo  se  pueden  pasar  valores  r  como  parámetros.
X::método(Tipo&&  parámetro)

La  Tabla  5­2  muestra  la  situación  de  los  tipos  de  devolución  de  una  función  o  método  y  lo  que  se  permite  para  la  
declaración  de  devolución  de  la  función/método:

Tabla  5­2.  Tipos  posibles  de  tipos  de  retorno  de  funciones  respectivamente  parámetros

Firma  de  función/método Posibles  tipos  de  datos  devueltos  por  la  declaración  de  devolución

int  función()  int   [const]  int,  [const]  int&,  o  [const]  int&&.
X::método()  int&  

función()  int&   No  const  int  o  int&.
X::método()  int&&  

función()  int&&   Literales  (p.  ej.,  devuelve  42),  o  una  referencia  rvalue  (obtenida  con  
X::método() std::move())  a  un  objeto  con  una  duración  mayor  que  el  alcance  del  método  
respectivo  de  la  función.

Aunque,  por  supuesto,  se  permite  el  uso  de  referencias  rvalue  para  parámetros  en  cualquier  función  o  método,
su  campo  de  aplicación  predestinado  está  en  los  constructores  de  movimientos  y  los  operadores  de  asignación  de  movimientos.

Listado  5­10.  Una  clase  que  define  explícitamente  la  semántica  de  copiar  y  mover

#incluir  <utilidad> //  estándar::mover<T>

clase  Clazz  
{ público:
Clazz()  =  predeterminado;
97
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Clazz  (const  Clazz  y  otros)  {
//  Construcción  de  copia  clásica  para  lvalues }

Clazz(Clazz&&  otros)  no  excepto  {
//  Mover  constructor  para  rvalues:  mueve  contenido  de  'otro'  a  este
}
Operador  Clazz&  =(const  Clazz&  otro)  {
//  Asignación  de  copia  clásica  para  lvalues  return  *this;

Operador  Clazz&  =(Clazz&&  otro)  noexcept  {
//  Mover  asignación  para  rvalues:  mueve  contenido  de  'otro'  a  este  return  *this;

} // ... };

int  principal()  {
Clazz  anObjeto;
Clazz  otroObjeto1(unObjeto); //  Llama  al  constructor  de  copias
Clazz  otroObjeto2(std::move(unObjeto)); //  Llama  al  constructor  de  movimiento  anObject  =  
anotherObject1; //  Llama  al  operador  de  asignación  de  copia  
anotherObject2  =  std::move(anObject); //  Llama  al  operador  de  asignación  de  
movimiento  return  0;
}

No  aplique  Move  Everywhere  Tal  vez  haya  notado  el  uso  

de  la  función  auxiliar  std::move<T>()  (definida  en  el  encabezado  <utility>)  en  el  ejemplo  de  código  anterior  para  obligar  al  
compilador  a  usar  la  semántica  de  movimiento.
En  primer  lugar,  el  nombre  de  esta  pequeña  función  auxiliar  es  engañoso.  std::move<T>()  no  se  mueve
cualquier  cosa.  Es  más  o  menos  un  molde  que  produce  una  referencia  de  valor  r  a  un  objeto  de  tipo  T.
En  la  mayoría  de  los  casos,  no  es  necesario  hacer  eso.  En  circunstancias  normales,  la  selección  entre  las  versiones  
de  copia  y  movimiento  de  los  constructores  o  los  operadores  de  asignación  se  realiza  automáticamente  en  tiempo  de  
compilación  a  través  de  la  resolución  de  sobrecarga.  El  compilador  determina  si  se  confronta  con  un  valor  l  o  un  valor  r,  y  
luego  selecciona  el  constructor  u  operador  de  asignación  que  mejor  se  ajuste  en  consecuencia.  Las  clases  contenedoras  
de  la  biblioteca  estándar  de  C++  también  tienen  en  cuenta  el  nivel  de  seguridad  de  excepción  que  garantizan  las  operaciones  
de  movimiento  (analizaremos  este  tema  con  más  detalle  más  adelante  en  la  sección  "La  prevención  es  mejor  que  el  cuidado  posterior").
Tenga  en  cuenta  esto  especialmente:  no  escriba  código  como  este:

Listado  5­11.  Un  uso  inapropiado  de  std::move()

#include  <cadena>  
#include  <utilidad>  
#include  <vector>

utilizando  StringVector  =  std::vector<std::string>;

98
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

StringVector  crearVectorDeCadenas()  {
Resultado  de  
StringVector; // ...hacer  algo  para  que  el  vector  se  llene  con  muchas  cadenas...  return  
std::move(result); //  Malo  e  innecesario,  solo  escribe  "return  result;"! }

El  uso  de  std::move<T>()  con  la  declaración  de  retorno  no  solo  es  completamente  innecesario,  porque  el  compilador  
ya  sabe  que  la  variable  es  candidata  para  ser  movida  fuera  de  la  función  (desde  C++11,  la  semántica  de  movimiento  es  compatible  
con  todos  los  contenedores  de  la  biblioteca  estándar,  así  como  por  muchas  otras  clases  de  la  biblioteca  estándar,  como  
std::string).  Un  impacto  posiblemente  aún  peor  podría  ser  que  puede  interferir  con  la  RVO  (Optimización  del  valor  de  retorno),  
también  conocida  como  elisión  de  copia,  que  realizan  casi  todos  los  compiladores  en  la  actualidad.  La  elisión  de  copia  respectiva  
de  RVO  permite  a  los  compiladores  optimizar  una  construcción  de  copia  costosa  al  devolver  valores  de  una  función  o  método.

Piense  siempre  en  el  importante  principio  del  Capítulo  3:  ¡Tenga  cuidado  con  las  optimizaciones!  No  arruine  su  código  con  
declaraciones  std::move<T>()  en  todas  partes,  solo  porque  cree  que  puede  ser  más  inteligente  que  su  compilador  con  la  
optimización  de  su  código.  ¡Usted  no!  La  legibilidad  de  su  código  se  verá  afectada  con  todos  esos  std::move<T>()  en  todas  
partes,  y  es  posible  que  su  compilador  no  pueda  realizar  sus  estrategias  de  optimización  correctamente.

La  regla  del  cero
Como  desarrollador  experimentado  de  C++,  es  posible  que  ya  conozca  la  regla  de  los  tres  y  la  regla  de  los  cinco.
La  regla  de  los  tres  [Koenig01],  acuñada  originalmente  por  Marshall  Cline  en  1991,  establece  que  si  una  clase  define  un  
destructor  de  forma  explícita,  casi  siempre  debe  definir  un  constructor  de  copia  y  un  operador  de  asignación  de  copia.
Con  la  llegada  de  C++  11,  esta  regla  se  amplió  y  se  convirtió  en  la  Regla  de  los  cinco,  porque  el  constructor  de  movimiento  y  el  
operador  de  asignación  de  movimiento  se  agregaron  al  lenguaje,  y  también  estas  dos  funciones  miembro  especiales  deben  
definirse  también  si  una  clase  define  un  destructor.
La  razón  por  la  cual  la  Regla  de  tres  y,  respectivamente,  la  Regla  de  cinco  fueron  buenos  consejos  durante  mucho  tiempo  en  
el  diseño  de  clases  de  C++,  y  son  errores  sutiles  que  pueden  ocurrir  cuando  los  desarrolladores  no  los  están  considerando,  como  
se  demuestra  con  el  siguiente  ejemplo  de  código  intencionalmente  incorrecto .

Listado  5­12.  Una  implementación  incorrecta  de  una  clase  de  cadena

#incluir  <ccadena>

class  MyString  
{ público:  
explícito  MyString(const  std::size_t  sizeOfString) :  data  { new  char[sizeOfString] }  { }
MyString(const  char*  const  charArray,  const  std::size_t  sizeOfArray)  { data  =  new  char[sizeOfArray];  
strcpy(datos,  charArray); }  virtual  
~MyString()  { eliminar[]  datos; };

char&  operator[](const  std::size_t  index)  { return  data[index]; }  
const  char&  operador[]

(const  std::size_t  index)  const  {
devolver  datos[índice]; } // ...

privado:  
char*  datos; };

99
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

De  hecho,  esta  es  una  clase  de  cadena  implementada  de  manera  muy  amateur  con  algunos  defectos,  por  ejemplo,  una  
verificación  faltante  de  que  no  se  pasa  un  nullptr  al  constructor  de  inicialización,  e  ignorando  por  completo  el  hecho  de  que  las  cadenas  
generalmente  pueden  crecer  y  reducirse.  Por  supuesto,  hoy  en  día  nadie  tiene  que  implementar  una  clase  de  cadena  y,  por  lo  tanto,  
reinventar  la  rueda.  Con  std::string,  una  clase  de  cadena  a  prueba  de  viñetas  está  disponible  en  la  biblioteca  estándar  de  C++.
Sin  embargo,  sobre  la  base  del  ejemplo  anterior,  es  muy  fácil  demostrar  por  qué  es  importante  adherirse  a  la  regla  de  los  cinco.

Para  que  la  memoria  asignada  por  los  constructores  de  inicialización  para  la  representación  de  cadena  interna  
se  libere  de  forma  segura,  se  debe  definir  un  destructor  explícito  y  se  debe  implementar  para  hacer  esto.
En  la  clase  anterior,  sin  embargo,  se  viola  la  regla  de  los  cinco  y  faltan  los  constructores  explícitos  de  copiar/mover,  así  como  los  
operadores  de  asignación  de  copiar/mover.
Ahora,  supongamos  que  estamos  usando  la  clase  MyString  de  la  siguiente  manera:

int  main()  
{ MiCadena  unaCadena("Prueba",  4);  
MiCadena  otraCadena  { unaCadena }; //  ¡UH  oh! :­( devuelve  0;

Debido  al  hecho  de  que  nuestra  clase  MyString  no  define  explícitamente  un  constructor  de  copia  o  movimiento,  el  
compilador  sintetizará  estas  funciones  miembro  especiales,  es  decir,  el  compilador  generará  un  constructor  de  copia  predeterminado  
respectivamente  y  un  constructor  de  movimiento  predeterminado.  Y  estas  implementaciones  predeterminadas  solo  crean  una  copia  
plana  de  las  variables  miembro  del  objeto  de  origen.  En  nuestro  caso,  el  valor  de  dirección  almacenado  en  los  datos  del  puntero  de  
carácter  se  copia,  pero  no  el  área  de  la  memoria  a  la  que  apunta  este  puntero.
Eso  significa  lo  siguiente:  después  de  llamar  al  constructor  de  copia  predeterminado  generado  automáticamente  para  crear  
otra  Cadena,  ambas  instancias  de  Mi  Cadena  comparten  los  mismos  datos,  como  se  puede  ver  fácilmente  en  la  Vista  de  Variables  del  
Depurador  que  se  muestra  en  la  Figura  5­2.

Figura  5­2.  Ambos  punteros  de  caracteres  apuntan  a  la  misma  dirección  de  memoria

Esto  resultará  en  una  doble  eliminación  de  los  datos  internos  si  se  destruyen  los  objetos  de  cadena  y,  por  lo  tanto,  puede
causar  problemas  críticos,  como  fallas  de  segmentación  o  comportamiento  indefinido.
En  circunstancias  normales,  no  hay  motivo  para  definir  un  destructor  explícito  para  una  clase.  Cada  vez  que  se  ve  obligado  
a  definir  un  destructor,  esta  es  una  excepción  notable,  porque  indica  que  necesita  hacer  algo  especial  con  los  recursos  al  final  de  la  
vida  útil  de  un  objeto  que  requiere  un  esfuerzo  considerable.  Por  lo  general,  se  requiere  un  destructor  no  trivial  para  desasignar  
recursos,  por  ejemplo,  memoria  en  el  montón.  Como  consecuencia,  también  necesita  definir  constructores  de  copiar/mover  
explícitos  y  operadores  de  asignación  de  copiar/mover  para  manejar  estos  recursos  correctamente  mientras  copia  o  mueve.

Eso  es  lo  que  implica  la  regla  de  cinco.

100
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Existen  diferentes  enfoques  para  tratar  el  problema  descrito  anteriormente.  Por  ejemplo,  podemos  proporcionar  
constructores  explícitos  de  copiar/mover  y  también  operadores  de  asignación  de  copiar/mover  para  manejar  
correctamente  la  memoria  asignada,  por  ejemplo,  creando  una  copia  profunda  del  área  de  memoria  a  la  que  
apunta  el  puntero.  Otro  enfoque  sería  prohibir  copiar  y  mover,  y  evitar  que  el  compilador  genere  versiones  
predeterminadas  de  estas  funciones.  Esto  se  puede  hacer  desde  C  ++  11  eliminando  estas  funciones  miembro  
especiales  para  que  cualquier  uso  de  una  función  eliminada  esté  mal  formado,  es  decir,  el  programa  no  se  compilará.

Listado  5­13.  Una  clase  MyString  modificada  que  elimina  explícitamente  el  constructor  de  copia  y  el  operador  de  asignación  
de  copia

class  MyString  
{ público:  
explícito  MyString(const  std::size_t  sizeOfString) :  data  { new  char[sizeOfString] }  { }
MyString(const  char*  const  charArray,  const  int  sizeOfArray)  { data  =  new  
char[sizeOfArray];  strcpy(datos,  
charArray); }  virtual  ~MyString()  

{ eliminar[]  datos; };  MiCadena(const  MiCadena&)  
=  delete;  MiCadena&  operator=(const  
MiCadena&)  =  borrar; // ... };

El  problema  es  que  al  eliminar  las  funciones  miembro  especiales,  la  clase  ahora  tiene  un  área  de  uso  muy  
limitada.  Por  ejemplo,  MyString  no  se  puede  usar  en  un  std::vector  ahora,  porque  std::vector  requiere  que  su  tipo  
de  elemento  T  sea  asignable  por  copia  y  construible  por  copia.
Bien,  ahora  es  el  momento  de  elegir  un  enfoque  diferente  y  pensar  de  manera  diferente.  Lo  que  tenemos  que  hacer  es  
deshacernos  del  destructor  que  libera  el  recurso  asignado.  Si  esto  tiene  éxito,  tampoco  es  necesario,  de  acuerdo  con  la  Regla  de  los  
Cinco,  proporcionar  explícitamente  las  otras  funciones  especiales  de  los  miembros.  Así  que,  aquí  vamos:

Listado  5­14.  Reemplazar  el  puntero  char  por  un  vector  de  char  hace  que  un  destructor  explícito  sea  superfluo

#incluir  <vector>

class  MyString  
{ public:  
explicit  MyString(const  std::size_t  sizeOfString)  
{ data.resize(sizeOfString,  '  '); }

MyString(const  char*  const  charArray,  const  int  sizeOfArray) :  MyString(sizeOfArray)  { if  (charArray !=  nullptr)  
{ for  (int  index  =  0;  index  <  
sizeOfArray;  index++)  { data[index]  =  charArray[index]; } } }

char&  operator[](const  std::size_t  index)  { return  
data[index]; }

101
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

const  char&  operator[](const  std::size_t  index)  const  {
devolver  datos[índice]; } // ...

privado:  
std::vector<char>  datos; };

Una  vez  más,  un  comentario:  sé  que  esta  es  una  implementación  poco  práctica  y  amateur  de  una  cadena  hecha  a  sí  misma,  
que  no  es  necesaria  hoy  en  día,  pero  es  solo  para  fines  de  demostración.
¿Qué  ha  cambiado  ahora?  Bueno,  hemos  reemplazado  el  miembro  privado  de  tipo  char*  por  un  std::vector  con  el  tipo  
de  elemento  char.  Por  lo  tanto,  ya  no  necesitamos  un  destructor  explícito,  porque  no  tenemos  nada  que  hacer  si  se  destruye  un  
objeto  de  nuestro  tipo  MyString.  No  es  necesario  desasignar  ningún  recurso.  Como  resultado,  las  funciones  miembro  especiales  
generadas  por  el  compilador,  como  el  constructor  de  copiar/mover  o  el  operador  de  asignación  de  copiar/mover,  hacen  lo  correcto  
automáticamente  si  se  usan,  y  no  tenemos  que  definirlas  explícitamente.
Y  esas  son  buenas  noticias,  porque  hemos  seguido  el  principio  KISS  (consulte  el  Capítulo  3).
¡Y  eso  nos  lleva  a  la  Regla  del  Cero!  La  Regla  del  Cero  fue  acuñada  por  R.  Martinho  Fernandes  en  un  blog
puesto  en  2012  [Fernandes12].  La  regla  también  fue  promovida  por  el  miembro  del  comité  de  normas  ISO,  Prof.  Peter  
Sommerlad,  Director  del  Instituto  IFS  para  Software  en  HSR  Hochschule  für  Technik  Rapperswil  (Suiza),  en  una  conferencia  sobre  
Meeting  C++  2013  [Sommerlad13].  Esto  es  lo  que  dice  la  regla:

Escriba  sus  clases  de  manera  que  no  necesite  declarar/definir  ni  un  destructor,  ni  un  constructor  de  copiar/mover  
ni  un  operador  de  asignación  de  copiar/mover.  Use  punteros  inteligentes  de  C++  y  clases  y  contenedores  de  
biblioteca  estándar  para  administrar  recursos.

En  otras  palabras,  la  regla  del  cero  establece  que  sus  clases  deben  diseñarse  de  manera  que  las  funciones  miembro  
generadas  por  el  compilador  para  copiar,  mover  y  destruir  automáticamente  hagan  lo  correcto.  Esto  hace  que  sus  clases  sean  
más  fáciles  de  entender  (piense  siempre  en  el  principio  KISS  del  Capítulo  3),  menos  propensas  a  errores  y  más  fáciles  de  
mantener.  El  principio  detrás  de  esto  es  este:  hacer  más  escribiendo  menos  código.

El  compilador  es  tu  colega
Como  ya  he  escrito  en  otro  lugar,  la  llegada  del  estándar  de  lenguaje  C++  11  ha  cambiado  fundamentalmente  la  forma  en  que  se  
diseñarán  los  programas  C++  modernos  y  limpios  en  la  actualidad.  Los  estilos,  patrones  y  modismos  que  utilizan  los  programadores  
al  escribir  el  código  C++  moderno  son  totalmente  diferentes  a  los  anteriores.  Además  del  hecho  de  que  los  estándares  C++  más  
nuevos  ofrecen  muchas  características  nuevas  y  útiles  para  escribir  código  C++  que  es  fácil  de  mantener,  comprensible,  eficiente  
y  comprobable,  algo  más  ha  cambiado:  ¡ el  papel  del  compilador!
En  tiempos  anteriores,  el  compilador  era  solo  una  herramienta  para  traducir  el  código  fuente  a  una  máquina  ejecutable
instrucciones  (código  objeto)  para  una  computadora;  pero  ahora  se  está  convirtiendo  cada  vez  más  en  una  herramienta  
para  apoyar  al  desarrollador  en  diferentes  niveles.  Los  tres  principios  rectores  para  trabajar  con  un  compilador  de  C++  hoy  en  día  son  
los  siguientes:

•Todo  lo  que  se  puede  hacer  en  tiempo  de  compilación  también  debe  hacerse  en  tiempo  de  compilación.

•Todo  lo  que  se  puede  verificar  en  tiempo  de  compilación  también  debe  verificarse  en  tiempo  de  compilación.

•  Todo  lo  que  el  compilador  puede  saber  acerca  de  un  programa  también  debe  ser  determinado  por
el  compilador

102
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

En  capítulos  y  secciones  anteriores,  ya  hemos  experimentado  en  algunos  puntos  cómo  el  compilador  puede  ayudarnos.  
Por  ejemplo,  en  la  sección  sobre  la  semántica  de  movimiento,  hemos  visto  que  los  compiladores  modernos  de  C++  hoy  en  día  
pueden  realizar  múltiples  optimizaciones  sofisticadas  (p.  ej.,  elisión  de  copias)  que  ya  no  nos  tienen  que  importar.  En  las  siguientes  
secciones,  le  mostraré  cómo  el  compilador  puede  ayudarnos  a  los  desarrolladores  y  facilitarnos  muchas  cosas.

Deducción  automática  de  tipos  ¿Recuerda  el  

significado  de  la  palabra  clave  auto  de  C++  antes  de  C++11?  Estoy  bastante  seguro  de  que  probablemente  fue  la  palabra  clave  menos  
conocida  y  utilizada  en  el  idioma.  Tal  vez  recuerde  que  auto  en  C++98  o  C++03  era  un  especificador  de  clase  de  almacenamiento  y  se  
usaba  para  definir  que  una  variable  local  tiene  "duración  automática",  es  decir,  la  variable  se  crea  en  el  punto  de  definición,  y  se  destruye  
cuando  se  sale  del  bloque  del  que  formaba  parte.
Desde  C++11,  todas  las  variables  tienen  una  duración  automática  por  defecto  a  menos  que  se  especifique  lo  contrario.  Por  lo  tanto,  la  
semántica  anterior  de  auto  se  estaba  volviendo  inútil  y  la  palabra  clave  adquirió  un  significado  completamente  nuevo.
Hoy  en  día,  auto  se  usa  para  la  deducción  automática  de  tipos,  a  veces  también  llamada  inferencia  de  tipos.  Si  se  usa  como  
especificador  de  tipo  para  una  variable,  especifica  que  el  tipo  de  la  variable  que  se  declara  se  deducirá  (o  inferirá)  automáticamente  de  
su  inicializador,  como  en  los  siguientes  ejemplos:

auto  theAnswerToAllQuestions  =  42;  iteración  
automática  =  comenzar  (miMapa);  
const  auto  gravitationalAccelerationOnEarth  =  9.80665;  constexpr  suma  
automática  =  10  +  20  +  12;  auto  strings  =  { "El",  
"grande",  "marrón",  "zorro",  "salta",  "sobre",  "el",  "perezoso",  "perro" };  auto  númeroDeCadenas  =  cadenas.tamaño();

ARGUMENTO  DE  BUSQUEDA  DE  NOMBRE  DEPENDIENTE  (ADL)

La  búsqueda  dependiente  de  argumentos  (nombre)  (abreviatura:  ADL),  también  conocida  como  búsqueda  de  Koenig  
(llamada  así  por  el  científico  informático  estadounidense  Andrew  Koenig),  es  una  técnica  de  compilación  para  buscar  un  
nombre  de  función  no  calificado  (es  decir,  un  nombre  de  función  sin  un  prefijo).  calificador  de  espacio  de  nombres)  dependiendo  
de  los  tipos  de  argumentos  pasados  a  la  función  en  su  sitio  de  llamada.

Suponga  que  tiene  un  std::map<K,  T>  (definido  en  el  encabezado  <map>)  como  el  siguiente:

#include  <mapa>  
#include  <cadena>  
std::map<unsigned  int,  std::string>  palabras;

Debido  a  ADL,  no  es  necesario  especificar  el  espacio  de  nombres  estándar  si  usa  la  función  begin()  o  end()  para  recuperar  un  
iterador  del  contenedor.  Simplemente  puede  escribir:

iterador  de  palabras  automático  =  comenzar  (palabras);

El  compilador  no  solo  mira  el  ámbito  local,  sino  también  los  espacios  de  nombres  que  contienen  el  tipo  del  argumento  (en  
este  caso,  el  espacio  de  nombres  de  map<T>,  que  es  estándar).  Por  lo  tanto,  en  el  ejemplo  anterior,  el  compilador  encuentra  
una  función  begin()  adecuada  para  los  mapas  en  el  espacio  de  nombres  estándar.

En  algunos  casos,  debe  definir  explícitamente  el  espacio  de  nombres,  por  ejemplo,  si  desea  usar  std::begin()  y  
std::end()  con  una  matriz  de  estilo  C  simple.

103
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

A  primera  vista,  usar  auto  en  lugar  de  un  tipo  concreto  parece  ser  una  característica  conveniente.  Los  
desarrolladores  ya  no  están  obligados  a  recordar  el  nombre  de  un  tipo.  Simplemente  escriben  auto,  const  auto,  auto&  
(para  referencias)  o  const  auto&  (para  referencias  const),  y  el  compilador  hace  el  resto,  porque  conoce  el  tipo  del  valor  
asignado.  Por  supuesto,  la  deducción  automática  de  tipos  también  se  puede  usar  junto  con  constexpr  (consulte  
la  sección  sobre  cálculos  en  tiempo  de  compilación).
Por  favor,  no  tenga  miedo  de  usar  auto  (o  auto&  respectivamente  const  auto&)  tanto  como  sea  posible.  El  código  
todavía  se  escribe  estáticamente  y  los  tipos  de  las  variables  están  claramente  definidos.  Por  ejemplo,  el  tipo  de  cadenas  
variables  del  ejemplo  anterior  es  std::initializer_list<const  char*>,  el  tipo  de  numberOfStrings  es  std::initializer_list<const  
char*>::size_type.

STD::INITIALIZER_LIST<T>  [C++11]

En  días  anteriores  (antes  de  C++  11),  si  queríamos  inicializar  un  contenedor  de  biblioteca  estándar  usando  literales,  
teníamos  que  hacer  lo  siguiente:

std::vector<int>  integerSequence;  
secuencia_entera.push_back(14);  
secuencia_entera.push_back(33);  
secuencia_entera.push_back(69); // ...etcétera...

Desde  C++  11,  simplemente  podemos  hacerlo  de  esta  manera:

std::vector<int>  integerSequence  { 14,  33,  69,  104,  222,  534 };

La  razón  de  esto  es  que  std::vector<T>  tiene  un  constructor  sobrecargado  que  acepta  una  llamada  lista  de  
inicializadores  como  parámetro.  Una  lista  de  inicializadores  es  un  objeto  de  tipo  std::initializer_list<T>  (definido  
en  el  encabezado  <initializer_list>).

Una  instancia  de  tipo  std::initializer_list<T>  se  construye  automáticamente  cuando  usa  una  lista  de  literales  separados  
por  comas  que  están  rodeados  por  un  par  de  llaves,  lo  que  se  conoce  como  lista  de  inicio  entre  llaves.
Puede  equipar  sus  propias  clases  con  constructores  que  acepten  listas  de  inicializadores,  como  se  muestra  en  este  
ejemplo:

#include  <cadena>  
#include  <vector>

usando  WordList  =  std::vector<std::string>;

class  LexicalRepository  { público:  
explícito  
LexicalRepository(const  std::initializer_list<const  char*>&  words)  {
listaPalabras.insert(comienzo(ListaPalabras),  comienzo(palabras),  

fin(palabras)); } // ...

privado:
lista  de  palabras  lista  de  

palabras; };

104
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

int  main()  
{ LexicalRepository  repo  { "El",  "grande",  "marrón",  "zorro",  "salta",  "sobre",  "el",  "perezoso",  
"perro" }; // ...  devuelve  0; }

Nota:  ¡Esta  lista  de  inicializadores  no  debe  confundirse  con  una  clase  de  su  lista  de  inicializadores  de  miembros!

Desde  C++14,  también  se  admite  la  deducción  automática  del  tipo  de  devolución  para  funciones.  Esto  es  especialmente  útil  
cuando  un  tipo  de  devolución  tiene  un  nombre  difícil  de  recordar  o  imposible  de  pronunciar,  que  suele  ser  el  caso  cuando  se  trata  
de  tipos  de  datos  complejos  no  estándar  como  tipos  de  devolución.

auto  function()  
{ std::vector<std::map<std::pair<int,  double>,  int>>  returnValue; // ...llenar  'returnValue'  
con  datos...  return  returnValue; }

No  hemos  discutido  las  funciones  lambda  hasta  ahora  (se  discutirán  en  detalle  en  el  Capítulo  7),  pero
C++  11  y  versiones  posteriores  le  permiten  almacenar  expresiones  lambda  en  variables  con  nombre:

*
cuadrado  automático  =  [](int  x)  { retorno  x X; };

Tal  vez  te  estés  preguntando  ahora  esto:  bueno,  en  el  Capítulo  4  el  autor  nos  dijo  que  una  expresiva  y  buena
la  asignación  de  nombres  es  importante  para  la  legibilidad  del  código  y  debe  ser  un  objetivo  importante  para  todos  los  
programadores  profesionales.  El  mismo  autor  ahora  promueve  el  uso  de  la  palabra  clave  auto,  que  hace  más  difícil  reconocer  
rápidamente  el  tipo  de  una  variable  con  solo  leer  el  código.  ¿No  es  eso  una  contradicción?
Mi  respuesta  clara  es  esta:  ¡no,  todo  lo  contrario!  Aparte  de  unas  pocas  excepciones,  el  auto  puede  aumentar  la
legibilidad  del  código.  Veamos  las  siguientes  dos  alternativas  de  una  asignación  de  variable:

Listado  5­15.  ¿Cuál  de  las  siguientes  dos  versiones  preferirías?

//  1ra  versión:  sin  auto  
std::shared_ptr<controller::CreateMonthlyInvoicesController>  createMonthlyInvoicesController  =
std::make_shared<controller::CreateMonthlyInvoicesController>();

//  2da  versión:  con  auto:  auto  
createMonthlyInvoicesController  =  
std::make_shared<controller::CreateMonthlyInvoicesController>();

Desde  mi  punto  de  vista,  la  versión  que  usa  auto  es  más  fácil  de  leer.  No  hay  necesidad  de  repetir  el  tipo  
explícitamente,  porque  es  bastante  claro  a  partir  de  su  inicializador  qué  tipo  será  createMonthlyInvoicesController.  Por  cierto,  
repetir  el  tipo  explícito  también  sería  una  especie  de  violación  del  principio  DRY  (ver  Capítulo  3).  Y  si  piensa  en  la  
expresión  lambda  anterior  llamada  cuadrado,  cuyo  tipo  es  un  tipo  de  clase  único,  sin  nombre  y  sin  unión,  ¿cómo  se  puede  
definir  explícitamente  dicho  tipo?

105
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Entonces,  aquí  está  mi  consejo:

¡Si  no  oscurece  la  intención  de  su  código,  use  automático  siempre  que  sea  posible!

Cálculos  durante  el  tiempo  de  compilación  Los  fanáticos  de  la  computación  

de  alto  rendimiento  (HPC),  pero  también  los  desarrolladores  de  software  integrado  y  los  programadores  que  prefieren  usar  tablas  estáticas  y  
constantes  para  separar  datos  y  códigos,  desean  calcular  tanto  como  sea  posible  en  el  momento  de  la  compilación.  Las  razones  de  esto  
son  muy  fáciles  de  comprender:  todo  lo  que  se  puede  calcular  o  evaluar  en  tiempo  de  compilación  no  tiene  que  calcularse  o  evaluarse  
en  tiempo  de  ejecución.  En  otras  palabras:  el  cálculo  de  tanto  como  sea  posible  en  el  momento  de  la  compilación  es  una  tarea  fácil  
para  aumentar  la  eficiencia  del  tiempo  de  ejecución  de  su  programa.  Esta  ventaja  a  veces  va  acompañada  de  un  inconveniente,  que  es  el  
tiempo  más  o  menos  creciente  que  se  tarda  en  compilar  nuestro  código.

Desde  C++11  existe  el  especificador  constexpr  (expresión  constante)  para  definir  que  es  posible  evaluar  el  valor  de  una  función  
o  una  variable  en  tiempo  de  compilación.  Y  con  el  estándar  posterior  C++14,  se  eliminaron  algunas  de  las  estrictas  restricciones  para  
constexpr  que  existían  antes.  Por  ejemplo,  se  permitía  que  una  función  especificada  por  constexpr  tuviera  exactamente  una  sola  declaración  
de  retorno.  Esta  restricción  ha  sido  abolida  desde  C++14.

Uno  de  los  ejemplos  más  simples  es  que  el  valor  de  una  variable  se  calcula  a  partir  de  literales  mediante  operaciones  
aritméticas  en  tiempo  de  compilación,  así:

constexpr  int  laRespuestaATodasLasPreguntas  =  10  +  20  +  12;

La  variable  theAnswerToAllQuestions  también  es  una  constante  como  si  hubiera  sido  declarada  con  const;  así  tú
no  puede  manipularlo  durante  el  tiempo  de  ejecución:

int  principal()  { // ...

la  respuesta  a  todas  las  preguntas  =  23; //  Error  del  compilador:  ¡asignación  de  variable  de  solo  lectura!  devolver  0; }

También  hay  funciones  constexpr:

constexpr  int  multiplicar(const  int  multiplicador,  const  int  multiplicando)  { return  multiplicador  *  multiplicando; }

Estas  funciones  se  pueden  llamar  en  tiempo  de  compilación,  pero  también  se  usan  como  funciones  ordinarias  con  argumentos  no  
constantes  en  tiempo  de  ejecución.  Esto  ya  es  necesario  por  la  razón  de  probar  esas  funciones  con  la  ayuda  de  Unit  Tests  (ver  Capítulo  2).

constexpr  int  theAnswerToAllQuestions  =  multiplicar  (7,  6);

Como  era  de  esperar,  también  las  funciones  específicas  de  constexpr  se  pueden  llamar  recursivamente,  como  se  muestra  a  continuación
ejemplo  de  una  función  para  calcular  factoriales.

106
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Listado  5­16.  Cálculo  del  factorial  de  un  entero  no  negativo  'n'  en  tiempo  de  compilación

01  #incluye  <iostream>
02
03  constexpr  sin  signo  largo  largo  factorial(const  sin  signo  corto  n)  { *  factorial(n  ­  1) :  1;  04
devolver  n  >  1 ?  norte
05 }  06

07  int  main()  { número  
corto  sin  signo  =  6;  08  resultado  
09
automático1  =  factorial(número);  constexpr  auto  
10 resultado2  =  factorial(10);
11
"
12 std::cout  <<  "resultado1:  "  return   <<  resultado1  <<  ",  resultado2: <<  resultado2  <<  std::endl;
13 0;  14 }

El  ejemplo  anterior  ya  funciona  bajo  C++11.  La  función  factorial()  consta  de  una  sola  declaración,  y  la  recursividad  
se  permitió  desde  el  principio  en  las  funciones  constexpr.  La  función  main()  contiene  dos  llamadas  de  la  función  factorial().  
Vale  la  pena  echar  un  vistazo  más  de  cerca  a  estas  dos  llamadas  de  función.
La  primera  llamada  en  la  línea  no.  9  usa  el  número  de  variable  como  argumento  para  el  parámetro  de  la  función  n,  y  su  
resultado  se  asigna  a  una  variable  no  constante  resultado1.  La  segunda  llamada  de  función  en  la  línea  no.  10  utiliza  un  literal  
numérico  como  argumento  y  su  resultado  se  asigna  a  una  variable  con  un  especificador  constexpr.  La  diferencia  entre  
estas  dos  llamadas  de  función  en  tiempo  de  ejecución  se  puede  ver  mejor  en  el  código  objeto  desensamblado.  La  Figura  
5­3  muestra  el  código  de  objeto  en  nuestro  punto  clave  en  la  ventana  de  desmontaje  de  Eclipse  CDT.

Figura  5­3.  El  código  objeto  desensamblado

La  primera  llamada  de  función  en  la  línea  no.  9  da  como  resultado  cinco  instrucciones  de  máquina.  La  4ª  de  estas  instrucciones
(callq)  es  el  salto  a  la  función  factorial()  en  la  dirección  de  memoria  0x5555555549bd.  En  otras  palabras,  es  obvio  que  la  
función  se  llama  en  tiempo  de  ejecución.  En  contraste,  vemos  que  la  segunda  llamada  de  factorial()  en  la  línea  no.  10  da  
como  resultado  una  sola  instrucción  de  máquina  simple.  La  instrucción  movq  copia  una  palabra  cuádruple  del  operando  
de  origen  al  operando  de  destino.  No  hay  llamada  de  función  costosa  en  tiempo  de  ejecución.  El  resultado  de  
factorial(10),  que  es  0x375f00  en  hexadecimal  y  respectivamente  3.628.800  en  decimal,  ha  sido  calculado  en  tiempo  
de  compilación  y  está  disponible  como  una  constante  en  el  código  objeto.
Como  ya  he  escrito  anteriormente,  algunas  restricciones  para  las  funciones  específicas  de  contexto  en  C++11  se  
han  derogado  desde  C++14.  Por  ejemplo,  una  función  especificada  constexpr  ahora  puede  tener  más  de  una  declaración  
de  retorno,  puede  tener  condicionales  como  if­else­branches,  variables  locales  de  tipo  "literal"  o  bucles.
Básicamente,  casi  todas  las  declaraciones  de  C++  están  permitidas  si  no  presuponen  o  requieren  algo  que  solo  está  disponible  
en  el  contexto  de  un  entorno  de  tiempo  de  ejecución,  por  ejemplo,  asignar  memoria  en  el  montón  o  generar  excepciones.

107
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Plantillas  variables
Creo  que  es  menos  sorprendente  que  constexpr  también  se  pueda  usar  en  plantillas,  como  se  muestra  en  el  siguiente  ejemplo.

Listado  5­17.  Una  plantilla  variable  para  la  constante  matemática  pi

template  <typename  T>  
constexpr  T  pi  =  T(3.1415926535897932384626433L);

Esto  se  conoce  como  plantilla  variable  y  es  una  alternativa  buena  y  flexible  al  estilo  arcaico  de  definiciones  
constantes  mediante  el  uso  de  #define  para  macros  (consulte  la  sección  "Evitar  macros"  en  el  Capítulo  4).  Según  su  contexto  
de  uso  durante  la  instanciación  de  la  plantilla,  la  constante  matemática  pi  se  escribe  como  float,  double  o  long  double.

Listado  5­18.  Cálculo  de  la  circunferencia  de  un  círculo  en  tiempo  de  compilación  usando  la  plantilla  variable  'pi'

plantilla  <nombre  de  tipo  T>  
constexpr  T  computarCircunferencia(const  T  radio)  { pi<T>;
retorno  2  *  radio  *
}

int  main()  { const  
long  doble  radio  { 10.0L };  constexpr  larga  doble  
circunferencia  =  calcularCircunferencia(radio);  std::cout  <<  circunferencia  <<  std::endl;  devolver  
0; }

Por  último,  pero  no  menos  importante,  también  puede  usar  clases  en  los  cálculos  en  tiempo  de  compilación.  Puede  definir  constexpr
constructores  y  funciones  miembro  para  clases.

Listado  5­19.  Rectangle  es  una  clase  constexpr

#incluir  <iostream>  #incluir  
<cmath>

class  Rectangle  { public:  
constexpr  
Rectangle()  =  delete;  constexpr  
Rectangle(const  double  width,  const  double  height) :  ancho  { ancho },  alto  { alto }  { }  
constexpr  double  getWidth()  const  { return  ancho; }  
constexpr  double  getHeight()  const  { altura  de  retorno ; }  constexpr  
double  getArea()  const  { return  ancho  *  alto; }  constexpr  double  
getLengthOfDiagonal()  const  { return  std::sqrt(std::pow(ancho,  2.0)  +  std::pow(alto,  
2.0)); }

privado:  
doble  ancho;  
doble  altura; };

108
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

int  main()  
{ constexpr  Rectangle  americanFootballPlayingField  { 48.76,  110.0 };  constexpr  doble  área  =  
campo  de  juego  de  fútbol  americano.  getArea  ();  constexpr  doble  diagonal  =  
campodejuegodefútbolamericano.getLengthOfDiagonal();

"
std::cout  <<  "El  área  de  un  campo  de  fútbol  americano  es  el  área  <<  "m^2  y  la  longitud  de   <<
"
su  diagonal  es  <<  diagonal  <<  "m".  <<  estándar::endl;  devolver  0;

Además,  las  clases  constexpr  se  pueden  usar  tanto  en  tiempo  de  compilación  como  en  tiempo  de  ejecución.  Sin  
embargo,  a  diferencia  de  las  clases  ordinarias,  no  está  permitido  definir  funciones  miembro  virtuales  (no  hay  polimorfismo  en  tiempo  
de  compilación),  y  una  clase  constexpr  no  debe  tener  un  destructor  definido  explícitamente.

■  Nota  El  ejemplo  de  código  anterior  podría  fallar  al  compilar  en  algunos  compiladores  de  C++.  Según  los  estándares  
actuales,  el  estándar  C++  no  especifica  funciones  matemáticas  comunes  de  la  biblioteca  numérica  (encabezado  <cmath>)  
como  constexpr,  como  std::sqrt()  y  std::pow().  Las  implementaciones  del  compilador  son  libres  de  hacerlo  de  todos  modos,  
pero  no  es  un  requisito  obligatorio.

Sin  embargo,  ¿cómo  se  deberían  haber  juzgado  estos  cálculos  en  tiempo  de  compilación  a  partir  de  un  código  limpio?
¿perspectiva?  ¿Es  básicamente  una  buena  idea  agregar  constexpr  a  cualquier  cosa  que  pueda  tenerlo?
Bueno,  mi  opinión  es  que  constexpr  no  reduce  la  legibilidad  del  código.  El  especificador  siempre  está  delante  de  las  definiciones  
de  variables  y  constantes,  respectivamente  delante  de  las  declaraciones  de  funciones  o  métodos.  Por  lo  tanto,  no  molesta  tanto.  Por  
otro  lado,  si  definitivamente  sé  que  algo  nunca  se  evaluará  en  tiempo  de  compilación,  también  debería  renunciar  al  especificador.

No  permitir  un  comportamiento  indefinido

En  C++  (y  también  en  algunos  otros  lenguajes  de  programación),  la  especificación  del  lenguaje  no  define  el  comportamiento  
en  ninguna  situación  posible.  En  algunos  lugares,  la  especificación  dice  que  el  comportamiento  de  una  determinada  
operación  no  está  definido  en  determinadas  circunstancias.  En  tal  tipo  de  situación,  no  puede  predecir  lo  que  sucederá,  porque  
el  comportamiento  del  programa  depende  de  la  implementación  del  compilador,  el  sistema  operativo  subyacente  o  los  
interruptores  de  optimización  especiales.  ¡Es  realmente  malo!  El  programa  puede  fallar  o  generar  silenciosamente  resultados  
incorrectos.
Aquí  hay  un  ejemplo  de  comportamiento  indefinido,  un  uso  incorrecto  de  un  puntero  inteligente:

const  std::size_t  NUMBER_OF_STRINGS  { 100 };  
std::shared_ptr<std::string>  arrayOfStrings(new  std::string[NÚMERO_DE_CADENAS]>);

Supongamos  que  este  objeto  std::shared_ptr<T>  es  el  último  que  apunta  al  recurso  de  matriz  de  cadenas
y  se  queda  sin  alcance  en  alguna  parte,  ¿qué  pasará?
Respuesta:  El  destructor  de  std::shared_ptr<T>  disminuye  el  número  de  propietarios  compartidos  y  el  contador  llega  
a  cero.  Como  consecuencia,  el  recurso  administrado  por  el  puntero  inteligente  (la  matriz  de  std::string)  se  destruye  llamando  
a  su  destructor.  Pero  lo  hará  mal,  porque  cuando  asigna  el  recurso  administrado  usando  new[],  debe  llamar  al  formulario  de  
matriz  delete[],  y  no  eliminar,  para  liberar  el  recurso  y  el  eliminador  predeterminado  de  std::shared_ptr<T>  usa  eliminar.

109
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Eliminar  una  matriz  con  delete  en  lugar  de  delete[]  da  como  resultado  un  comportamiento  indefinido.  No  se  especifica  qué
sucede  Tal  vez  resulte  en  una  pérdida  de  memoria,  pero  eso  es  solo  una  suposición.

■  Precaución  ¡Evite  el  comportamiento  indefinido!  Es  un  grave  error  y  termina  con  programas  que  silenciosamente  se  comportan  mal.

Hay  varias  soluciones  para  permitir  que  el  puntero  inteligente  elimine  la  matriz  de  cadenas  correctamente.  Por  ejemplo  tu
puede  proporcionar  un  eliminador  personalizado  como  un  objeto  similar  a  una  función  (también  conocido  como  "Functor",  consulte  el  Capítulo  7):

template<  typename  Type  >  struct  
CustomArrayDeleter  { void  operator()

(Type  const*  pointer)  {

borrar  []  puntero; } };

Ahora  puede  usar  su  propio  eliminador  de  la  siguiente  manera:

const  std::size_t  NUMBER_OF_STRINGS  { 100 };  
std::shared_ptr<std::string>  arrayOfStrings(new  std::string[NUMBER_OF_STRINGS],  CustomArrayD  eleter<std::string>());

En  C++  11,  hay  un  eliminador  predeterminado  para  los  tipos  de  matriz  definidos  en  el  encabezado  <memoria>:

const  std::size_t  NUMBER_OF_STRINGS  { 100 };  
std::shared_ptr<std::string>  arrayOfStrings(new  std::string[NÚMERO_DE_CADENAS],  std::default_delete<std::string[]>());

Por  supuesto,  debe  tenerse  en  cuenta,  dependiendo  de  los  requisitos  a  cumplir,  si  el
el  uso  de  un  std::vector  no  siempre  es  la  mejor  solución  para  implementar  una  "matriz  de  cosas".

Programación  rica  en  tipos
No  confíes  en  los  nombres.

Tipos  de  confianza.

Los  tipos  no  mienten.

¡Los  tipos  son  tus  amigos!

—Mario  Fusco  (@mariofusco),  13  de  abril  de  2016,  en  Twitter

El  23  de  septiembre  de  1999,  la  NASA  perdió  su  Mars  Climate  Orbiter  I,  una  sonda  espacial  robótica,  después  de  un  viaje  de  10  
meses  al  cuarto  planeta  de  nuestro  Sistema  Solar.  Cuando  la  nave  espacial  entró  en  inserción  orbital,  la  transferencia  de  datos  importantes  
falló  entre  el  equipo  de  propulsión  de  Lockheed  Martin  Astronautics  en  Colorado  y  el  equipo  de  navegación  de  la  misión  de  la  NASA  
en  Pasadena  (California).  Este  error  empujó  a  la  nave  espacial  demasiado  cerca  de  la  atmósfera  de  Marte,  donde  se  quemó  
inmediatamente.

110
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Figura  5­4.  Representación  artística  del  Mars  Climate  Orbiter  (Autor:  NASA/JPL/Corby  Waste;  Licencia:  Dominio  público)

La  causa  de  la  transferencia  de  datos  fallida  fue  que  el  equipo  de  navegación  de  la  misión  de  la  NASA  usó  el  Sistema  Internacional  
de  Unidades  (SI),  mientras  que  el  software  de  navegación  de  Lockheed  Martin  usó  unidades  inglesas  (Sistema  de  Medición  Imperial).  
El  software  utilizado  por  el  equipo  de  navegación  de  la  misión  ha  enviado  valores  en  libras­fuerza­segundo  (lbf∙s),  pero  el  software  
de  navegación  del  Orbiter  esperaba  valores  en  newton­segundo  (N∙s).  La  pérdida  financiera  total  de  la  NASA  fue  de  328  millones  
de  dólares  estadounidenses.  El  trabajo  de  toda  una  vida  de  alrededor  de  200  buenos  ingenieros  de  naves  espaciales  fue  destruido  en  
unos  pocos  segundos.
Esta  falla  no  es  un  ejemplo  típico  de  un  simple  error  de  software.  Ambos  sistemas  por  sí  mismos  pueden  haber  funcionado  
correctamente.  Pero  revela  un  aspecto  interesante  en  el  desarrollo  de  software.  Parece  que  los  problemas  de  comunicación  y  
coordinación  entre  ambos  equipos  de  ingeniería  serán  la  razón  elemental  de  este  fallo.
Es  obvio:  ni  se  realizaron  pruebas  conjuntas  del  sistema  con  ambos  subsistemas,  ni  se  diseñaron  adecuadamente  las  interfaces  
entre  ambos  subsistemas.

La  gente  a  veces  comete  errores.  El  problema  aquí  no  fue  el  error,  fue  la  falla  de  la  ingeniería  de  
sistemas  de  la  NASA  y  los  controles  y  equilibrios  en  nuestros  procesos  para  detectar  el  error.  Por  
eso  perdimos  la  nave  espacial.

­Dr.  Edward  Weiler,  Administrador  Asociado  de  Ciencias  Espaciales  de  la  NASA  [JPL99]

De  hecho,  no  conozco  ningún  detalle  sobre  el  software  del  sistema  Mars  Climate  Orbiter.  Pero  según  el  informe  de  examen  de  
la  falla,  entendí  que  una  pieza  de  software  produjo  resultados  en  una  unidad  del  "sistema  inglés",  mientras  que  la  otra  pieza  de  software  
que  usó  esos  resultados  esperaba  que  estuvieran  en  unidades  métricas.

111
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Creo  que  todo  el  mundo  conoce  las  declaraciones  de  funciones  miembro  de  C++  que  se  parecen  a  las  de  la  siguiente  clase:

class  SpacecraftTrajectoryControl  { public:  void  

applyMomentumToSpacecraftBody(const  double  impulseValue); };

¿Qué  significa  el  doble?  ¿De  qué  unidad  es  el  valor  que  espera  la  función  miembro  denominada  
applyMomentumToSpacecraftBody?  ¿Es  un  valor  medido  en  newton  (N),  newton­segundo  (N∙s),  libra­fuerza­segundo  (lbf∙s)  o  
cualquier  otra  unidad?  De  hecho  no  lo  sabemos.  El  doble  puede  ser  cualquier  cosa.  Es,  por  supuesto,  un  tipo,  pero  no  es  un  tipo  
semántico.  Tal  vez  se  haya  documentado  en  alguna  parte,  o  podríamos  darle  al  parámetro  un  nombre  más  significativo  y  detallado  
como  impulseValueInNewtonSeconds,  que  sería  mejor  que  nada.  Pero  incluso  la  mejor  documentación  o  nombre  de  parámetro  no  
puede  garantizar  que  un  cliente  de  esta  clase  pase  un  valor  de  una  unidad  incorrecta  a  esta  función  miembro.

¿Podemos  hacerlo  mejor?  Por  supuesto  que  podemos.

Lo  que  realmente  queremos  tener  para  definir  una  interfaz  correctamente  y  rica  en  semántica,  es  algo  como  esto:

clase  SpacecraftTrajectoryControl  { public:  void  

applyMomentumToSpacecraftBody(const  Momentum&  impulseValue); };

En  mecánica,  la  cantidad  de  movimiento  se  mide  en  newton­segundo  (Ns).  Un  newton­segundo  (1  Ns)  es  la  fuerza
de  un  Newton  (que  es  1  kg  m/s2  en  unidades  básicas  del  SI)  actuando  sobre  un  cuerpo  (un  objeto  físico)  durante  un  segundo.
Para  usar  un  tipo  como  Momentum  en  lugar  del  doble  de  tipo  de  punto  flotante  inespecífico,  debemos  introducir  que
escriba  primero.  En  un  primer  paso  definimos  una  plantilla  que  se  puede  utilizar  para  representar  cantidades  físicas  sobre  la  base  
del  sistema  de  unidades  MKS.  La  abreviatura  MKS  significa  metro  (longitud),  kilogramo  (masa)  y  segundos  (tiempo).  Estas  tres  
unidades  fundamentales  se  pueden  utilizar  para  expresar  cualquier  medida  física  dada.

Listado  5­20.  Una  plantilla  de  clase  para  representar  unidades  MKS

template  <int  M,  int  K,  int  S>  struct  MksUnit  
{ enum  { metro  =  M,  
kilogramo  =  K,  segundo  =  S}; };

Además,  necesitamos  una  plantilla  de  clase  para  la  representación  de  valores:

Listado  5­21.  Una  plantilla  de  clase  para  representar  valores  de  unidades  MKS

plantilla  <typename  MksUnit>  clase  
Valor  { privado:  
largo  doble  
magnitud  { 0.0 };

público:  
Valor  explícito  (const  long  double  magnitud) :  magnitud(magnitud)  {}  long  double  getMagnitude()  const  
{
magnitud  de  retorno ; } };

112
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

A  continuación,  podemos  usar  ambas  plantillas  de  clase  para  definir  alias  de  tipo  para  cantidades  físicas  concretas.  Aquí  
hay  unos  ejemplos:

usando  DimensionlessQuantity  =  Value<MksUnit<0,  0,  0>>;  usando  Longitud  
=  Valor<MksUnit<1,  0,  0>>;  usando  Área  =  
Valor<MksUnit<2,  0,  0>>;  usando  Volumen  =  
Valor<MksUnit<3,  0,  0>>;  usando  Masa  =  
Valor<MksUnit<0,  1,  0>>;  usando  Tiempo  =  
Valor<MksUnit<0,  0,  1>>;  usando  Velocidad  =  
Valor<MksUnit<1,  0,  ­1>>;  usando  Aceleración  =  
Valor<MksUnit<1,  0,  ­2>>;  usando  Frecuencia  =  Valor<MksUnit<0,  
0,  ­1>>;  usando  Force  =  Value<MksUnit<1,  1,  ­2>>;  usando  
Presión  =  Valor<MksUnit<­1,  1,  ­2>>; // ...  etc. ...

Ahora  también  es  posible  definir  Momentum,  que  se  requiere  como  tipo  de  parámetro  para  nuestra  función  
miembro  applyMomentumToSpacecraftBody:

usando  Momentum  =  Value<MksUnit<1,  1,  ­1>>;

Después  de  haber  introducido  el  alias  de  tipo  Momentum,  el  siguiente  código  no  se  compilará  porque  no  hay  un  constructor  
adecuado  para  convertir  de  doble  a  Value<MksUnit<1,1,­1>>:

Control  de  control  de  trayectoria  de  la  nave  
espacial;  const  doble  algunValor  =  13.75;  
control.applyMomentumToSpacecraftBody(algúnValor); //  ¡Error  en  tiempo  de  compilación!

Incluso  el  siguiente  ejemplo  conducirá  a  errores  en  tiempo  de  compilación,  porque  una  variable  de  tipo  Force  no  debe  
usarse  como  Momentum,  y  debe  evitarse  una  conversión  implícita  entre  estas  diferentes  dimensiones:

Control  de  control  de  trayectoria  de  la  nave  
espacial;  Fuerza  fuerza  
{ 13.75 };  control.applyMomentumToSpacecraftBody(fuerza); //  ¡Error  en  tiempo  de  compilación!

Pero  esto  funcionará  bien:

Control  de  control  de  trayectoria  de  la  nave  
espacial;  Momento  impulso  { 13,75 };  
control.applyMomentumToSpacecraftBody(momentum);

Las  unidades  también  se  pueden  utilizar  para  la  definición  de  constantes.  Para  este  propósito,  necesitamos  modificar  
ligeramente  el  valor  de  la  plantilla  de  clase.  Agregamos  la  palabra  clave  constexpr  (consulte  la  sección  “Cálculos  durante  el  tiempo  
de  compilación”  en  el  Capítulo  4)  al  constructor  de  inicialización  y  la  función  miembro  getMagnitude().  Esto  nos  permite  no  solo  
crear  constantes  de  valor  en  tiempo  de  compilación  que  no  tienen  que  inicializarse  durante  el  tiempo  de  ejecución.  Como  
veremos  más  adelante,  ahora  también  podemos  realizar  cálculos  con  nuestros  valores  físicos  durante  el  tiempo  de  compilación.

template  <typename  MksUnit>  clase  
Valor  { público:  
constexpr  
valor  explícito  (const  long  doble  magnitud)  noexcept :  magnitud  { magnitud }  {}

113
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

constexpr  long  double  getMagnitude()  const  noexcept  { return  
magnitud; }

privado:  
largo  doble  magnitud  { 0.0 }; };

A  partir  de  entonces,  las  constantes  de  diferentes  unidades  físicas  se  pueden  definir  como  en  el  siguiente  ejemplo:

constexpr  Aceleración  gravitacionalAccelerationOnEarth  { 9.80665 };  constexpr  
Presión  standardPressureOnSeaLevel  { 1013.25 };  constexpr  
Velocidad  speedOfLight  { 299792458.0 };  constexpr  
Frecuencia  concertPitchA  { 440.0 };  constexpr  Mass  
neutronMass  { 1.6749286e­27 };

Además,  los  cálculos  entre  unidades  son  posibles  si  se  implementan  los  operadores  necesarios.
Por  ejemplo,  estas  son  las  plantillas  de  operadores  de  suma,  resta,  multiplicación  y  división  para  realizar  
diferentes  cálculos  con  dos  valores  de  diferentes  unidades  MKS:

template  <int  M,  int  K,  int  S>  
constexpr  Value<MksUnit<M,  K,  S>>  operador+  
(const  Value<MksUnit<M,  K,  S>>&  lhs,  const  Value<MksUnit<M,  K,  S  >>&  rhs)  noexcept  { return  
Value<MksUnit<M,  K,  S>>(lhs.getMagnitude()  +  rhs.getMagnitude()); }

template  <int  M,  int  K,  int  S>  
constexpr  Value<MksUnit<M,  K,  S>>  operador­  
(const  Value<MksUnit<M,  K,  S>>&  lhs,  const  Value<MksUnit<M,  K,  S>>&  rhs)  noexcept  { return  
Value<MksUnit<M,  K,  S>>(lhs.getMagnitude()  ­  rhs.getMagnitude()); }

template  <int  M1,  int  K1,  int  S1 ,  int  M2,  int  K2,  int  S2>  constexpr  
Value<MksUnit<M1  +  M2,  K1  +  K2,  S1  +  S2>>  operador*  (const  
Value<MksUnit<M1,  K1,  S1>>&  lhs,  const  Value<MksUnit<M2,  K2,  S2>>&  rhs)  noexcept  { return  
Value<MksUnit<M1  +  M2,  K1  +  K2,  S1  +  S2>>(lhs.getMagnitude()  *  rhs.  getMagnitud()); }

template  <int  M1,  int  K1,  int  S1 ,  int  M2,  int  K2,  int  S2>  constexpr  
Value<MksUnit<M1  ­  M2,  K1  ­  K2,  S1  ­  S2>>  operador/  (const  
Value<MksUnit<M1,  K1,  S1>>&  lhs,  const  Value<MksUnit<M2,  K2,  S2>>&  rhs)  noexcept  { return  
Value<MksUnit<M1  ­  M2,  K1  ­  K2,  S1  ­  S2>>(lhs.getMagnitude() /  rhs.  getMagnitud()); }

Ahora  podrás  escribir  algo  como  esto:

constexpr  Momentum  impulseValueForCourseCorrection  =  Force  { 30.0 }  *  Time  { 3.0 };  Control  de  
control  de  trayectoria  de  la  nave  espacial;  
control.applyMomentumToSpacecraftBody(impulseValueForCourseCorrection);

114
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Eso  es  obviamente  una  mejora  significativa  sobre  la  multiplicación  de  dos  dobles  sin  sentido  y  la  asignación  de  su  
resultado  a  otro  doble  sin  sentido.  Es  bastante  expresivo.  Y  es  más  seguro,  porque  no  podrás  asignar  el  resultado  de  la  multiplicación  
a  algo  diferente  a  una  variable  de  tipo  Momentum.
Y  la  mejor  parte  es  esta:  ¡ la  seguridad  de  tipos  está  garantizada  durante  el  tiempo  de  compilación!  No  hay  gastos  generales  durante
tiempo  de  ejecución,  porque  un  compilador  compatible  con  C++  11  (y  superior)  puede  realizar  todas  las  comprobaciones  de  compatibilidad  de  tipos  
necesarias.

Vayamos  un  paso  más  allá.  ¿No  sería  muy  conveniente  e  intuitivo  si  pudiéramos  escribir  algo  como  lo  siguiente?

constexpr  Aceleración  gravitacionalAccelerationOnEarth  =  9.80665_ms2;

Incluso  eso  es  posible  con  C++  moderno.  Desde  C++  11  podemos  proporcionar  sufijos  personalizados  para  literales  por
definiendo  funciones  especiales,  los  llamados  operadores  literales ,  para  ellos:

constexpr  Operador  de  fuerza  ""  _N(magnitud  doble  larga )  { return  
Force(magnitud); }

constexpr  Operador  de  aceleración  ""  _ms2( magnitud  doble  larga )  { return  
Aceleración(magnitud); }

constexpr  Operador  de  tiempo  ""  _s(magnitud  doble  larga )  { return  
Tiempo(magnitud); }

constexpr  Operador  Momentum  ""  _Ns( magnitud  doble  larga)  { return  
Momentum(magnitud); }

// ...más  operadores  literales  aquí...

LITERALES  DEFINIDOS  POR  EL  USUARIO  [C++11]

Básicamente,  un  literal  es  una  constante  de  tiempo  de  compilación  cuyo  valor  se  especifica  en  el  archivo  fuente.  Desde  C++  11,  
los  desarrolladores  pueden  producir  objetos  de  tipos  definidos  por  el  usuario  definiendo  sufijos  definidos  por  el  usuario  para  los  
literales.  Por  ejemplo,  si  se  debe  inicializar  una  constante  con  un  literal  de  US­$  145,67,  esto  se  puede  hacer  escribiendo  la  
siguiente  expresión:

constexpr  Cantidad  de  dinero  =  145.67_USD;

En  este  caso,  “_USD”  es  el  sufijo  definido  por  el  usuario  para  los  literales  de  coma  flotante  que  representan  cantidades  de  dinero.
Para  que  se  pueda  utilizar  dicho  literal  definido  por  el  usuario,  se  debe  definir  una  función  que  se  conoce  como  operador  
literal:

constexpr  Operador  de  dinero  ""  _USD  (cantidad  doble  constante  larga )  {
devolver  dinero  (cantidad); }

115
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Una  vez  que  hemos  definido  los  literales  definidos  por  el  usuario  para  nuestras  unidades  físicas,  podemos  trabajar  con  ellos  de  la  siguiente  manera
manera:

Fuerza  fuerza  =  30.0_N;  Tiempo  
tiempo  =  3.0_s;  cantidad  de  
movimiento  cantidad  de  movimiento  =  fuerza  *  tiempo;

Esta  notación  no  solo  es  familiar  para  los  físicos  y  otros  científicos.  Es  aún  más  seguro.  Con  la  programación  rica  en  tipos  y  los  literales  
definidos  por  el  usuario,  está  protegido  contra  la  asignación  de  un  literal  que  exprese  un  valor  de  segundos  a  una  variable  de  tipo  Force.

Fuerza  fuerza1  =  3,0; //  ¡Error  en  tiempo  de  compilación!
Fuerza  fuerza2  =  3.0_s; //  ¡Error  en  tiempo  de  compilación!
Fuerza  fuerza3  =  3.0_N; //  ¡Obras!

Por  supuesto,  también  es  posible  utilizar  literales  definidos  por  el  usuario  junto  con  la  deducción  automática  de  tipos  y/o  expresiones  constantes:

fuerza  automática  =  3.0_N;  
aceleración  automática  constexpr  =  100.0_ms2;

Eso  es  bastante  conveniente  y  bastante  elegante,  ¿no?  Entonces,  aquí  está  mi  consejo  para  el  diseño  de  interfaz  pública:

Cree  interfaces  (API)  fuertemente  tipadas.

En  otras  palabras:  debe  evitar  en  gran  medida  los  tipos  integrados  generales  de  bajo  nivel,  como  int,  double  o,  en  el  peor  de  los  casos,  
void*,  en  las  interfaces  públicas,  respectivamente,  las  API.  Estos  tipos  no  semánticos  son  peligrosos  en  determinadas  circunstancias,  porque  pueden  
representar  casi  cualquier  cosa.

■  Sugerencia  Ya  hay  disponibles  algunas  bibliotecas  basadas  en  plantillas  que  proporcionan  tipos  para  cantidades  físicas,  
incluidas  todas  las  unidades  SI.  Un  ejemplo  muy  conocido  es  Boost.Units  (parte  de  Boost  desde  la  versión  1.36.0;  
consulte  http://www.boost.org).

Conozca  sus  bibliotecas
¿Alguna  vez  has  oído  hablar  del  síndrome  “No  inventado  aquí” (NIH)?  Es  un  antipatrón  organizacional.
El  síndrome  NIH  es  un  término  despectivo  para  una  postura  en  muchas  organizaciones  de  desarrollo  que  describe  el  desconocimiento  del  conocimiento  
existente  o  soluciones  probadas  basadas  en  su  lugar  de  origen.  Es  una  forma  de  "reinventar  la  rueda",  es  decir,  volver  a  implementar  algo  (una  
biblioteca  o  un  marco)  que  ya  está  disponible  en  algún  lugar  de  bastante  alta  calidad.  El  razonamiento  detrás  de  esta  actitud  suele  ser  la  creencia  
de  que  los  desarrollos  internos  deben  ser  mejores  en  varios  aspectos.  A  menudo  se  las  considera  erróneamente  más  baratas,  más  seguras,  más  
flexibles  y  más  controlables  que  las  soluciones  existentes  y  bien  establecidas.

116
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

De  hecho,  solo  unas  pocas  empresas  logran  desarrollar  una  alternativa  verdaderamente  equivalente,  o  incluso  
mejor,  a  una  solución  que  ya  existe  en  el  mercado.  A  menudo,  el  enorme  esfuerzo  de  tales  desarrollos  no  justifica  el  
escaso  beneficio.  Y  no  es  raro  que  la  biblioteca  o  el  marco  de  desarrollo  propio  sean  claramente  peores  en  calidad  
en  comparación  con  las  soluciones  existentes  y  maduras  que  ya  existen  desde  hace  años.
Durante  las  últimas  décadas,  han  surgido  muchas  bibliotecas  y  marcos  excelentes  en  el  entorno  C++.
Estas  soluciones  tuvieron  la  oportunidad  de  madurar  durante  mucho  tiempo  y  se  han  utilizado  con  éxito  en  decenas  
de  miles  de  proyectos.  No  hay  necesidad  de  reinventar  la  rueda.  Los  buenos  artesanos  del  software  deben  conocer  
estas  bibliotecas.  No  es  necesario  conocer  cada  pequeño  detalle  sobre  estas  bibliotecas  y  sus  API.  Sin  embargo,  es  bueno  
saber  que  ya  existen  soluciones  probadas  para  ciertos  campos  de  aplicación,  que  vale  la  pena  considerar  para  tener  
una  selección  más  limitada  para  su  proyecto  de  desarrollo  de  software.

Aproveche  el  <algoritmo>

Si  desea  mejorar  la  calidad  del  código  en  su  organización,  reemplace  todas  sus  pautas  de  
codificación  con  un  objetivo:  ¡No  bucles  sin  procesar!

—Sean  Parent,  arquitecto  de  software  principal  de  Adobe,  en  CppCon  2013

Jugar  con  colecciones  de  elementos  es  una  actividad  cotidiana  en  la  programación.  Independientemente  de  si  
estamos  tratando  con  colecciones  de  datos  de  medición,  con  correos  electrónicos,  cadenas,  registros  de  una  base  
de  datos  u  otros  elementos,  el  software  debe  filtrarlos,  clasificarlos,  eliminarlos,  manipularlos  y  más.
En  muchos  programas  podemos  encontrar  "bucles  en  bruto" (por  ejemplo,  bucles  for  o  while  hechos  a  mano)  
para  visitar  algunos  o  todos  los  elementos  en  un  contenedor,  o  secuencia,  para  hacer  algo  con  ellos.  Un  ejemplo  simple  es  
invertir  un  orden  de  enteros  que  se  almacenan  en  un  std::vector  de  esta  manera:

#incluir  <vector>

std::vector<int>  enteros  { 2,  5,  8,  22,  45,  67,  99 };

// ...en  algún  lugar  del  programa:  
std::size_t  leftIndex  =  0;  std::size_t  
rightIndex  =  integers.size()  ­  1;

while  (índiceizquierdo  <índicederecho)  {
int  buffer  =  enteros[rightIndex];  
enteros[índicederecho]  =  enteros[índiceizquierdo];  
enteros[índiceizquierdo]  =  búfer;  +
+índiceizquierdo;  
­­índicederecho; }

Básicamente,  este  código  funcionará.  Pero  tiene  varias  desventajas.  Es  difícil  ver  inmediatamente  qué
esta  pieza  de  código  está  haciendo  (de  hecho,  las  tres  primeras  líneas  dentro  del  bucle  while  podrían  sustituirse  
por  std::swap  from  header  <utility>).  Además,  escribir  código  de  esta  manera  es  muy  tedioso  y  propenso  a  errores.
Solo  imagine  que,  por  cualquier  motivo,  violamos  los  límites  del  vector  e  intentamos  acceder  a  un  elemento  en  una  
posición  fuera  de  rango.  A  diferencia  de  la  función  miembro  std::vector::at(),  std::vector::operator[]  no  genera  una  
excepción  std::out_of_range  entonces.  Conducirá  a  un  comportamiento  indefinido.
La  biblioteca  estándar  de  C++  proporciona  más  de  100  algoritmos  útiles  que  se  pueden  aplicar  a  los  contenedores.
o  secuencias  para  buscar,  contar  y  manipular  elementos.  Se  recogen  en  la  cabecera  <algoritmo>.

117
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Por  ejemplo,  para  invertir  el  orden  de  los  elementos  en  cualquier  tipo  de  contenedor  de  biblioteca  estándar,  por  ejemplo,  en
un  std::vector,  simplemente  podemos  usar  std::reverse:

#include  <algoritmo>  
#include  <vector>

std::vector<int>  enteros  =  { 2,  5,  8,  22,  45,  67,  99 }; // ...en  algún  lugar  del  
programa:  std::reverse(std::begin(integers),  
std::end(integers)); //  El  contenido  de  'enteros'  ahora  es:  99,  67,  45,  22,  8,  5,  2

A  diferencia  de  nuestra  solución  autoescrita  anterior,  este  código  no  solo  es  mucho  más  compacto,  menos  propenso  
a  errores  y  más  fácil  de  leer.  Dado  que  std::reverse  es  una  plantilla  de  función  (al  igual  que  todos  los  demás  algoritmos),  es  
universalmente  aplicable  a  todos  los  contenedores  de  secuencias  de  la  biblioteca  estándar,  contenedores  asociativos,  
contenedores  asociativos  desordenados,  std::string  y  también  matrices  primitivas  (que,  por  cierto, ,  ya  no  debe  usarse  en  
un  programa  C++  moderno;  consulte  la  sección  "Preferir  contenedores  de  biblioteca  estándar  sobre  matrices  simples  de  
estilo  C"  en  el  Capítulo  4).

Listado  5­22.  Aplicando  std::reverse  a  una  matriz  de  estilo  C  y  una  cadena

#include  <algoritmo>  
#include  <cadena>

//  Funciona,  pero  las  matrices  primitivas  no  deben  usarse  en  un  programa  C++  moderno  int  integers[]  =  
{ 2,  5,  8,  22,  45,  67,  99 };  std::reverse(std::begin(enteros),  
std::end(enteros));

std::string  text  { "¡El  gran  zorro  marrón  salta  sobre  el  perro  perezoso!" };  
std::reverse(std::begin(texto),  std::end(texto)); //  El  contenido  del  
'texto'  es  ahora:  "!god  yzal  eht  revo  spmuj  xof  nworb  gib  ehT"

El  algoritmo  inverso  se  puede  aplicar,  por  supuesto,  también  a  sub­rangos  de  un  contenedor  o  secuencia:

Listado  5­23.  Solo  se  invierte  una  subárea  de  la  cadena

std::string  text  { "¡El  gran  zorro  marrón  salta  sobre  el  perro  perezoso!" };  
std::reverse(std::begin(texto)  +  13,  std::end(texto)  ­  9); //  El  contenido  del  'texto'  
es  ahora:  "¡El  gran  perro  marrón  eht  revo  spmuj  xof  lazy  dog!"

Paralelización  más  fácil  de  algoritmos  desde  C++17

Tu  almuerzo  gratis  pronto  terminará.

—Hierba  Sutter  [Sutter05]

La  cita  anterior,  dirigida  a  desarrolladores  de  software  de  todo  el  mundo,  está  tomada  de  un  artículo  publicado  por  Herb  
Sutter,  miembro  del  comité  de  estandarización  de  ISO  C++  en  ese  momento,  en  2005.  Fue  en  un  momento  en  que  las  
velocidades  de  reloj  de  los  procesadores  dejó  de  aumentar  año  tras  año.  En  otras  palabras,  la  velocidad  de  procesamiento  
en  serie  ha  alcanzado  un  límite  físico.  En  cambio,  los  procesadores  estaban  cada  vez  más  equipados  con  más

118
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

núcleos  Este  desarrollo  en  las  arquitecturas  de  procesador  lleva  a  una  grave  consecuencia:  los  desarrolladores  ya  no  pueden  
aprovechar  el  rendimiento  cada  vez  mayor  del  procesador  por  frecuencias  de  reloj  (el  "almuerzo  gratis"  del  que  hablaba  
Herb),  sino  que  se  verán  obligados  a  desarrollar  programas  masivos  de  subprocesos  múltiples  como  un  manera  de  
utilizar  mejor  los  procesadores  multinúcleo  modernos.  Como  resultado,  los  desarrolladores  y  arquitectos  de  software  ahora  
deben  considerar  la  paralelización  en  su  diseño  y  arquitectura  de  software.
Antes  de  la  llegada  de  C++11,  el  estándar  C++  solo  admitía  la  programación  de  un  solo  subproceso  y  usted
tiene  que  usar  bibliotecas  de  terceros  (p.  ej.,  Boost.Thread)  o  extensiones  de  compilador  (p.  ej.,  Open  Multi­Processing  
(OpenMP))  para  paralelizar  sus  programas.  Desde  C  ++  11,  la  llamada  Biblioteca  de  soporte  de  subprocesos  está  disponible  
para  admitir  la  programación  paralela  y  multiproceso.  Esta  extensión  de  la  Biblioteca  estándar  ha  introducido  subprocesos,  
exclusiones  mutuas,  variables  de  condición  y  futuros.
Paralelizar  una  sección  de  código  requiere  un  buen  conocimiento  del  problema  y  debe  ser  considerado  en  el  
diseño  del  software  en  consecuencia.  De  lo  contrario,  pueden  ocurrir  errores  sutiles  causados  por  condiciones  de  carrera  
que  podrían  ser  muy  difíciles  de  depurar.  Especialmente  para  los  algoritmos  de  la  biblioteca  estándar,  que  a  menudo  
tienen  que  operar  en  contenedores  llenos  de  una  gran  cantidad  de  objetos,  la  paralelización  debe  simplificarse  para  los  
desarrolladores  a  fin  de  aprovechar  los  modernos  procesadores  multinúcleo  de  la  actualidad.
A  partir  de  C++17,  partes  de  la  Biblioteca  estándar  se  han  rediseñado  de  acuerdo  con  la  Especificación  técnica  para  
extensiones  de  C++  para  paralelismo  (ISO/IEC  TS  19570:2015),  también  conocida  como  Paralelismo  TS  (TS  =  
especificación  técnica)  en  resumen.  En  otras  palabras,  con  C++17  estas  extensiones  se  convirtieron  en  parte  del  estándar  
principal  ISO  C++.  Su  objetivo  principal  es  aliviar  un  poco  a  los  desarrolladores  de  la  compleja  tarea  de  jugar  con  las  
funciones  de  lenguaje  de  bajo  nivel  de  la  biblioteca  de  soporte  de  subprocesos,  como  std::thread,  std::mutex,  etc.

De  hecho,  eso  significa  que  69  algoritmos  bien  conocidos  estaban  sobrecargados  y  ahora  también  están  disponibles  
en  una  o  más  versiones  que  aceptan  un  parámetro  de  plantilla  adicional  para  la  paralelización  llamado  ExecutionPolicy  (ver  
barra  lateral).  Algunos  de  estos  algoritmos  son,  por  ejemplo,  std::for_each,  std::transform,  std::copy_if  o  std::sort.  
Además,  se  han  añadido  siete  nuevos  algoritmos  que  también  se  pueden  paralelizar,  como  std::reduce,  
std::exclusive_scan  o  std::transform_reduce.  Estos  nuevos  algoritmos  son  particularmente  útiles  en  la  programación  
funcional,  razón  por  la  cual  los  analizaré  más  adelante  en  el  Capítulo  7.

POLÍTICAS  DE  EJECUCIÓN  [C++17]

La  mayoría  de  las  plantillas  de  algoritmos  del  encabezado  <algorithm>  se  han  sobrecargado  y  ahora  también  
están  disponibles  en  una  versión  paralelizable.  Por  ejemplo,  además  de  la  plantilla  ya  existente  para  la  función  
std::find,  se  ha  definido  otra  versión  que  toma  un  parámetro  de  plantilla  adicional  para  especificar  la  política  de  
ejecución:

//  Versión  estándar  (un  solo  subproceso):  template<  
class  InputIt,  class  T  >
InputIt  find( InputIt  primero,  InputIt  último,  const  T&  value );
//  Versión  adicional  con  política  de  ejecución  definible  por  el  usuario  (desde  C++17):  template<  class  
ExecutionPolicy,  class  ForwardIt,  class  T  >
ForwardIt  find(ExecutionPolicy&&  policy,  ForwardIt  first,  ForwardIt  last,  const  T&  value);

Las  tres  etiquetas  de  política  estándar  que  están  disponibles  para  el  parámetro  de  plantilla  ExecutionPolicy  son:

•  std::execution::seq :  un  tipo  de  política  de  ejecución  que  define  que  un  paralelo
la  ejecución  del  algoritmo  puede  ser  secuencial.  Por  lo  tanto,  es  más  o  menos  lo  mismo  que  
usaría  la  versión  estándar  de  subproceso  único  de  la  función  de  plantilla  de  algoritmo  sin  
una  política  de  ejecución.

119
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

•  std::execution::par :  un  tipo  de  política  de  ejecución  que  define  que  un  paralelo
la  ejecución  del  algoritmo  puede  ser  paralelizada.  Permite  la  implementación  para  ejecutar  el  
algoritmo  en  múltiples  subprocesos.  Importante:  ¡ Los  algoritmos  paralelos  no  protegen  
automáticamente  contra  carreras  de  datos  críticos  o  interbloqueos!  Usted  es  responsable  de  
asegurarse  de  que  no  se  produzcan  condiciones  de  carrera  de  datos  mientras  ejecuta  la  función.

•  std::execution::par_unseq :  un  tipo  de  política  de  ejecución  que  define  que  un  paralelo
la  ejecución  del  algoritmo  puede  ser  vectorizada  y  paralelizada.  La  vectorización  aprovecha  el  conjunto  
de  comandos  SIMD  (instrucción  única,  datos  múltiples)  de  las  CPU  modernas.  SIMD  significa  
que  un  procesador  puede  realizar  la  misma  operación  en  múltiples  puntos  de  datos  
simultáneamente.

Por  supuesto,  no  tiene  absolutamente  ningún  sentido  ordenar  un  vector  pequeño  con  algunos  elementos  en  
paralelo.  La  sobrecarga  de  la  gestión  de  subprocesos  sería  mucho  mayor  que  la  ganancia  en  el  rendimiento.  
Por  lo  tanto,  una  política  de  ejecución  también  se  puede  seleccionar  dinámicamente  durante  el  tiempo  de  ejecución,  por  
ejemplo,  teniendo  en  cuenta  el  tamaño  del  vector.  Lamentablemente,  la  política  de  ejecución  dinámica  aún  no  se  ha  
aceptado  para  el  estándar  C++17.  Ahora  está  planificado  para  el  próximo  estándar  C++20.

Una  discusión  completa  de  todos  los  algoritmos  disponibles  está  mucho  más  allá  del  alcance  de  este  libro.  Pero  después  
de  esta  breve  introducción  al  encabezado  <algoritmo>  y  las  nuevas  posibilidades  de  paralelización  con  C++17,  echemos  un  vistazo  
a  algunos  ejemplos  de  lo  que  se  puede  hacer  con  los  algoritmos.

Clasificación  y  salida  de  un  contenedor
El  siguiente  ejemplo  usa  dos  plantillas  del  encabezado  <algoritmo>:  std::sort  y  std::for_each.
Internamente,  std::sort  utiliza  el  algoritmo  quicksort.  Por  defecto,  las  comparaciones  dentro  de  std::sort  se  
realizan  con  la  función  operator<  de  los  elementos.  Esto  significa  que  si  desea  ordenar  una  secuencia  de  
instancias  de  una  de  sus  propias  clases,  debe  asegurarse  de  que  operator<  esté  implementado  correctamente  
en  ese  tipo.

Listado  5­24.  Ordenar  un  vector  de  cadenas  e  imprimirlas  en  stdout

#include  <algoritmo>  
#include  <iostream>  
#include  <cadena>  
#include  <vector>

void  printCommaSeparated(const  std::string&  text)  {
std::cout  <<  texto  <<  ",  "; }

int  main()  
{ std::vector<std::string>  nombres  =  { "Peter",  "Harry",  "Julia",  "Marc",  "Antonio",  "Glenn" };  
std::sort(std::begin(nombres),  std::end(nombres));  
std::for_each(std::begin(nombres),  std::end(nombres),  printCommaSeparated);  devolver  
0; }

120
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Comparación  de  dos  secuencias  El  
siguiente  ejemplo  compara  dos  secuencias  de  cadenas  utilizando  std::equal.

Listado  5­25.  Comparando  dos  secuencias  de  cadenas

#include  <algoritmo>  
#include  <iostream>  
#include  <cadena>  
#include  <vector>

int  main()  
{ const  std::vector<std::string>  nombres1  { "Peter",  "Harry",  "Julia",  "Marc",  "Antonio",
"Glenn" };
const  std::vector<std::string>  nombres2  { "Pedro",  "Harry",  "Julia",  "Juan",  "Antonio",
"Glenn" };

const  bool  isEqual  =  std::equal(std::begin(names1),  std::end(names1),  std::begin(names2),  std::end(names2));

if  (isEqual)  
{ std::cout  <<  "El  contenido  de  ambas  secuencias  es  igual.\n"; }  else  
{ std::cout  
<<  "Los  contenidos  de  ambas  secuencias  difieren.\n"; }  devuelve  0;

Por  defecto,  std::equal  compara  elementos  usando  operator==.  Pero  puedes  definir  "igualdad"  como  tú
desear.  La  comparación  estándar  se  puede  reemplazar  por  una  operación  de  comparación  personalizada:

Listado  5­26.  Comparación  de  dos  secuencias  de  cadenas  mediante  una  función  de  predicado  personalizada

#include  <algoritmo>  
#include  <iostream>  
#include  <cadena>  
#include  <vector>

bool  compareFirstThreeCharactersOnly(const  std::string&  string1,
const  std::string&  string2)  { return  
(string1.compare(0,  3,  string2,  0,  3)  ==  0); }

int  main()  
{ const  std::vector<std::string>  nombres1  { "Peter",  "Harry",  "Julia",  "Marc",  "Antonio",
"Glenn" };
const  std::vector<std::string>  nombres2  { "Pedro",  "Harold",  "Julia",  "María",  "Antonio",
"Glenn" };

121
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

const  bool  isEqual  =  std::equal(std::begin(names1),  std::end(names1),  std::begin(names2),
std::end(nombres2),  compareFirstThreeCharactersOnly);

if  (isEqual)  
{ std::cout  <<  "Los  primeros  tres  caracteres  de  todas  las  cadenas  en  ambas  secuencias  son  iguales.\n"; }  else  
{ std::cout  
<<  "Los  primeros  tres  caracteres  de  todas  las  cadenas  en  ambas  secuencias  difieren.\n"; }  devuelve  0;

Si  no  se  requiere  reutilización  para  la  función  de  comparación  compareFirstThreeCharactersOnly(),  la  línea  
anterior  en  la  que  se  lleva  a  cabo  la  comparación  también  se  puede  implementar  usando  una  expresión  lambda  
(Discutiremos  las  expresiones  lamda  con  más  detalle  en  el  Capítulo  7),  así :

//  Compara  solo  los  tres  primeros  caracteres  de  cada  cadena  para  determinar  la  igualdad:  const  bool  
isEqual  =
std::equal(std::begin(nombres1),  std::end(nombres1),  std::begin(nombres2),  std::end(nombres2),  []( const  auto&  
string1,  const  auto&  string2)  {
return  (cadena1.compare(0,  3,  cadena2,  0,  3)  ==  0); });

Esta  alternativa  puede  parecer  más  compacta,  pero  no  necesariamente  contribuye  a  la  legibilidad  del  código.  
La  función  explícita  compareFirstThreeCharactersOnly()  tiene  un  nombre  semántico  que  expresa  muy  claramente  lo  
que  se  compara  (no  el  Cómo;  consulte  la  sección  "Usar  nombres  que  revelan  la  intención"  en  el  Capítulo  4).  Lo  que  se  
compara  exactamente  no  necesariamente  se  puede  ver  a  primera  vista  en  la  versión  con  la  expresión  lambda.
Siempre  tenga  en  cuenta  que  la  legibilidad  de  nuestro  código  debe  ser  uno  de  nuestros  primeros  objetivos.  Además,  siempre  
tenga  en  cuenta  que  los  comentarios  del  código  fuente  son  básicamente  un  olor  a  código  y  no  son  adecuados  para  explicar  el  
código  difícil  de  leer  (recuerde  la  sección  sobre  Comentarios  en  el  Capítulo  4 ).

Aproveche  Boost  No  puedo  dar  una  

introducción  amplia  a  la  famosa  biblioteca  de  Boost  (http://www.boost.org,  distribuido  bajo  la  licencia  de  software  
de  Boost,  versión  1.0)  aquí.  La  biblioteca  (de  hecho,  es  una  biblioteca  de  bibliotecas)  es  demasiado  grande  y  
poderosa,  y  discutirla  en  detalle  está  más  allá  del  alcance  de  este  libro.  Además,  hay  numerosos  buenos  libros  y  
tutoriales  sobre  Boost.
Pero  creo  que  es  muy  importante  conocer  esta  biblioteca  y  su  contenido.  Muchos  problemas  y
Los  desafíos  a  los  que  se  enfrentan  los  desarrolladores  de  C++  en  su  trabajo  diario  se  pueden  resolver  bastante  bien  con  las  
bibliotecas  de  Boost.
Más  allá  de  eso,  Boost  es  una  especie  de  "incubadora"  para  varias  bibliotecas  que  a  veces  se  aceptan  
para  formar  parte  del  estándar  del  lenguaje  C++,  si  tienen  un  cierto  nivel  de  madurez.  Ojo:  ¡eso  no  significa  
necesariamente  que  sean  totalmente  compatibles!  Por  ejemplo,  std::thread  (parte  del  estándar  desde  C++11)  
es  parcialmente  igual  a  Boost.Thread,  pero  hay  algunas  diferencias.  Por  ejemplo,  la  implementación  de  Boost  
admite  la  cancelación  de  subprocesos,  los  subprocesos  de  C++  11  no.  Por  otro  lado,  C++11  admite  std::async,  
pero  Boost  no.
Desde  mi  perspectiva,  vale  la  pena  conocer  las  bibliotecas  de  Boost  y  recordar  cuando  tienes  un
problema  adecuado  que  puede  ser  resuelto  adecuadamente  por  ellos.

122
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Más  bibliotecas  que  debe  conocer
Además  de  los  contenedores  de  la  biblioteca  estándar,  <algorithm>  y  Boost,  existen  algunas  bibliotecas  más  que  puede  tener  
en  cuenta  al  escribir  su  código.  Aquí  hay  una  lista  de  bibliotecas,  ciertamente  incompleta,  que  vale  la  pena  mirar  cuando  se  
enfrenta  a  un  determinado  problema  adecuado:

•  Utilidades  de  fecha  y  hora  (<crono>):  desde  C++11,  el  lenguaje  proporciona  una  colección  de  tipos  para  
representar  relojes,  puntos  de  tiempo  y  duraciones.  Por  ejemplo,  puede  representar  intervalos  
de  tiempo  con  la  ayuda  de  std::chrono::duration.  Y  con  std::chrono::system_clock,  está  
disponible  un  reloj  en  tiempo  real  para  todo  el  sistema.  Puede  usar  la  biblioteca  desde  C  ++  11  
simplemente  incluyendo  el  encabezado  <chrono>.

•  Biblioteca  de  expresiones  regulares  (<regex>):  desde  C++11,  hay  disponible  una  biblioteca  de  
expresiones  regulares  que  se  puede  usar  para  realizar  coincidencias  de  patrones  dentro  de  
cadenas.  También  se  admite  la  sustitución  de  texto  dentro  de  una  cadena  basada  en  expresiones  
regulares.  Puede  usar  la  biblioteca  desde  C++  11  simplemente  incluyendo  el  encabezado  <regex>.

•  Biblioteca  del  sistema  de  archivos  (<sistema  de  archivos>):  desde  C++17,  la  biblioteca  del  sistema  de  
archivos  se  ha  convertido  en  parte  del  estándar.  Antes  de  que  se  convirtiera  en  parte  del  
estándar  principal  de  C++,  ha  sido  una  especificación  técnica  (ISO/IEC  TS  18822:2015).  La  
biblioteca  independiente  del  sistema  operativo  proporciona  varias  instalaciones  para  realizar  
operaciones  en  los  sistemas  de  archivos  y  sus  componentes.  Con  la  ayuda  de  <filesystem>  
puede  crear  directorios,  copiar  archivos,  iterar  sobre  las  entradas  del  directorio,  recuperar  el  tamaño  de  un  archivo,  etc.
Puede  usar  la  biblioteca  desde  C  ++  17  simplemente  incluyendo  el  encabezado  <filesystem>.

■  Sugerencia  TSi  actualmente  aún  no  trabaja  de  acuerdo  con  el  último  estándar  C++17,  Boost.Filesystem  podría  
ser  una  alternativa.

•  Range­v3:  una  biblioteca  de  rangos  para  C++11/14/17  escrita  por  Eric  Niebler,  miembro  del  Comité  
de  estandarización  de  ISO  C++.  Range­v3  es  una  biblioteca  de  solo  encabezado  que  simplifica  
el  manejo  de  contenedores  de  la  biblioteca  estándar  de  C++  o  contenedores  de  otras  bibliotecas  
(por  ejemplo,  Boost).  Con  la  ayuda  de  esta  biblioteca,  puede  deshacerse  de  los  malabarismos  a  
veces  un  poco  complicados  con  los  iteradores  en  diversas  situaciones.  Por  ejemplo,  en  lugar  de  
escribir  std::sort(std::begin(container),  std::end(container)),  simplemente  puede  escribir  
ranges::sort(container).
Range­v3  está  disponible  en  GitHub,  URL:  https://github.com/ericniebler/range­v3.
La  documentación  se  puede  encontrar  aquí:  https://ericniebler.github.io/range­v3/.
•  Estructuras  de  datos  concurrentes  (libcds):  una  biblioteca  de  plantillas  de  C++  en  su  mayoría  solo  de  
encabezado  escrita  por  Max  Khizhinsky,  que  proporciona  algoritmos  sin  bloqueo  e  
implementaciones  de  estructuras  de  datos  concurrentes  para  computación  paralela  de  alto  
rendimiento.  La  biblioteca  está  escrita  en  C++  11  y  publicada  bajo  una  licencia  BSD.  libcds  y  su  
documentación  se  pueden  encontrar  en  SourceForge,  URL:  http://libcds.sourceforge.net.

Manejo  adecuado  de  excepciones  y  errores
Tal  vez  ya  haya  escuchado  el  término  preocupaciones  transversales.  Esta  expresión  incluye  todas  aquellas  cosas  que  son  
difíciles  de  abordar  a  través  de  un  concepto  de  modularización  y,  por  lo  tanto,  requieren  un  tratamiento  especial  por  parte  de  la  
arquitectura  y  el  diseño  del  software.  Una  de  estas  típicas  preocupaciones  transversales  es  la  Seguridad.  Si  tiene  que  cuidar  la  
Seguridad  de  los  Datos  y  las  Restricciones  de  Acceso  en  su  sistema  de  software,  porque  lo  exigen  ciertos  requisitos  de  
calidad,  es  un  tema  delicado  que  impregna  todo  el  sistema.  Tienes  que  lidiar  con  eso  en  casi  todas  partes,  en  prácticamente  
todos  los  componentes.

123
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Otra  preocupación  transversal  es  el  manejo  de  transacciones.  Especialmente  en  las  aplicaciones  de  software  que  usan  
bases  de  datos,  debe  asegurarse  de  que  una  llamada  Transacción,  que  es  una  serie  coherente  de  operaciones  individuales,  tenga  
éxito  o  falle  como  una  unidad  completa;  nunca  puede  ser  sólo  parcialmente  completo.
Y  como  otro  ejemplo,  también  Logging  es  una  preocupación  transversal.  El  registro  suele  ser  necesario  en  todas  
partes  en  un  sistema  de  software.  A  veces,  el  código  productivo  y  específico  del  dominio  está  plagado  de  declaraciones  de  registro,  
lo  que  es  perjudicial  para  la  legibilidad  y  la  comprensión  del  código.
Si  la  arquitectura  del  software  no  se  ocupara  de  estas  preocupaciones  transversales,  esto  podría  conducir  a  soluciones  
inconsistentes.  Por  ejemplo,  se  podrían  usar  dos  marcos  de  registro  diferentes  en  el  mismo  proyecto,  porque  dos  equipos  de  
desarrollo  que  trabajan  en  el  mismo  sistema  decidieron  elegir  marcos  diferentes.
El  manejo  de  excepciones  y  errores  es  otra  preocupación  transversal.  Tratar  con  errores  y  excepciones  
impredecibles  que  requieren  respuestas  y  tratamientos  especiales  es  obligatorio  en  todo  sistema  de  software.  Y,  por  supuesto,  
las  estrategias  de  manejo  de  errores  en  todo  el  sistema  deben  ser  uniformes  y  consistentes.  Por  lo  tanto,  es  muy  importante  que  las  
personas  responsables  de  la  arquitectura  del  software  diseñen  y  desarrollen  una  estrategia  de  manejo  de  errores  bastante  temprano  
en  el  proyecto.
Bueno,  pero  ¿cuáles  son  los  principios  que  nos  guían  en  el  desarrollo  de  una  buena  estrategia  de  manejo  de  errores?  
¿Cuándo  está  justificado  lanzar  una  excepción?  ¿Cómo  trato  las  excepciones  lanzadas?  ¿Y  con  qué  fines  nunca  deben  utilizarse  
las  excepciones?  ¿Cuáles  son  las  alternativas?
Las  siguientes  secciones  presentan  algunas  reglas,  pautas  y  principios  que  ayudan  a  los  programadores  de  C++  a  diseñar  e  
implementar  una  buena  estrategia  de  manejo  de  errores.

La  prevención  es  mejor  que  el  cuidado  posterior
Una  estrategia  básica  fundamentalmente  buena  para  lidiar  con  errores  y  excepciones  es  evitarlos  en  general.
La  razón  de  esto  es  obvia:  todo  lo  que  no  puede  suceder  no  tiene  que  ser  tratado.
Tal  vez  dirás  ahora:  “Bueno,  esto  es  una  perogrullada.  Por  supuesto  que  es  mucho  mejor  evitar  errores  o  
excepciones,  pero  a  veces  no  es  posible  prevenirlos”.  Tienes  razón,  suena  banal  a  primera  vista.
Y  sí,  especialmente  cuando  se  utilizan  bibliotecas  de  terceros,  se  accede  a  bases  de  datos  o  se  accede  a  un  sistema  externo,  
pueden  ocurrir  cosas  imprevisibles.  Pero  para  su  propio  código,  es  decir,  las  cosas  que  puede  diseñar  como  desee,  puede  tomar  
las  medidas  adecuadas  para  evitar  excepciones  en  la  medida  de  lo  posible.
David  Abrahams,  un  programador  estadounidense,  exmiembro  del  comité  de  estandarización  de  ISO  C++  y  miembro  
fundador  de  Boost  C++  Libraries  creó  una  comprensión  de  lo  que  se  llama  seguridad  de  excepciones  y  las  presentó  en  un  documento  
[Abrahams98]  en  1998.  El  conjunto  de  pautas  contractuales  formuladas  en  este  documento,  que  también  se  conocen  como  las  
"Garantías  de  Abraham",  tuvieron  una  influencia  significativa  en  el  diseño  de  la  biblioteca  estándar  de  C++  y  en  cómo  esta  biblioteca  
trata  las  excepciones.  Pero  estas  pautas  no  solo  son  relevantes  para  los  implementadores  de  bibliotecas  de  bajo  nivel.  También  
pueden  ser  considerados  por  los  desarrolladores  de  software  que  escriben  el  código  de  la  aplicación  en  niveles  de  abstracción  más  
altos.
La  seguridad  de  excepciones  es  parte  del  diseño  de  la  interfaz.  Una  interfaz  (API)  no  solo  consta  de  firmas  de  función,  
es  decir,  los  parámetros  de  una  función  y  los  tipos  de  devolución.  También  las  excepciones  que  pueden  lanzarse  si  se  invoca  
una  función  son  parte  de  su  interfaz.  Además,  hay  tres  aspectos  más  que  deben  tenerse  en  cuenta:

•  Precondición:  Una  precondición  es  una  condición  que  siempre  debe  ser  cierta  antes  de  que
se  invoca  la  función  o  el  método  de  una  clase.  Si  se  viola  una  condición  previa,  no  se  puede  garantizar  
que  la  llamada  a  la  función  produzca  el  resultado  esperado:  la  llamada  a  la  función  puede  tener  éxito,  
puede  fallar,  puede  causar  efectos  secundarios  no  deseados  o  mostrar  un  comportamiento  indefinido.

•  Invariante:  Una  invariante  es  una  condición  que  siempre  debe  ser  cierta  durante  la  ejecución  de  una  función  
o  método.  En  otras  palabras,  es  una  condición  que  se  cumple  al  principio  y  al  final  de  la  ejecución  de  una  
función.  Una  forma  especial  de  un  invariante  en  la  orientación  a  objetos  es  un  invariante  de  clase.  
Si  se  viola  tal  invariante,  el  objeto  (instancia)  de  la  clase  queda  en  un  estado  incorrecto  e  inconsistente  
después  de  una  llamada  al  método.

124
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

•  Poscondición:  una  poscondición  es  una  condición  que  siempre  debe  cumplirse  inmediatamente  después  de  
la  ejecución  de  una  función  o  método.  Si  se  viola  una  condición  posterior,  debe  haber  ocurrido  un  
error  durante  la  ejecución  de  la  función  o  el  método.

La  idea  detrás  de  la  seguridad  de  excepción  es  que  las  funciones,  o  una  clase  y  sus  métodos,  dan  a  sus  clientes  una  
especie  de  promesa,  o  garantía,  sobre  invariantes,  condiciones  posteriores  y  sobre  excepciones  que  pueden  lanzarse  o  no.  Hay  
cuatro  niveles  de  excepción  de  seguridad.  En  las  siguientes  subsecciones  los  analizo  brevemente  en  orden  creciente  de  seguridad.

Sin  Excepciones­Seguridad

Con  este  nivel  más  bajo  de  seguridad  de  excepción,  literalmente,  sin  seguridad  de  excepción,  no  se  garantiza  absolutamente  nada.
Cualquier  excepción  que  ocurra  puede  tener  consecuencias  desastrosas.  Por  ejemplo,  se  violan  las  invariantes  y  las  condiciones  
posteriores  de  la  función  o  el  método  llamado,  y  una  parte  de  su  código,  por  ejemplo,  un  objeto,  posiblemente  se  quede  en  un  estado  
corrupto.
¡Creo  que  no  hay  duda  de  que  el  código  escrito  por  usted  nunca  debería  ofrecer  este  nivel  inadecuado  de  
seguridad  de  excepción!  Solo  pretenda  que  no  existe  tal  cosa  como  "sin  excepción,  seguridad".
Eso  es  todo;  no  hay  nada  más  que  decir  al  respecto.

Excepción  básica  de  seguridad

La  garantía  básica  de  seguridad  de  excepción  es  la  garantía  que  cualquier  pieza  de  código  debe  ofrecer  al  menos.  También  es  el  
nivel  de  seguridad  excepcional  que  se  puede  lograr  con  un  esfuerzo  de  implementación  relativamente  pequeño.  Este  nivel  garantiza  
lo  siguiente:
• Si  se  lanza  una  excepción  durante  una  llamada  de  función  o  método,  ¡se  garantiza  que  no  se  filtren  
recursos!  Esta  garantía  incluye  recursos  de  memoria  así  como  otros  recursos.  Esto  se  puede  lograr  
aplicando  el  patrón  RAII  (consulte  la  sección  sobre  RAII  y  Smart  Pointers).
• Si  se  lanza  una  excepción  durante  una  llamada  de  función  o  método,  se  conservan  todas  las  
invariantes.

• Si  se  lanza  una  excepción  durante  una  llamada  de  función  o  método,  no  habrá  corrupción  
de  datos  o  memoria  después,  y  todos  los  objetos  estarán  en  un  estado  saludable  y  consistente.  
Sin  embargo,  no  se  garantiza  que  el  contenido  de  los  datos  sea  el  mismo  que  antes  de  llamar  a  la  
función  o  al  método.

La  regla  estricta  es  esta:

Diseñe  su  código,  especialmente  sus  clases,  de  modo  que  garanticen  al  menos  la  seguridad  de  excepción  básica.  ¡Este  debería  

ser  siempre  el  nivel  de  seguridad  de  excepción  predeterminado!

Es  importante  saber  que  la  biblioteca  estándar  de  C++  espera  que  todos  los  tipos  de  usuarios  proporcionen  siempre  al  menos  la
garantía  de  excepción  básica.

Fuerte  excepción­seguridad

La  fuerte  seguridad  de  excepción  garantiza  todo  lo  que  también  está  garantizado  por  el  nivel  básico  de  seguridad  de  excepción,  pero  
asegura  además  que,  en  caso  de  una  excepción,  el  contenido  de  los  datos  se  recupera  exactamente  igual  que  antes  de  que  se  
llamara  a  la  función  o  método.  En  otras  palabras,  con  este  nivel  de  seguridad  de  excepción  obtenemos  semántica  de  
compromiso  o  reversión  como  en  el  manejo  de  transacciones  en  bases  de  datos.

125
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Es  fácil  comprender  que  este  nivel  de  seguridad  excepcional  lleva  a  un  mayor  esfuerzo  de  implementación  y  puede  ser  costoso  en  
tiempo  de  ejecución.  Un  ejemplo  de  este  esfuerzo  adicional  es  el  llamado  modismo  de  copiar  e  intercambiar  que  debe  usarse  para  
garantizar  una  fuerte  seguridad  de  excepción  para  la  asignación  de  copias.
Equipar  todo  su  código  con  una  fuerte  seguridad  de  excepción  sin  ninguna  buena  razón  violaría  el
principios  KISS  y  YAGNI  (ver  Capítulo  3).  Por  lo  tanto,  la  directriz  al  respecto  es  la  siguiente:

Emita  la  fuerte  garantía  de  seguridad  de  excepciones  para  su  código  solo  si  es  absolutamente  necesario.

Por  supuesto,  si  hay  ciertos  requisitos  de  calidad  con  respecto  a  la  integridad  de  los  datos  y  la  corrección  de  los  datos  
que  deben  cumplirse,  debe  proporcionar  el  mecanismo  de  reversión  que  está  garantizado  a  través  de  una  fuerte  seguridad  de  
excepción.

La  garantía  de  no  tirar
Este  es  el  nivel  de  seguridad  de  excepción  más  alto,  también  conocido  como  transparencia  de  fallas.  En  pocas  palabras,  este  nivel  
significa  que,  como  llamador  de  una  función  o  método,  no  tiene  que  preocuparse  por  las  excepciones.  La  llamada  a  la  función  o  al  
método  tendrá  éxito.  ¡Siempre!  Nunca  arrojará  una  excepción,  porque  todo  se  maneja  correctamente  internamente.  Nunca  se  violarán  
invariantes  y  poscondiciones.
Este  es  el  paquete  completo  y  despreocupado  de  seguridad  de  excepción,  pero  a  veces  es  muy  difícil  o  incluso  imposible  de  
lograr,  especialmente  en  C++.  Por  ejemplo,  si  usa  algún  tipo  de  asignación  de  memoria  dinámica  dentro  de  una  función,  como  
operator  new,  ya  sea  directa  o  indirectamente  (por  ejemplo,  a  través  de  std::make_shared<T>),  ya  no  tiene  ninguna  posibilidad  de  
terminar  con  un  procesamiento  exitoso.  después  de  que  se  encontró  una  excepción.

Estos  son  los  casos  en  los  que  la  garantía  de  no  tirar  es  absolutamente  obligatoria  o  al  menos  explícitamente  recomendada:

•  ¡ Los  destructores  de  clases  deben  garantizar  no  tirar  en  todas  las  circunstancias!
La  razón  es  que,  entre  otras  situaciones,  también  se  llama  a  los  destructores  mientras  se  desenrolla  la  
pila  después  de  que  se  haya  encontrado  una  excepción.  Sería  fatal  si  ocurriera  otra  excepción  
durante  el  desenrollado  de  la  pila,  porque  el  programa  terminaría  inmediatamente.

Como  consecuencia,  cualquier  operación  dentro  de  un  destructor  que  trate  con  recursos  asignados  
e  intente  cerrarlos,  como  archivos  abiertos  o  memoria  asignada  en  el  montón,  no  debe  fallar.

•  Las  operaciones  de  movimiento  (constructores  de  movimiento  y  operadores  de  asignación  de  movimiento;  
consulte  la  sección  anterior  sobre  la  semántica  de  movimiento)  deben  garantizar  que  no  se  
produzca  ningún  lanzamiento.  Si  una  operación  de  movimiento  arroja  una  excepción,  la  probabilidad  
de  que  el  movimiento  no  haya  tenido  lugar  es  enormemente  alta.  Por  lo  tanto,  debe  evitarse  a  toda  
costa  que  las  implementaciones  de  operaciones  de  movimiento  asignen  recursos  a  través  de  técnicas  
de  asignación  de  recursos  que  pueden  generar  excepciones.  Además,  es  importante  otorgar  la  
garantía  de  no  generación  de  tipos  destinados  a  usarse  con  los  contenedores  de  biblioteca  estándar  
de  C++.  Si  el  constructor  de  movimiento  para  un  tipo  de  elemento  en  un  contenedor  no  ofrece  una  
garantía  de  no  lanzamiento  (es  decir,  el  constructor  de  movimiento  no  se  declara  con  el  especificador  
noexcept,  consulte  la  barra  lateral  a  continuación),  entonces  el  contenedor  preferirá  usar  las  operaciones  
de  copia  en  lugar  de  las  operaciones  de  movimiento.

•  Los  constructores  predeterminados  deben  ser  preferentemente  sin  tiro.  Básicamente,  lanzar  una  
excepción  en  un  constructor  no  es  deseable,  pero  es  la  mejor  manera  de  lidiar  con  las  fallas  del  
constructor.  Es  muy  probable  que  un  "objeto  construido  a  medias"  viole  invariantes.
Y  un  objeto  en  un  estado  corrupto  que  viola  sus  invariantes  de  clase  es  inútil  y

126
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

peligroso.  Por  lo  tanto,  no  hay  nada  en  contra  de  lanzar  una  excepción  en  un  constructor  predeterminado  
cuando  es  inevitable.  Sin  embargo,  es  una  buena  estrategia  de  diseño  para  evitarlo  en  gran  medida.  Los  
constructores  predeterminados  deben  ser  simples.  Si  un  constructor  predeterminado  puede  lanzar,  
probablemente  esté  haciendo  demasiadas  cosas  complejas.  Por  lo  tanto,  al  diseñar  una  clase,  debe  intentar  
evitar  excepciones  en  el  constructor  predeterminado.

•  ¡ Una  función  de  intercambio  debe  garantizar  que  no  se  produzca  ningún  lanzamiento  en  todas  las  
circunstancias!  Una  función  swap()  implementada  por  expertos  no  debe  asignar  ningún  recurso  (por  
ejemplo,  memoria)  utilizando  técnicas  de  asignación  de  memoria  que  potencialmente  pueden  generar  excepciones.
Sería  fatal  si  swap()  puede  lanzar,  porque  puede  terminar  con  un  estado  inconsistente.  Y  la  mejor  
manera  de  escribir  un  operador  seguro  de  excepción  =  ()  es  mediante  el  uso  de  una  función  de  
intercambio  ()  que  no  lanza  para  su  implementación.

NOEXCEPTO  ESPECIFICADOR  Y  OPERADOR  [C++11]

Antes  de  C++  11,  existía  la  palabra  clave  throw  que  podía  estar  en  la  declaración  de  una  función.  Se  usó  para  enumerar  todos  
los  tipos  de  excepción  en  una  lista  separada  por  comas  que  una  función  podría  arrojar  directa  o  indirectamente,  conocida  
como  especificación  de  excepción  dinámica.  El  uso  de  throw(exceptionType,ExceptionType, ...)  está  en  desuso  desde  C++11  y  
ahora  finalmente  se  eliminó  del  estándar  con  C++17.
Lo  que  todavía  está  disponible,  pero  también  está  marcado  como  obsoleto  desde  C++  11,  es  el  especificador  throw()  sin  una  
lista  de  tipos  de  excepción.  Su  semántica  ahora  es  la  misma  que  la  del  especificador  noexcept  (verdadero) .

El  especificador  noexcept  en  la  firma  de  una  función  declara  que  la  función  no  puede  generar  ninguna  excepción.  
Lo  mismo  es  válido  para  noexcept  (verdadero),  que  es  solo  un  sinónimo  de  noexcept.  En  su  lugar,  una  función  que  se  declara  
con  noexcept  (falso)  está  generando  potencialmente,  es  decir,  puede  generar  excepciones.
Aquí  hay  unos  ejemplos:

void  nonThrowingFunction()  noexcept;  anular  otra  
función  que  no  arroja  ()  no  excepto  (verdadero);  void  
aPotentiallyThrowingFunction()  noexcept(false);

Hay  dos  buenas  razones  para  el  uso  de  noexcept:  Primero,  las  excepciones  que  una  función  o  método  podría  lanzar  (o  no)  
son  partes  de  la  interfaz  de  la  función.  Se  trata  de  semántica  y  ayuda  al  desarrollador  que  está  leyendo  el  código  a  saber  qué  
puede  pasar  y  qué  no.  noexcept  les  dice  a  los  desarrolladores  que  pueden  usar  esta  función  de  manera  segura  en  sus  
propias  funciones  que  no  son  de  lanzamiento.  Por  lo  tanto,  la  presencia  de  noexcept  es  algo  similar  a  const.

Segundo,  puede  ser  usado  por  el  compilador  para  optimizaciones.  noexcept  permite  potencialmente  que  un  compilador  
compile  la  función  sin  agregar  la  sobrecarga  de  tiempo  de  ejecución  que  antes  requería  el  throw(...)  eliminado,  es  decir,  el  
código  objeto  que  era  necesario  para  llamar  a  std::unexpected()  cuando  se  producía  una  excepción.  no  listado  fue  arrojado.

Para  los  implementadores  de  plantillas,  también  hay  un  operador  noexcept ,  que  realiza  una  verificación  en  tiempo  de  
compilación  que  devuelve  verdadero  si  se  declara  que  la  expresión  no  genera  excepciones:

constexpr  auto  isNotThrowing  =  noexcept(nonThrowingFunction());

Nota:  También  las  funciones  constexpr  (consulte  la  sección  "Cálculos  durante  el  tiempo  de  compilación")  pueden  generarse  cuando  se  

evalúan  en  tiempo  de  ejecución,  por  lo  que  es  posible  que  también  necesite  noexcept  para  algunas  de  ellas.

127
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Una  excepción  es  una  excepción,  ¡literalmente!
En  el  Capítulo  4  discutimos  en  la  sección  "No  pasar  o  devolver  0  (NULL,  nullptr)"  que  no  debe  devolver  nullptr  
como  valor  de  retorno  de  una  función.  Como  ejemplo  de  código,  hemos  tenido  una  pequeña  función  que  debe  
realizar  una  búsqueda  de  un  cliente  por  nombre,  lo  que,  por  supuesto,  no  genera  ningún  resultado  si  no  se  puede  
encontrar  a  este  cliente.  A  alguien  se  le  podría  ocurrir  la  idea  de  lanzar  una  excepción  para  un  cliente  no  
encontrado,  como  se  muestra  en  el  siguiente  código  de  ejemplo.

#include  "Cliente.h"
#include  <cadena>  
#include  <excepción>

clase  CustomerNotFoundException:  public  std::exception  { virtual  const  
char*  what()  const  noexcept  override  {
volver  "¡Cliente  no  encontrado!"; } };

// ...

Cliente  CustomerService::findCustomerByName(const  std::string&  name)  const  noexcept(false)  {

//  Código  que  busca  al  cliente  por  su  nombre... // ...y  si  
no  se  encuentra  al  cliente:
throw  CustomerNotFoundException(); }

Y  ahora  echemos  un  vistazo  al  sitio  de  invocación  de  esta  función:

cliente  cliente;  pruebe  

{ cliente  =  findCustomerByName("Nombre  no  existente");
}  catch  (const  CustomerNotFoundException&  ex)  {
// ...

} // ...

A  primera  vista,  esto  parece  una  solución  factible.  Si  tenemos  que  evitar  devolver  nullptr  desde  el
función,  podemos  lanzar  una  CustomerNotFoundException  en  su  lugar.  En  el  sitio  de  invocación,  ahora  podemos  
distinguir  entre  el  caso  feliz  y  el  caso  malo  con  la  ayuda  de  una  construcción  de  prueba  y  captura.
De  hecho,  ¡es  una  solución  realmente  mala!  No  encontrar  un  cliente  solo  porque  su  nombre  no  existe  
definitivamente  no  es  un  caso  excepcional.  Estas  son  cosas  que  sucederán  normalmente.  Lo  que  se  ha  hecho  en  el  ejemplo  
anterior  es  un  abuso  de  las  excepciones.  Las  excepciones  no  existen  para  controlar  el  flujo  normal  del  programa.  ¡Las  
excepciones  deben  reservarse  para  lo  que  es  verdaderamente  excepcional!
¿Qué  significa  “verdaderamente  excepcional”?  Bueno,  significa  que  no  hay  nada  que  puedas  hacer  al  respecto,  y
realmente  no  puede  manejar  esa  excepción.  Por  ejemplo,  supongamos  que  se  enfrenta  a  una  excepción  std::bad_  
alloc,  lo  que  significa  que  hubo  un  error  en  la  asignación  de  memoria.  ¿Cómo  debe  continuar  el  programa  
ahora?  ¿Cuál  fue  la  causa  raíz  de  este  problema?  ¿El  sistema  de  hardware  subyacente  tiene  falta  de  memoria?  
Bueno,  ¡entonces  tenemos  un  problema  realmente  serio!  ¿Hay  alguna  forma  significativa  de  recuperarse  de  esta  
grave  excepción  y  reanudar  la  ejecución  del  programa?  ¿Podemos  seguir  asumiendo  la  responsabilidad  de  que  el  
programa  simplemente  siga  funcionando  como  si  nada  hubiera  pasado?

128
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Estas  preguntas  no  se  pueden  responder  fácilmente.  Quizás  el  desencadenante  real  de  este  problema  fue  un  puntero  
colgante,  que  se  ha  utilizado  de  manera  inexperta  en  millones  de  instrucciones  antes  de  encontrar  la  excepción  std::bad_  
alloc.  Todo  esto  rara  vez  puede  reproducirse  en  el  momento  de  la  excepción.
Aquí  está  mi  consejo:

Lanzar  excepciones  solo  en  casos  muy  excepcionales.  No  haga  mal  uso  de  las  excepciones  para  controlar  el  flujo  

normal  del  programa.

Tal  vez  te  preguntes  ahora:  “Bueno,  es  malo  usar  nullptr  respectivamente  NULL  como  valor  de  retorno,
y  las  excepciones  tampoco  son  deseadas...  ¿qué  debo  hacer  ahora  en  su  lugar?  En  la  sección  “Objeto  de  caso  
especial  (Objeto  nulo)”  en  el  Capítulo  9  sobre  patrones  de  diseño,  presentaré  una  solución  factible  para  manejar  estos  casos  de  
manera  adecuada.

Si  no  puede  recuperarse,  salga  rápidamente
Si  se  enfrenta  a  una  excepción  de  la  que  no  puede  recuperarse,  a  menudo  el  mejor  enfoque  es  registrar  la  excepción  (si  es  
posible)  o  generar  un  archivo  de  volcado  de  memoria  para  fines  de  análisis  posteriores  y  finalizar  el  programa  
inmediatamente.  Un  buen  ejemplo  en  el  que  una  terminación  rápida  puede  ser  la  mejor  reacción  es  una  asignación  de  
memoria  fallida.  Si  un  sistema  carece  de  memoria,  bueno,  ¿qué  debe  hacer  entonces  en  el  contexto  de  su  programa?

El  principio  detrás  de  esta  estricta  estrategia  de  manejo  de  algunas  excepciones  y  errores  críticos  se  denomina  "Dead
Programs  Tell  No  Lies”  y  se  describe  en  el  libro  Pragmatic  Programmer  [Hunt99].
Nada  es  peor  que  continuar  después  de  un  error  grave  como  si  nada  hubiera  pasado  y  producir,  por  ejemplo,  decenas  
de  miles  de  reservas  erróneas,  o  enviar  el  ascensor  por  centésima  vez  desde  el  sótano  hasta  el  último  piso  y  viceversa.  En  su  
lugar,  salga  antes  de  que  ocurran  demasiados  daños  consecuentes.

Definir  tipos  de  excepción  específicos  del  usuario  Aunque  puede  

lanzar  lo  que  quiera  en  C++,  como  un  int  o  un  const  char*,  no  lo  recomendaría.  Las  excepciones  son  capturadas  por  sus  
tipos;  por  lo  tanto,  es  una  muy  buena  idea  crear  sus  clases  de  excepción  personalizadas  para  ciertas  excepciones,  en  su  
mayoría  específicas  del  dominio.  Como  ya  expliqué  en  el  Capítulo  4,  un  buen  nombre  es  crucial  para  la  legibilidad  y  el  
mantenimiento  del  código,  y  también  los  tipos  de  excepción  deben  tener  buenos  nombres.  Y  también  otros  principios,  que  
son  válidos  para  el  diseño  del  código  de  programa  "normal",  por  supuesto  también  son  válidos  para  los  tipos  de  
excepción  (discutiremos  estos  principios  en  detalle  en  el  Capítulo  6) .  sobre  la  orientación  a  objetos).

Para  proporcionar  su  propio  tipo  de  excepción,  simplemente  puede  crear  su  propia  clase  y  derivarla  de  
std::exception  (definida  en  el  encabezado  <stdexcept>):

#incluir  <stdexcept>

clase  MyCustomException:  public  std::exception  {
virtual  const  char*  what()  const  noexcept  override  {
return  "¡Proporcione  algunos  detalles  sobre  lo  que  estaba  fallando  aquí!"; } };

129
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Al  anular  la  función  miembro  virtual  what()  heredada  de  std::exception,  podemos  proporcionar  cierta  información  
a  la  persona  que  llama  sobre  lo  que  salió  mal.  Además,  derivar  nuestra  propia  clase  de  excepción  de  std::exception  la  
hará  capturable  mediante  una  cláusula  catch  genérica  (que,  por  cierto,  solo  debe  considerarse  como  la  última  posibilidad  
de  capturar  una  excepción),  como  esta:

#incluir  <iostream>

// ...  
intenta  
{ hacerAlgoQueLanza(); }  catch  
(const  std::exception&  ex)  {
std::cerr  <<  ex.what()  <<  std::endl; }

Básicamente,  las  clases  de  excepción  deben  tener  un  diseño  simple,  pero  si  desea  proporcionar  más  detalles  sobre
la  causa  de  la  excepción,  también  puede  escribir  clases  más  sofisticadas,  como  las  siguientes:

Listado  5­27.  Una  clase  de  excepción  personalizada  para  divisiones  por  cero

clase  DivisionByZeroException:  public  std::exception  { public:

DivisionByZeroException()  =  borrar;  explícito  
DivisionByZeroException(const  int  dividendo)  {
buildErrorMessage(dividendo); }

virtual  const  char*  what()  const  noexcept  override  {
devolver  mensaje  de  error.c_str(); }

privado:  
void  buildErrorMessage(const  int  dividendo)  {
mensaje  de  error  =  "Una  división  con  dividendo  =  ";  mensaje  
de  error  +=  std::to0_string(dividendo);  errorMessage  +=  
",  y  divisor  =  0,  no  está  permitido  (división  por  cero)!"; }

std::string  mensaje  de  error; };

Tenga  en  cuenta  que,  debido  a  su  implementación,  la  función  de  miembro  privado  buildErrorMessage()  solo  puede  
garantizar  una  fuerte  seguridad  de  excepción,  es  decir,  puede  generarse  debido  al  uso  de  std::string::operator+=().
Por  lo  tanto,  el  constructor  de  inicialización  tampoco  puede  dar  la  garantía  de  no  lanzamiento.  Esa  es  la  razón  por  la  cual  las  
clases  de  excepción  generalmente  deberían  tener  un  diseño  bastante  simple.
Aquí  hay  un  pequeño  ejemplo  de  uso  de  nuestra  clase  DivisionByZeroException:

int  divide(const  int  dividendo,  const  int  divisor)  { if  (divisor  ==  0)  {

throw  DivisionByZeroException(dividendo); }  devolver  

dividendo /  divisor; }

130
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

int  principal  ()  

{ probar  { dividir  (10,  
0); }  catch  (const  DivisionByZeroException&  ex)  { std::cerr  
<<  ex.what()  <<  std::endl;  devolver  1; }  
devuelve  

0; }

Lanzamiento  por  valor,  captura  por  constante  Referencia
A  veces  he  visto  que  los  objetos  de  excepción  se  asignan  en  el  montón  con  la  ayuda  de  new  y  se  lanzan  como  un  
puntero,  como  en  este  ejemplo:

pruebe  { CFile  f(_T("M_Cause_File.dat"),  CFile::modeWrite); //  Si  
"M_Cause_File.dat"  no  existe,  el  constructor  de  CFile  lanza  una  excepción //  de  esta  manera:  throw  new  
CFileException() }  catch(CFileException*  e)  {

if( e­>m_cause  ==  CFileException::fileNotFound)
TRACE(_T("ERROR:  Archivo  no  encontrado\n"));  
e­>Borrar(); }

Tal  vez  haya  reconocido  este  estilo  de  codificación  de  C++:  lanzar  y  capturar  excepciones  de  esta  manera
se  puede  encontrar  en  la  antigua  biblioteca  de  MFC  (Microsoft  Foundation  Classes)  en  abundancia.  Y  es  importante  
que  no  olvide  llamar  a  la  función  miembro  Delete()  al  final  de  la  cláusula  catch;  de  lo  contrario,  puede  decir  "¡Hola!"  a  
las  fugas  de  memoria.
Bueno,  lanzar  excepciones  con  new  y  capturarlas  como  un  puntero  es  posible  en  C++,  pero  es  un  mal  
diseño.  ¡No  lo  hagas!  Si  olvida  eliminar  el  objeto  de  excepción,  se  producirá  una  pérdida  de  memoria.  Lance  siempre  
el  objeto  de  excepción  por  valor  y  captúrelos  por  referencia  constante,  como  se  puede  ver  en  todos  los  ejemplos  anteriores.

Preste  atención  al  orden  correcto  de  las  cláusulas  catch  Si  proporciona  más  de  una  

cláusula  catch  después  de  un  bloque  de  prueba,  por  ejemplo,  para  distinguir  entre  diferentes  tipos  de  
excepciones,  es  importante  que  tenga  cuidado  con  el  orden  correcto.  Las  cláusulas  catch  se  evalúan  en  el  orden  
en  que  aparecen.  Esto  significa  que  las  cláusulas  catch  para  los  tipos  de  excepción  más  específicos  deben  ir  
primero.  En  el  siguiente  ejemplo,  las  clases  de  excepción  DivisionByZeroException  y  
CommunicationInterruptedException  se  derivan  de  std::exception.

131
Machine Translated by Google

Capítulo  5  ■  Conceptos  avanzados  de  C++  moderno

Listado  5­28.  Las  excepciones  más  específicas  deben  manejarse  primero.

intente  { hacerAlgoQuePuedeLanzarVariasExcepciones(); }  
captura  (const  DivisionByZeroException&  ex)  {
// ...
}  catch  (const  CommunicationInterruptedException&  ex)  {
// ...
}  catch  (const  std::exception&ex)  {
//  Manejar  todas  las  demás  excepciones  aquí  que  se  derivan  de  std::exception
}  catch  (...)  { //  El  
resto...
}

Creo  que  la  razón  es  obvia:  supongamos  que  la  cláusula  catch  para  la  excepción  general  std::exception  
sería  la  primera,  ¿qué  pasaría?  Los  más  específicos  a  continuación  nunca  tendrían  una  oportunidad  
porque  están  "ocultos"  por  el  más  general.  Por  lo  tanto,  los  desarrolladores  deben  prestar  atención  para  
colocarlos  en  el  orden  correcto.

132
Machine Translated by Google

CAPÍTULO  6

Orientación  a  objetos

Las  raíces  históricas  de  la  orientación  a  objetos  (OO)  se  pueden  encontrar  a  fines  de  la  década  de  1950.  Los  informáticos  
noruegos  Kristen  Nygaard  y  Ole­Johan  Dahl  llevaron  a  cabo  cálculos  de  simulación  para  el  desarrollo  y  la  construcción  del  
primer  reactor  nuclear  de  Noruega  en  el  instituto  de  investigación  militar  Norwegian  Defense  Research  Establishment  
(NDRE).  Mientras  desarrollaban  los  programas  de  simulación,  los  dos  científicos  notaron  que  los  lenguajes  de  programación  
de  procedimientos  utilizados  para  esa  tarea  no  eran  adecuados  para  la  complejidad  de  los  problemas  que  debían  
abordarse.  Dahl  y  Nygaard  sintieron  la  necesidad  de  posibilidades  adecuadas  en  esos  lenguajes  para  abstraer  y  reproducir  
las  estructuras,  conceptos  y  procesos  del  mundo  real.
En  1960,  Nygaard  se  mudó  al  Norwegian  Computing  Center  (NCC)  que  se  había  establecido  en  Oslo  dos  años  antes.  
Tres  años  más  tarde,  Ole­Johan  Dahl  también  se  unió  al  NCC.  En  esta  fundación  de  investigación  privada,  independiente  y  
sin  fines  de  lucro,  los  dos  científicos  desarrollaron  las  primeras  ideas  y  conceptos  para  un  lenguaje  de  programación  
orientado  a  objetos,  desde  el  punto  de  vista  actual.  Nygaard  y  Dahl  buscaban  un  lenguaje  que  fuera  adecuado  para  todos  los  
dominios  y  menos  especializado  para  ciertos  campos  de  aplicación,  como,  por  ejemplo,  Fortran  para  cálculos  numéricos  y  
álgebra  lineal;  o  COBOL,  que  está  diseñado  especialmente  para  uso  empresarial.
El  resultado  de  sus  actividades  de  investigación  fue  finalmente  el  lenguaje  de  programación  Simula­67,  una  extensión  
del  lenguaje  de  programación  procedimental  ALGOL  60.  El  nuevo  lenguaje  introdujo  clases,  subclases,  objetos,  variables  
de  instancia,  métodos  virtuales  e  incluso  un  recolector  de  basura.  Simula­67  se  considera  el  primer  lenguaje  de  programación  
orientado  a  objetos  y  ha  influido  en  muchos  otros  de  los  siguientes  lenguajes  de  programación,  por  ejemplo,  el  lenguaje  
de  programación  completamente  orientado  a  objetos  Smalltalk,  que  fue  diseñado  por  Alan  Kay  y  su  equipo  a  principios  de  la  
década  de  1970.
Mientras  el  científico  informático  danés  Bjarne  Stroustrup  trabajaba  en  su  tesis  doctoral  Comunicación  y
Control  en  Sistemas  Computacionales  Distribuidos  en  la  Universidad  de  Cambridge  a  fines  de  1970,  usó  Simula­67  y  lo  
encontró  bastante  útil,  pero  demasiado  lento  para  un  uso  práctico.  Entonces  comenzó  a  buscar  posibilidades  para  combinar  
los  conceptos  de  abstracción  de  datos  orientados  a  objetos  de  Simula­67  con  la  alta  eficiencia  de  los  lenguajes  de  
programación  de  bajo  nivel.  El  lenguaje  de  programación  más  eficiente  en  ese  momento  era  C,  que  había  sido  desarrollado  
por  el  científico  informático  estadounidense  Dennis  Ritchie  en  Bell  Telephone  Laboratories  a  principios  de  la  década  de  
1970.  Stroustrup,  quien  se  unió  al  Centro  de  Investigación  de  Ciencias  de  la  Computación  de  Bell  Telephone  Laboratories  
en  1979,  comenzó  a  agregar  características  orientadas  a  objetos,  como  clases,  herencia,  verificación  de  tipo  fuerte  y  muchas  
otras  cosas  al  lenguaje  C  y  lo  llamó  "C  con  Clases".  ”  En  1983,  el  nombre  del  lenguaje  se  cambió  a  C  ++,  una  palabra  
creada  por  el  asociado  de  Stroustrups  Rick  Mascitti,  por  lo  que  ++  se  inspiró  en  el  operador  de  incremento  posterior  del  
lenguaje.
En  las  décadas  siguientes,  la  orientación  a  objetos  se  convirtió  en  el  paradigma  de  programación  dominante.

Pensamiento  orientado  a  objetos
Hay  un  punto  muy  importante  que  debemos  tener  en  cuenta.  Solo  porque  hay  varios  lenguajes  de  programación  disponibles  
en  el  mercado  que  admiten  conceptos  orientados  a  objetos,  no  hay  absolutamente  ninguna  garantía  de  que  los  desarrolladores  
que  utilicen  estos  lenguajes  produzcan  un  diseño  de  software  orientado  a  objetos  automáticamente.  Especialmente

©  Stephan  Roth  2017   133
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0_6
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

aquellos  desarrolladores  que  han  trabajado  con  lenguajes  procedimentales  durante  mucho  tiempo  suelen  tener  dificultades  con  la  
transición  a  ese  paradigma  de  programación.  La  orientación  a  objetos  no  es  un  concepto  simple  de  comprender.  Requiere  que  los  
desarrolladores  vean  el  mundo  de  una  manera  nueva.
Dr.  Alan  Curtis  Kay,  quien  desarrolló  el  lenguaje  de  programación  orientado  a  objetos  Smalltalk  con  algunos
colegas  de  Xerox  PARC  a  principios  de  la  década  de  1970,  es  bien  conocido  como  uno  de  los  padres  del  término  
"orientación  a  objetos".  En  una  discusión  documentada  por  correo  electrónico  con  el  profesor  universitario  alemán  Dipl.­Ing.  Stefan  
Ram  de  Freie  Universität  Berlin  desde  el  año  2003,  Kay  explicó  lo  que  hace  que  la  orientación  a  objetos  para  él:

Pensé  que  los  objetos  eran  como  células  biológicas  y/o  computadoras  individuales  en  una  red,  solo  
capaces  de  comunicarse  con  mensajes  (por  lo  que  la  mensajería  apareció  desde  el  principio;  tomó  
un  tiempo  ver  cómo  enviar  mensajes  en  un  lenguaje  de  programación  lo  suficientemente  eficiente  
como  para  ser  útil).  (…)  OOP  para  mí  significa  solo  mensajería,  retención  local  y  protección  y  
ocultamiento  del  proceso  estatal,  y  vinculación  tardía  extrema  de  todas  las  cosas.

­Dr.  Alan  Curtis  Kay,  informático  estadounidense,  23  de  julio  de  2003  [Ram03]

Una  célula  biológica  se  puede  definir  como  la  unidad  estructural  y  funcional  más  pequeña  de  todos  los  organismos.  A  menudo  se  les  
llama  los  "bloques  de  construcción  de  la  vida".  Alan  Kay  consideró  el  software  de  la  misma  manera  que  un  biólogo  ve  organismos  
vivos  complejos.  Esta  perspectiva  de  Alan  Kay  no  debería  sorprender,  porque  tiene  una  licenciatura  en  matemáticas  y  biología  
molecular.
Las  células  de  Alan  Kay  son  lo  que  llamamos  objetos  en  OO.  Un  objeto  puede  ser  considerado  una  “cosa”  que  tiene  estructura  
y  comportamiento.  Una  célula  biológica  tiene  una  membrana  que  la  rodea  y  la  encapsula.  Esto  también  se  puede  aplicar  a  objetos  en  
orientación  a  objetos.  Un  objeto  debe  estar  bien  encapsulado  y  ofrecer  sus  servicios  a  través  de  interfaces  bien  definidas.

Además,  Alan  Kay  enfatizó  que  la  "mensajería"  juega  un  papel  central  para  él  en  la  orientación  a  objetos.
Sin  embargo,  no  define  exactamente  lo  que  quiere  decir  con  eso.  ¿Llamar  a  un  método  llamado  foo()  en  un  objeto  es  lo  
mismo  que  enviar  un  mensaje  llamado  "foo"  a  ese  objeto?  ¿O  Alan  Kay  tenía  en  mente  una  infraestructura  de  paso  de  
mensajes,  como  CORBA  (Common  Object  Request  Broker  Architecture)  y  tecnologías  similares?  El  Dr.  Kay  también  es  
matemático,  por  lo  que  también  podría  referirse  a  un  modelo  matemático  prominente  de  paso  de  mensajes  llamado  modelo  Actor,  
que  es  muy  popular  en  el  cálculo  concurrente.
En  cualquier  caso  y  sea  lo  que  sea  lo  que  Alan  Kay  tenía  en  mente  cuando  hablaba  de  mensajería,  considero  esta  visión  
interesante  y,  en  general,  aplicable  para  explicar  la  estructura  típica  de  un  programa  orientado  a  objetos  en  un  nivel  abstracto.  
Pero  las  elucidaciones  del  Sr.  Kay  definitivamente  no  son  suficientes  para  responder  las  siguientes  preguntas  importantes:

•¿Cómo  encuentro  y  formo  las  “células” (objetos)?

•¿Cómo  diseño  la  interfaz  pública  disponible  de  esas  celdas?

•¿Cómo  controlo  quién  puede  comunicarse  con  quién  (dependencias)?

La  orientación  a  objetos  (OO)  es  principalmente  una  mentalidad  y  menos  una  cuestión  del  lenguaje  utilizado.  Y  también  puede
ser  abusado  y  mal  aplicado.
He  visto  muchos  programas  escritos  en  C++,  o  en  un  lenguaje  OO  puro  como  Java,  donde  se  usan  clases,
pero  estas  clases  solo  han  constituido  grandes  espacios  de  nombres  que  envuelven  un  programa  procedimental.  O  expresado  
con  un  poco  de  sarcasmo:  los  programas  similares  a  Fortran  se  pueden  escribir  en  casi  cualquier  lenguaje  de  programación,  
obviamente.  Por  otro  lado,  todo  desarrollador  que  haya  interiorizado  el  pensamiento  orientado  a  objetos  podrá  desarrollar  software  
con  un  diseño  orientado  a  objetos  incluso  en  lenguajes  como  ANSI­C,  Assembler  o  utilizando  scripts  de  shell.

134
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Abstracción:  la  clave  para  dominar  la  complejidad
La  idea  básica  detrás  de  OO  es  que  estamos  modelando  cosas  y  conceptos  de  partes  relevantes  de  nuestro  dominio  en  nuestro  
software.  Por  lo  tanto,  nos  limitamos  solo  a  aquellas  cosas  que  deben  estar  representadas  en  nuestro  sistema  de  software  para  
satisfacer  las  necesidades  de  las  partes  interesadas,  también  conocidas  como  requisitos.  La  abstracción  es  la  herramienta  más  
importante  para  modelar  estas  cosas  y  conceptos  de  manera  adecuada.  No  queremos  modelar  una  reproducción  de  todo  el  mundo  
real.  Solo  necesitamos  un  extracto  del  mundo  real,  reducido  a  los  detalles  que  son  relevantes  para  realizar  los  casos  de  uso  del  
sistema.
Por  ejemplo,  si  queremos  representar  a  un  cliente  en  un  sistema  de  librería,  es  muy  probable  y  no  interesa  en  absoluto  
qué  grupo  sanguíneo  tiene  el  cliente.  Por  otro  lado,  para  un  sistema  de  software  del  dominio  médico,  el  grupo  sanguíneo  de  un  ser  
humano  puede  ser  un  detalle  importante.
Para  mí,  la  orientación  a  objetos  se  trata  de  abstracción  de  datos,  responsabilidades,  modularización  y  también  de  divide  y  
vencerás.  Si  tengo  que  resumirlo,  diría  que  OO  se  trata  del  dominio  de  la  complejidad.  Me  explico  con  un  pequeño  ejemplo.

Considere  un  automóvil.  Un  automóvil  es  una  composición  de  varias  partes,  por  ejemplo,  carrocería,  motor,  engranajes,  
ruedas,  asientos,  etc.  Cada  una  de  estas  partes  también  consta  de  partes  más  pequeñas.  Tomemos  por  ejemplo  el  motor  del  
automóvil  (supongamos  que  es  un  motor  de  combustión  y  no  un  motor  eléctrico).  El  motor  consta  del  bloque  de  cilindros,  la  
bomba  de  encendido  de  gasolina,  el  eje  impulsor,  el  árbol  de  levas,  los  pistones,  una  unidad  de  control  del  motor  (ECU),  un  
subsistema  de  refrigerante,  etc.  El  subsistema  de  refrigerante  nuevamente  consta  de  un  intercambiador  de  calor,  una  bomba  de  
refrigerante,  depósito  de  refrigerante,  ventilador,  termostato  y  núcleo  del  calentador.  En  teoría,  la  descomposición  del  automóvil  
puede  continuar  hasta  el  tornillo  más  pequeño.  Y  cada  subsistema  o  parte  identificado  tiene  una  responsabilidad  bien  definida.  
Pero  solo  todas  las  partes  juntas  y  ensambladas  de  la  manera  correcta  construyen  un  automóvil  que  brinda  los  servicios  que  los  conductores  esper
Los  sistemas  de  software  complejos  se  pueden  considerar  de  la  misma  manera.  Se  pueden  descomponer  jerárquicamente.
en  módulos  de  grano  grueso  a  fino.  Eso  ayuda  a  hacer  frente  a  la  complejidad  del  sistema,  proporciona  más  flexibilidad  y  
fomenta  la  reutilización,  el  mantenimiento  y  la  capacidad  de  prueba.  Los  principios  rectores  para  hacer  esta  descomposición  
son  principalmente  los  siguientes:

•  Ocultación  de  información  (ver  la  sección  homónima  en  el  Capítulo  3),

•Fuerte  cohesión  (ver  la  sección  homónima  en  el  Capítulo  3),

•Acoplamiento  flojo  (consulte  la  sección  homónima  en  el  Capítulo  3),  y

• Principio  de  responsabilidad  única  (SRP;  consulte  la  sección  homónima  más  adelante  en  este  
capítulo).

Principios  para  un  buen  diseño  de  clases
El  mecanismo  generalizado  y  bien  conocido  para  la  formación  de  los  módulos  descritos  anteriormente  en  lenguajes  
orientados  a  objetos  es  el  concepto  de  clase.  Las  clases  se  consideran  módulos  de  software  encapsulados  que  combinan  
características  estructurales  (sinónimos:  atributos,  miembros  de  datos,  campos)  y  características  de  comportamiento  (sinónimos:  
funciones  de  miembros,  métodos,  operaciones)  en  una  unidad  cohesiva.
En  los  lenguajes  de  programación  con  funciones  orientadas  a  objetos  como  C++,  las  clases  son  el  siguiente  
concepto  estructurante  más  alto  que  las  funciones.  A  menudo  se  describen  como  los  planos  de  los  objetos  (sinónimo:  instancias).  
Esa  es  razón  suficiente  para  investigar  más  a  fondo  el  concepto  de  clases.  En  este  capítulo  doy  varias  pistas  importantes  para  
diseñar  y  escribir  buenas  clases  en  C++.

135
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Mantenga  las  clases  pequeñas

En  mi  carrera  como  desarrollador  de  software,  he  visto  muchas  clases  que  eran  muy  grandes.  Muchos  miles  de  líneas  de  código  no  eran  una  
rareza.  En  una  inspección  más  cercana,  noté  que  estas  clases  grandes  a  menudo  solo  se  usaban  como  espacios  de  nombres  para  un  
programa  más  o  menos  procedimental,  cuyos  desarrolladores  comúnmente  no  entendían  la  orientación  a  objetos.

Creo  que  los  problemas  con  clases  tan  grandes  son  obvios.  Si  las  clases  contienen  varios  miles  de  líneas  de  código,  son  difíciles  de  entender  
y  su  mantenibilidad  y  capacidad  de  prueba  suelen  ser  malas,  sin  mencionar  la  reutilización.  Y  según  varios  estudios,  las  clases  grandes  generalmente  
contienen  una  mayor  cantidad  de  defectos.

EL  ANTI­PATRÓN  DE  LA  CLASE  DIOS

En  muchos  sistemas,  existen  clases  excepcionalmente  grandes  con  muchos  atributos  y  varios  cientos  de  
operaciones.  Los  nombres  de  estas  clases  a  menudo  terminan  con  "...Controller",  "...Manager"  o  "...Helpers".
Los  desarrolladores  a  menudo  argumentan  que  en  algún  lugar  del  sistema  debe  haber  una  instancia  central  
que  mueva  los  hilos  y  coordine  todo.  Los  resultados  de  esta  forma  de  pensar  son  clases  gigantes  con  una  cohesión  
muy  pobre  (ver  la  sección  sobre  cohesión  fuerte  en  el  Capítulo  3).  Son  como  una  tienda  de  conveniencia  que  ofrece  
una  paleta  colorida  de  productos.

Tales  clases  se  llaman  Clases  de  Dios,  Objetos  de  Dios  o,  a  veces,  también  The  Blob  (The  Blob  es  una  película  
estadounidense  de  terror /  ciencia  ficción  de  1958  sobre  una  ameba  alienígena  que  se  come  a  los  ciudadanos  de  
una  aldea).  Este  es  el  llamado  Anti­Patrón,  un  sinónimo  de  lo  que  se  percibe  como  un  mal  diseño.  Una  clase  de  
Dios  es  una  bestia  indomable,  horrible  de  mantener,  difícil  de  entender,  no  comprobable,  propensa  a  errores  y  también  
tiene  una  gran  cantidad  de  dependencias  con  otras  clases.  Durante  el  ciclo  de  vida  del  sistema,  dichas  clases  son  
cada  vez  más  grandes.  Esto  empeora  los  problemas.

Lo  que  se  ha  demostrado  como  una  buena  regla  para  el  tamaño  de  una  función  (consulte  la  sección  "Que  sean  pequeños"  en  el  Capítulo  4),
lo  que  también  parece  ser  un  buen  consejo  para  el  tamaño  de  las  clases:  ¡ las  clases  deben  ser  pequeñas!
Si  el  tamaño  pequeño  es  un  objetivo  en  el  diseño  de  la  clase,  entonces  la  siguiente  pregunta  inmediata  es:  ¿Qué  tan  pequeño?
Para  funciones,  he  dado  un  número  de  líneas  de  código  en  el  Capítulo  4.  ¿No  sería  siquiera  posible  definir  un
número  de  líneas  para  clases  que  serían  percibidas  como  buenas  o  apropiadas?
En  The  ThoughtWorks®  Anthology  [ThoughtWorks08],  Jeff  Bay  contribuyó  con  un  ensayo  titulado  "Object  Calisthenics:  9  steps  to  
better  software  design  today"  que  recomienda  no  más  de  50  líneas  de  código  para  una  sola  clase.

Un  límite  superior  de  unas  50  líneas  parece  estar  fuera  de  discusión  para  muchos  desarrolladores.  Parece  que  sienten  una  especie  de  
resistencia  inexplicable  contra  la  creación  de  clases.  A  menudo  argumentan  de  la  siguiente  manera:  “¿No  más  de  50  líneas?  Pero  eso  dará  como  
resultado  una  gran  cantidad  de  pequeñas  clases  pequeñas,  con  solo  unos  pocos  miembros  y  funciones”.  Y  entonces  seguramente  evocarán  un  
ejemplo  irreductible  a  clases  de  tamaño  tan  reducido.
Estoy  convencido  de  que  esos  desarrolladores  están  totalmente  equivocados.  Estoy  bastante  seguro  de  que  cada  sistema  de  software  se  
puede  descomponer  en  bloques  de  construcción  elementales  tan  pequeños.
Sí,  si  las  clases  van  a  ser  pequeñas,  tendrá  más  de  ellas.  Pero  eso  es  OO!  En  el  desarrollo  de  software  orientado  a  objetos,  una  clase  
es  un  elemento  de  lenguaje  igualmente  natural,  como  una  función  o  una  variable.  En  otras  palabras:  no  tengas  miedo  de  crear  clases  pequeñas.  Las  
clases  pequeñas  son  mucho  más  fáciles  de  usar,  comprender  y  probar.
No  obstante,  eso  lleva  a  una  pregunta  fundamental:  ¿Es  la  definición  de  un  límite  superior  para  las  líneas  de  código  básicamente  la  forma  
correcta?  Creo  que  la  métrica  de  líneas  de  código  (LOC)  puede  ser  un  indicador  útil.  Demasiados  LOC  son  un  olor.  Puede  echar  un  vistazo  
cuidadoso  a  las  clases  con  más  de  50  líneas.  Pero  no  es  necesariamente  el  caso  que  muchas  líneas  de  código  sean  siempre  un  problema.  Un  
criterio  mucho  mejor  es  la  cantidad  de  responsabilidades  de  una  clase.

136
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Principio  de  responsabilidad  única  (PRS)
El  Principio  de  Responsabilidad  Única  (SRP)  establece  que  cada  unidad  de  software,  y  esto  incluye,  entre  otros,  componentes,  
clases  y  funciones,  debe  tener  una  sola  responsabilidad  única  y  bien  definida.
SRP  se  basa  en  el  principio  general  de  cohesión  que  he  discutido  en  el  Capítulo  3.  Si  una  clase  tiene  un
responsabilidad  bien  definida,  normalmente  también  su  cohesión  es  fuerte.
Pero,  ¿qué  es  exactamente  una  responsabilidad?  En  la  literatura  a  menudo  podemos  encontrar  la  explicación  de  que  solo  
debe  haber  una  razón  para  cambiar  de  clase.  Y  un  ejemplo  que  se  menciona  con  frecuencia  es  que  esta  regla  se  viola  cuando  es  
necesario  cambiar  la  clase  debido  a  requisitos  nuevos  o  modificados  para  diferentes  aspectos  del  sistema.
Estos  aspectos  pueden  ser,  por  ejemplo,  el  controlador  del  dispositivo  y  la  interfaz  de  usuario.  Si  se  debe  cambiar  la  
misma  clase,  ya  sea  porque  la  interfaz  del  controlador  del  dispositivo  ha  cambiado  o  se  debe  implementar  un  nuevo  requisito  con  
respecto  a  la  interfaz  gráfica  de  usuario,  entonces  esta  clase  obviamente  tiene  demasiadas  responsabilidades.
Otro  tipo  de  aspectos  se  relaciona  con  el  dominio  del  sistema.  Si  se  debe  cambiar  la  misma  clase,  ya  sea  porque  hay  
nuevos  requisitos  con  respecto  a  la  gestión  de  clientes,  o  hay  nuevos  requisitos  con  respecto  a  la  facturación,  entonces  esta  clase  
tiene  demasiadas  responsabilidades.
Las  clases  que  siguen  el  SRP  suelen  ser  pequeñas  y  tienen  pocas  dependencias.  Son  claros,  fáciles  de  entender  y  
se  pueden  probar  fácilmente.
La  responsabilidad  es  un  criterio  mucho  mejor  que  la  cantidad  de  líneas  de  código  de  una  clase.  Puede  haber  clases  
con  100,  200  o  incluso  500  líneas,  y  puede  estar  perfectamente  bien  si  esas  clases  no  violan  el  principio  de  responsabilidad  
única.  No  obstante,  un  recuento  alto  de  LOC  puede  ser  un  indicador.  Es  una  pista  que  dice:  “¡Deberías  echar  un  vistazo  a  
estas  clases!  Tal  vez  todo  esté  bien,  pero  tal  vez  son  tan  grandes  porque  tienen  demasiadas  responsabilidades”.

Principio  abierto­cerrado  (OCP)
Todos  los  sistemas  cambian  durante  sus  ciclos  de  vida.  Esto  debe  tenerse  en  cuenta  al  desarrollar  
sistemas  que  se  espera  que  duren  más  que  la  primera  versión.

—Ivar  Jacobson,  informático  sueco,  1992

Otra  pauta  importante  para  cualquier  tipo  de  unidad  de  software,  pero  especialmente  para  el  diseño  de  clases,  es  el  
Principio  Abierto  Cerrado  (OCP).  Establece  que  las  entidades  de  software  (módulos,  clases,  funciones,  etc.)  deben  estar  abiertas  
para  la  extensión,  pero  cerradas  para  la  modificación.
Es  un  hecho  simple  que  los  sistemas  de  software  evolucionarán  con  el  tiempo.  Deben  satisfacerse  constantemente  
nuevos  requisitos,  y  los  requisitos  existentes  deben  cambiarse  de  acuerdo  con  las  necesidades  del  cliente  o  el  progreso  de  la  tecnología.
Estas  extensiones  deben  hacerse  no  solo  de  manera  elegante  y  con  el  menor  esfuerzo  posible.  Deben  estar  hechos  especialmente  
de  tal  manera  que  no  sea  necesario  cambiar  el  código  existente.  Sería  fatal  si  cualquier  requisito  nuevo  diera  lugar  a  una  cascada  
de  cambios  y  ajustes  en  partes  del  software  existentes  y  bien  probadas.

Una  forma  de  apoyar  este  principio  en  la  orientación  a  objetos  es  el  concepto  de  herencia.  Con  herencia  es  posible  agregar  
nueva  funcionalidad  a  una  clase  sin  modificar  esa  clase.  Además,  hay  muchos  patrones  de  diseño  orientados  a  objetos  que  
fomentan  OCP,  como  Estrategia  o  Decorador  (consulte  el  Capítulo  9) .  sobre  patrones  de  diseño).

En  la  sección  sobre  acoplamiento  suelto  en  el  Capítulo  3  ya  hemos  discutido  un  diseño  que  soporta  OCP
muy  bien  (ver  Figura  3­6).  Allí  hemos  desacoplado  un  interruptor  y  una  lámpara  a  través  de  una  interfaz.  A  través  de  este  
paso,  el  diseño  está  cerrado  contra  modificaciones  pero  agradablemente  abierto  para  extensiones.  Podemos  agregar  más  
dispositivos  conmutables  fácilmente,  y  no  necesitamos  tocar  las  clases  Switch,  Lamp  y  la  interfaz  Switchable.
Y  como  puede  imaginar  fácilmente,  otra  ventaja  de  dicho  diseño  es  que  ahora  es  muy  fácil  proporcionar  un  Doble  de  prueba  (por  
ejemplo,  un  objeto  simulado)  con  fines  de  prueba  (consulte  la  sección  sobre  Dobles  de  prueba  (Objetos  falsos)  en  el  Capítulo  2) .

137
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Principio  de  sustitución  de  Liskov  (LSP)

Básicamente,  el  principio  de  sustitución  de  Liskov  establece  que  no  se  puede  crear  un  pulpo  
extendiendo  un  perro  con  cuatro  patas  falsas  adicionales.

—Mario  Fusco  (@mariofusco),  15  de  septiembre  de  2013,  en  Twitter

Los  conceptos  clave  orientados  a  objetos  de  herencia  y  polimorfismo  parecen  relativamente  simples  a  primera  vista.
La  herencia  es  un  concepto  taxonómico  que  debe  usarse  para  construir  una  jerarquía  de  especialización  de  tipos,  es  decir,  los  subtipos  
se  derivan  de  un  tipo  más  general.  Polimorfismo  significa,  en  general,  que  se  proporciona  una  sola  interfaz  como  posibilidad  de  acceso  a  
objetos  de  diferentes  tipos.
Hasta  ahora,  todo  bien.  Pero  a  veces  te  encuentras  en  situaciones  en  las  que  un  subtipo  realmente  no  quiere  encajar  en  un
jerarquía  de  tipos.  Discutamos  un  ejemplo  muy  popular  que  se  usa  a  menudo  para  ilustrar  el  problema.

El  dilema  cuadrado­rectángulo
Supongamos  que  estamos  desarrollando  una  biblioteca  de  clases  con  tipos  primitivos  de  formas  para  dibujar  en  un  lienzo,  por  ejemplo,  
un  círculo,  un  rectángulo,  un  triángulo  y  una  etiqueta  de  texto.  Visualizada  como  un  diagrama  de  clases  UML,  esta  biblioteca  podría  
parecerse  a  la  Figura  6­1.

Figura  6­1.  Una  biblioteca  de  clases  de  diferentes  formas.

138
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

La  clase  base  abstracta  Forma  tiene  atributos  y  operaciones  que  son  iguales  para  todas  las  formas  específicas.  Para
Por  ejemplo,  es  lo  mismo  para  todas  las  formas  cómo  se  pueden  mover  de  una  posición  a  otra  en  el  lienzo.  Sin  
embargo,  la  Forma  no  puede  saber  cómo  se  pueden  mostrar  (sinónimo:  dibujar)  u  ocultar  (sinónimo:  borrar)  formas  
específicas.  Por  lo  tanto,  estas  operaciones  son  abstractas,  es  decir,  no  pueden  implementarse  (totalmente)  en  Shape.

En  C++,  una  implementación  de  la  clase  abstracta  Shape  (y  la  clase  Point  que  requiere  Shape)  podría  verse  así:

Listado  6­1.  Así  es  como  se  ven  las  dos  clases  Punto  y  Forma

clase  Punto  final  { público:

Punto() :  x  { 5 },  y  { 5 }  { }
Punto  (const  unsigned  int  initialX,  const  unsigned  int  initialY):
x  { initialX },  y  { initialY }  { }  void  
setCoordinates(const  unsigned  int  newX,  const  unsigned  int  newY)  {
x  =  nuevoX;  
y  =  

nuevoY; } // ...más  funciones  miembro  aquí...

privado:  
sin  firmar  int  x;  int  
sin  firmar  y; };

forma  de  clase  
{ público:
Shape() :  isVisible  { false }  { }  virtual  
~Shape()  =  predeterminado;  void  
moveTo(const  Point&  newCenterPoint)  {

esconder();  centerPoint  =  

newCenterPoint;  espectáculo(); }  
espectáculo  vacío  virtual  ()  =  0;  
ocultar  vacío  virtual  ()  =  0; // ...

privado:
Punto  centroPunto;  
bool  esVisible; };

void  Shape::show()  
{ isVisible  =  true; }

void  Shape::hide()  
{ isVisible  =  false; }

139
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

ESPECIFICADOR  FINAL  [C++11]

El  especificador  final ,  disponible  desde  C++11,  se  puede  utilizar  de  dos  formas.

Por  un  lado,  puede  usar  este  especificador  para  evitar  que  las  funciones  de  miembros  virtuales  individuales  se  anulen  
en  clases  derivadas,  como  en  este  ejemplo:

clase  AbstractBaseClass  
{ public:  
virtual  void  hacerAlgo()  =  0; };

class  Derived1 :  public  AbstractBaseClass  { public:  
virtual  
void  doSomething()  final  {
//...

} };

class  Derived2 :  public  Derived1  { public:  
virtual  
void  doSomething()  override  { //  ¡Causa  un  error  de  compilación!
//...

} };

Además,  también  puede  marcar  una  clase  completa  como  final,  como  la  clase  Point  en  nuestra  biblioteca  Shape.
Esto  garantiza  que  un  desarrollador  no  pueda  usar  una  clase  de  este  tipo  como  clase  base  para  la  herencia.

clase  no  derivable  final  { // ... };

De  todas  las  clases  concretas  de  la  biblioteca  Shapes,  podemos  echar  un  vistazo  ejemplar  a  una  clase,  la
Rectángulo:

Listado  6­2.  Las  partes  importantes  de  la  clase  Rectángulo

clase  Rectángulo:  Forma  pública  
{ pública:
Rectángulo() :  ancho  { 2 },  alto  { 1 }  { }
Rectangle(const  unsigned  int  initialWidth,  const  unsigned  int  initialHeight) :
ancho  {anchurainicial},  altura  {alturainicial}  { }

virtual  void  show()  override  

{ Shape::show(); // ...código  para  mostrar  un  
rectángulo  aquí... }

140
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

virtual  void  hide()  override  

{ Shape::hide(); // ...código  para  ocultar  un  
rectángulo  aquí... }

void  setWidth(const  unsigned  int  newWidth)  { ancho  =  
newWidth; }

void  setHeight(const  unsigned  int  newHeight)  { altura  =  
newHeight; }

void  setEdges(const  unsigned  int  newWidth,  const  unsigned  int  newHeight)  { ancho  =  newWidth;  
altura  =  alturanueva; }

unsigned  long  long  getArea()  const  {
return  static_cast<unsigned  long  long>(ancho)  *  altura; } // ...

privado:  
ancho  int  sin  firmar ;  
altura  int  sin  firmar ; };

El  código  del  cliente  quiere  usar  todas  las  formas  de  manera  similar,  sin  importar  con  qué  instancia  en  
particular  (Rectángulo,  Círculo,  etc.)  se  enfrente.  Por  ejemplo,  todas  las  formas  deben  mostrarse  en  un  lienzo  de  una  
sola  vez,  lo  que  se  puede  lograr  usando  el  siguiente  código:

#include  "Formas.h" //  Círculo,  Rectángulo,  etc.  #include  
<memoria>  #include  
<vector>

utilizando  ShapePtr  =  std::shared_ptr<Forma>;  
usando  ShapeCollection  =  std::vector<ShapePtr>;

void  showAllShapes  (const  ShapeCollection  y  formas)  {
para  (auto  y  forma:  formas)  {
forma­>mostrar(); }

int  principal()  {

Formas  ShapeCollection;  
formas.push_back(std::make_shared<Círculo>());  
formas.push_back(std::make_shared<Rectangle>());  
formas.push_back(std::make_shared<TextLabel>());

141
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

// ...etc...

mostrarTodasLasFormas(formas);  
devolver  0;
}

Y  ahora  supongamos  que  los  usuarios  formulan  un  nuevo  requisito  para  nuestra  biblioteca:  ¡ quieren  tener  un  
cuadrado!
Probablemente  todos  recuerden  de  inmediato  sus  lecciones  de  geometría  en  la  escuela  primaria.  En  ese  momento  
también  tu  maestro  quizás  haya  dicho  que  un  cuadrado  es  un  tipo  especial  de  rectángulo  que  tiene  cuatro  lados  de  igual  
longitud  y  cuatro  ángulos  iguales  (ángulos  de  90  grados).  Por  lo  tanto,  una  primera  solución  obvia  parece  ser  derivar  una  
nueva  clase  Square  de  Rectangle,  como  se  muestra  en  la  figura  6­2.

Figura  6­2.  Derivar  un  cuadrado  de  la  clase  Rectángulo:  ¿una  buena  idea?

A  primera  vista,  esta  parece  ser  una  solución  factible.  Square  hereda  la  interfaz  y  la  implementación  de  
Rectangle.  Esto  es  bueno  para  evitar  la  duplicación  de  código  (vea  el  principio  DRY  que  hemos  discutido  en  el  Capítulo  3),  
porque  Square  puede  reutilizar  fácilmente  el  comportamiento  que  se  implementa  en  Rectangle.
Y  un  cuadrado  solo  tiene  que  cumplir  un  requisito  adicional  y  simple  que  se  muestra  en  el  diagrama  UML
arriba  como  una  restricción  en  la  clase  Square:  {ancho  =  alto}.  Esta  restricción  significa  que  una  instancia  de  tipo  Square  
asegura  en  todas  las  circunstancias  que  sus  bordes  siempre  tengan  la  misma  longitud.
Entonces,  primero  implementamos  nuestro  Cuadrado  derivándolo  de  nuestro  Rectángulo:

clase  Cuadrado:  Rectángulo  público  

{ público: //...

Pero,  de  hecho,  ¡no  es  una  buena  solución!

142
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Tenga  en  cuenta  que  el  Cuadrado  hereda  todas  las  operaciones  del  Rectángulo.  Eso  significa  que  podemos  hacer  el
siguiendo  con  una  instancia  de  Square:

cuadrado  cuadrado;  
cuadrado.setHeight(10); //  Err...  ¡¿Cambiar  solo  la  altura  de  un  cuadrado?!  cuadrado.setEdges(10,  
20); //  ¡UH  oh!

En  primer  lugar,  sería  muy  desconcertante  para  los  usuarios  de  Square  que  proporcione  un  setter  con  dos  parámetros
(recuerde  el  Principio  del  Mínimo  Asombro  en  el  Capítulo  3).  Ellos  piensan:  ¿Por  qué  hay  dos  parámetros?
¿Qué  parámetro  se  utiliza  para  establecer  la  longitud  de  todos  los  bordes?  ¿Debo  quizás  poner  ambos  parámetros  al  mismo  valor?
¿Qué  pasa  si  no  lo  hago?
La  situación  es  aún  más  dramática  cuando  hacemos  lo  siguiente:

std::unique_ptr<Rectángulo>  rectángulo  =  std::make_unique<Cuadrado>(); // ...y  en  algún  
otro  lugar  del  código...  rectángulo­>setEdges(10,  20);

En  este  caso,  el  código  del  cliente  usa  un  setter  que  tiene  sentido.  Ambos  bordes  de  un  rectángulo  se  pueden  manipular.
independientemente.  Eso  no  es  una  sorpresa;  es  exactamente  la  expectativa.  Sin  embargo,  el  resultado  puede  ser  extraño.  
La  instancia  de  tipo  Square  ya  no  sería  un  cuadrado  después  de  tal  llamada,  porque  tiene  dos  longitudes  de  borde  diferentes.  Así  
que  hemos  cometido  una  vez  más  una  violación  del  Principio  del  Mínimo  Asombro,  y  mucho  peor:  violado  el  invariante  de  
clase  del  Cuadrado.
Sin  embargo,  ahora  se  podría  argumentar  que  podemos  declarar  setEdges(),  setWidth()  y  setHeight()  como  virtuales  
en  la  clase  Rectangle  y  anular  estas  funciones  miembro  en  la  clase  Square  con  una  implementación  alternativa,  que  
genera  una  excepción  en  caso  de  uso  no  solicitado.  Además,  proporcionamos  una  nueva  función  miembro  setEdge()  en  la  
clase  Square  en  su  lugar,  de  la  siguiente  manera:

Listado  6­3.  Una  implementación  realmente  mala  de  Square  que  intenta  "borrar"  funciones  heredadas  no  deseadas

#include  <stdexcept> // ...

class  IllegalOperationCall :  public  std::logic_error  { public:  explícito  

IllegalOperationCall(const  std::string&  message) :  logic_error(message)  { }  virtual  ~IllegalOperationCall()  { } };

clase  Cuadrado:  Rectángulo  público  { público:

Cuadrado() :  Rectángulo  { 5,  5 }  { }  explícito  
Cuadrado(const  unsigned  int  edgeLength) :  Rectangle  { edgeLength,  edgeLength }  { }

virtual  void  setEdges([[maybe_unused]]  const  unsigned  int  newWidth,  [[maybe_unused]]  const  
unsigned  int  newHeight)  override  {
lanzar  IllegalOperationCall  { ILLEGAL_OPERATION_MSG }; }

virtual  void  setWidth([[maybe_unused]]  const  unsigned  int  newWidth)  override  {
lanzar  IllegalOperationCall  { ILLEGAL_OPERATION_MSG }; }

143
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

virtual  void  setHeight([[maybe_unused]]  const  unsigned  int  newHeight)  override  {
lanzar  IllegalOperationCall  { ILLEGAL_OPERATION_MSG }; }

void  setEdge( longitud  int  sin  signo  const)  {
Rectángulo::setEdges(longitud,  longitud); }

private:  
static  const  constexpr  char*  const  ILLEGAL_OPERATION_MSG  { "Llamada  no  solicitada  de  una  "operación  prohibida  en  
"
una  instancia  
de  clase  Square!" };

Bueno,  creo  que  es  obvio  que  ese  sería  un  diseño  terriblemente  malo.  Viola  un  principio  fundamental  de  la  orientación  a  objetos,  
que  una  clase  derivada  no  debe  eliminar  las  propiedades  heredadas  de  su  clase  base.  Definitivamente  no  es  una  solución  a  nuestro  
problema.  Primero,  el  nuevo  setter  setEdge()  no  sería  visible  si  queremos  usar  una  instancia  de  Square  como  Rectangle.  Además,  
todos  los  demás  setters  lanzan  una  excepción  si  se  usan,  ¡esto  es  realmente  abismal!  Arruinó  la  orientación  a  objetos.

Entonces,  ¿cuál  es  el  problema  fundamental  aquí?  ¿Por  qué  la  derivación  obviamente  sensata  de  una  clase  Cuadra
de  un  Rectángulo  causan  tantas  dificultades?
La  explicación  es  esta:  derivar  Square  de  Rectangle  viola  un  principio  importante  en  object
diseño  de  software  orientado:  ¡el  principio  de  sustitución  de  Liskov  (LSP)!
Barbara  Liskov,  una  científica  informática  estadounidense  que  es  profesora  de  instituto  en  la  Universidad  de  Massachusetts
Institute  of  Technology  (MIT),  y  Jeannette  Wing,  quien  fue  profesora  del  presidente  de  Ciencias  de  la  Computación  en
Carnegie  Mellon  University  hasta  2013,  formuló  el  principio  en  un  documento  de  1994  de  la  siguiente  manera:

Sea  q(x)  una  propiedad  demostrable  sobre  objetos  x  de  tipo  T.  Entonces  q(y)  debería  ser  demostrable  
para  objetos  y  de  tipo  S,  donde  S  es  un  subtipo  de  T.

—Barbara  Liskov,  Jeanette  Wing  [Liskov94]

Bueno,  esa  no  es  necesariamente  una  definición  para  el  uso  diario.  Robert  C.  Martin  formuló  este  principio  en
un  artículo  en  1996  de  la  siguiente  manera:

Las  funciones  que  usan  punteros  o  referencias  a  clases  base  deben  poder  usar  objetos  de  clases  
derivadas  sin  saberlo.

—Robert  C.  Martín  [Martin96]

De  hecho,  eso  significa  lo  siguiente:  los  tipos  derivados  deben  ser  completamente  sustituibles  por  sus  tipos  base.
En  nuestro  ejemplo  esto  no  es  posible.  Una  instancia  de  tipo  Cuadrado  no  puede  sustituir  un  Rectángulo.  La  razón  de  esto  radica  en  la  
restricción  {ancho  =  alto}  (una  llamada  invariante  de  clase)  que  sería  impuesta  por  el  Cuadrado,  pero  el  Rectángulo  no  puede  cumplir  
con  esa  restricción.
El  principio  de  sustitución  de  Liskov  estipula  las  siguientes  reglas  para  las  jerarquías  de  tipos  y  clases:

•Las  condiciones  previas  (consulte  también  la  sección  "La  prevención  es  mejor  que  el  cuidado  posterior"  en  el  
Capítulo  5  sobre  las  condiciones  previas)  de  una  clase  base  no  se  pueden  fortalecer  en  una  subclase  derivada.

•  Condiciones  posteriores  (ver  también  la  sección  “La  prevención  es  mejor  que  el  cuidado  posterior”  en  el  Capítulo  5)
de  una  clase  base  no  se  puede  debilitar  en  una  subclase  derivada.

144
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

•Todas  las  invariantes  de  una  clase  base  no  deben  ser  cambiadas  o  violadas  a  través  de  una  derivada
subclase.

•La  restricción  del  historial  (también  conocida  como  la  "regla  del  historial"):  el  estado  (interno)  de  los  objetos
solo  debe  cambiarse  mediante  llamadas  a  métodos  en  su  interfaz  pública  (encapsulación).
Dado  que  las  clases  derivadas  pueden  introducir  nuevos  atributos  y  métodos  que  no  existen  en  la  clase  
base,  la  introducción  de  estos  métodos  puede  permitir  cambios  de  estado  en  los  objetos  de  la  clase  derivada  
que  no  están  permitidos  en  la  clase  base.  La  llamada  restricción  Historial  prohíbe  esto.  Por  ejemplo,  
si  la  clase  base  está  diseñada  para  ser  el  modelo  de  un  objeto  inmutable  (consulte  el  Capítulo  9  sobre  clases  
inmutables),  la  clase  derivada  no  debería  invalidar  esta  propiedad  de  inmutabilidad  con  la  ayuda  de  las  
funciones  miembro  recién  introducidas.

La  interpretación  de  la  relación  de  generalización  (la  flecha  entre  Cuadrado  y  Rectángulo)  en  el
El  diagrama  de  clases  anterior  (Figura  6­2)  a  menudo  se  traduce  como  “…ES  UN…”:  Square  IS  A  Rectangle.  Pero  eso  podría  ser  
engañoso.  En  Matemáticas  es  posible  decir  que  un  cuadrado  es  un  tipo  especial  de  rectángulo,  ¡pero  en  programación  no  lo  es!

Para  hacer  frente  a  este  problema,  el  cliente  tiene  que  saber  con  qué  tipo  específico  está  trabajando.  Algunos  
desarrolladores  ahora  podrían  decir:  "No  hay  problema,  esto  se  puede  hacer  usando  información  de  tipo  de  tiempo  de  ejecución  (RTTI)".

INFORMACIÓN  DE  TIPO  DE  TIEMPO  DE  EJECUCIÓN  (RTTI)

El  término  Información  de  tipo  de  tiempo  de  ejecución  (a  veces  también  Identificación  de  tipo  de  tiempo  de  ejecución)  denota  un  
mecanismo  de  C++  para  acceder  a  información  sobre  el  tipo  de  datos  de  un  objeto  en  tiempo  de  ejecución.  El  concepto  general  
detrás  de  RTTI  se  denomina  introspección  de  tipos  y  también  está  disponible  en  otros  lenguajes  de  programación,  como  Java.

En  C++,  el  operador  typeid  (definido  en  header  <typeinfo>)  y  dynamic_cast<T>  (consulte  la  sección  sobre  conversiones  de  C++  en  
el  Capítulo  4)  pertenecen  a  RTTI.  Por  ejemplo,  para  determinar  la  clase  de  un  objeto  en  tiempo  de  ejecución,  puede  escribir:

const  std::type_info&  typeInformationAboutObject  =  typeid(instancia);

La  referencia  const  de  tipo  std::type_info  (también  definida  en  el  encabezado  <typeinfo>)  ahora  contiene  información  
sobre  la  clase  del  objeto,  por  ejemplo,  el  nombre  de  la  clase.  Desde  C++11,  también  está  disponible  un  código  hash  
(std::type_info::hash_code()),  que  es  idéntico  para  los  objetos  std::type_info  que  se  refieren  al  mismo  tipo.

Es  importante  saber  que  RTTI  está  disponible  solo  para  clases  que  son  polimórficas,  es  decir,  para  clases  que  tienen  al  menos  
una  función  virtual,  ya  sea  directamente  o  por  herencia.  Además,  RTTI  se  puede  activar  o  desactivar  en  algunos  compiladores.  
Por  ejemplo,  cuando  se  usa  gcc  (GNU  Compiler  Collection),  RTTI  se  puede  deshabilitar  usando  la  opción  ­fno­rtti .

145
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Listado  6­4.  Solo  otro  "truco":  usar  RTTI  para  distinguir  entre  diferentes  tipos  de  forma  durante  el  tiempo  de  ejecución

utilizando  ShapePtr  =  std::shared_ptr<Forma>;  usando  
ShapeCollection  =  std::vector<ShapePtr>; //...

void  resizeAllShapes  (const  ShapeCollection  y  formas)  {
pruebe  
{ para  (const  auto&  forma:  formas)  { const  auto  
rawPointerToShape  =  forma.get();  if  (typeid(*rawPointerToShape)  
==  typeid(Rectángulo))  {
Rectángulo*  rectángulo  =  dynamic_cast<Rectangle*>(rawPointerToShape);  rectángulo­
>establecerBordes(10,  20); //  Haz  más  
cosas  específicas  de  Rectángulo  aquí...
}  else  if  (typeid(*rawPointerToShape)  ==  typeid(Square))  {
Square*  square  =  dynamic_cast<Square*>(rawPointerToShape);  cuadrado­
>establecerBorde(10); }  más  
{ // ...

} }
}  captura  (const  std::bad_typeid&  ex)  {
//  ¡Intenté  un  typeid  de  puntero  NULL! } }

¡No  hagas  esto!  Esta  no  puede,  y  no  debería,  ser  la  solución  adecuada,  especialmente  en  un  programa  C++  limpio  y  moderno.  
Se  contrarrestan  muchos  de  los  beneficios  de  la  orientación  a  objetos,  como  el  polimorfismo  dinámico.

■  Precaución  Cada  vez  que  se  vea  obligado  a  utilizar   en  su  programa  para  distinguir  entre  diferentes  tipos,
RTTI  es  un  claro  "olor  de  diseño",  es  decir,  un  indicador  obvio  de  un  mal  diseño  de  software  orientado  a  objetos.

Además,  nuestro  código  estará  muy  contaminado  con  pésimas  construcciones  if­else  y  la  legibilidad  disminuirá.
por  el  desagüe.  Y  como  si  esto  no  fuera  suficiente,  la  construcción  try­catch  también  deja  en  claro  que  algo  podría  salir  mal.

¿Pero  que  podemos  hacer?

En  primer  lugar,  deberíamos  echar  otra  mirada  cuidadosa  a  lo  que  realmente  es  un  cuadrado.
Desde  un  punto  de  vista  matemático  puro,  un  cuadrado  puede  considerarse  como  un  rectángulo  con  lados  de  igual  longitud.  
Hasta  ahora,  todo  bien.  Pero  esta  definición  no  se  puede  transferir  directamente  a  una  jerarquía  de  tipos  orientada  a  objetos.  ¡Un  
cuadrado  no  es  un  subtipo  de  un  rectángulo!
En  cambio,  tener  una  forma  cuadrada  es  simplemente  un  estado  especial  de  un  rectángulo.  Si  un  rectángulo  tiene  longitudes  de  
borde  idénticas,  que  es  únicamente  un  estado  del  rectángulo,  generalmente  le  damos  a  ese  rectángulo  en  particular  un  nombre  especial  en  
nuestro  lenguaje  natural:  ¡entonces  hablamos  de  un  cuadrado!
Eso  significa  que  solo  necesitamos  agregar  un  método  de  inspección  a  nuestra  clase  Rectangle  para  consultar  su  estado,  lo  
que  nos  permite  renunciar  a  una  clase  Square  explícita.  De  acuerdo  con  el  principio  KISS  (ver  Capítulo  3),  esta  solución  podría  ser  
completamente  suficiente  para  satisfacer  el  nuevo  requisito.  Además,  podemos  proporcionar  a  los  clientes  un  método  de  ajuste  
conveniente  para  configurar  ambas  longitudes  de  borde  por  igual.

146
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Listado  6­5.  Una  solución  simple  sin  una  clase  explícita  Square

clase  Rectángulo:  Forma  pública  

{ pública: // ...
void  setEdgesToEqualLength(const  unsigned  int  newLength)  {
setEdges(nuevaLongitud,  nuevaLongitud); }

bool  esCuadrado()  const  {
retorno  ancho  ==  altura; } //...

Favorecer  la  composición  sobre  la  herencia
Pero,  ¿qué  podemos  hacer  si  se  requiere  inflexiblemente  un  Cuadrado  de  clase  explícito,  por  ejemplo,  porque  alguien  
lo  exige?  Bueno,  si  ese  es  el  caso,  entonces  nunca  deberíamos  heredar  de  Rectangle,  sino  de  la  clase  Shape,  
como  se  muestra  en  la  Figura  6­3.  Para  no  violar  el  principio  DRY,  usamos  una  instancia  de  la  clase  Rectangle  para  
la  implementación  interna  de  Square.

Figura  6­3.  Square  usa  y  delega  a  una  instancia  incrustada  de  Rectangle

Expresado  en  código  fuente,  la  implementación  de  esta  clase  Square  se  vería  así:

Listado  6­6.  Square  delega  todas  las  llamadas  de  método  a  una  instancia  incrustada  de  Rectangle

clase  Cuadrado:  Forma  pública  
{ público:  
Cuadrado  ()  
{ impl.setEdges  (5,  5); }

147
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Cuadrado  explícito  (const  unsigned  int  edgeLength)  
{ impl.setEdges  (edgeLength,  edgeLength); }

void  setEdge  
( longitud  int  sin  signo  const )  {
impl.setEdges(longitud,  longitud); }

virtual  void  moveTo(const  Point&  newCenterPoint)  override  
{ impl.moveTo(newCenterPoint); }

virtual  void  show()  override  { impl.show(); }

virtual  void  hide()  override  { impl.hide(); }

unsigned  lomg  longgetArea()  const  { return  
impl.getArea(); }

privado:
Rectángulo  impl; };

Tal  vez  haya  notado  que  el  método  moveTo()  también  se  sobrescribió.  Para  ello,  el  método  moveTo()  también  
debe  hacerse  virtual  en  la  clase  Shape.  Debemos  anularlo,  porque  moveTo()  heredado  de  Shape  opera  en  el  centerPoint  
de  la  clase  base  Shape,  y  no  en  la  instancia  incrustada  del  Rectangle  utilizado.  Este  es  un  pequeño  inconveniente  de  
esta  solución:  algunas  partes  heredadas  de  la  clase  base  Shape  quedan  en  barbecho.

Obviamente,  con  esta  solución  perderemos  la  posibilidad  de  que  una  instancia  de  Square  pueda  ser  asignada  a  un  
Rectangle:

std::unique_ptr<Rectángulo>  rectángulo  =  std::make_unique<Cuadrado>(); //  ¡Error  del  compilador!

El  principio  detrás  de  esta  solución  para  hacer  frente  a  los  problemas  de  herencia  en  OO  se  llama  
"Favorecer  la  composición  sobre  la  herencia" (FCoI),  a  veces  también  llamado  "Favorecer  la  delegación  sobre  la  
herencia".  Para  la  reutilización  de  la  funcionalidad,  la  programación  orientada  a  objetos  tiene  básicamente  dos  
opciones:  herencia  (“reutilización  de  caja  blanca”)  y  composición  o  delegación  (“reutilización  de  caja  negra”).  A  veces  es  
mejor  tratar  otro  tipo  como  si  fuera  una  caja  negra,  es  decir,  usarlo  solo  a  través  de  su  interfaz  pública  bien  definida,  
en  lugar  de  derivar  un  subtipo  de  este  tipo.  La  reutilización  por  composición/delegación  fomenta  un  acoplamiento  más  
flexible  entre  clases  que  la  reutilización  por  herencia.

148
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Principio  de  segregación  de  interfaz  (ISP)
Ya  conocemos  las  interfaces  como  una  forma  de  fomentar  el  acoplamiento  flexible  entre  clases.  En  una  sección  anterior  sobre  
el  Principio  Abierto­Cerrado,  vimos  que  las  interfaces  son  una  forma  de  tener  un  punto  de  extensión  y  variación  en  el  
código.  Una  interfaz  es  como  un  contrato:  las  clases  pueden  solicitar  servicios  a  través  de  este  contrato,  que  pueden  ser  
ofrecidos  por  otras  clases  que  cumplen  el  contrato.
Pero,  ¿qué  problemas  pueden  surgir  cuando  estos  contratos  se  vuelven  demasiado  extensos,  es  decir,  si  una  interfaz  se  vuelve
demasiado  amplio  o  "gordo"?  Las  consecuencias  se  pueden  demostrar  mejor  con  un  ejemplo.  Supongamos  que  tenemos  
la  siguiente  interfaz:

Listado  6­7.  Una  interfaz  para  Birds

clase  Pájaro  
{ público:  
virtual  ~  Pájaro  ()  =  predeterminado;

vuelo  vacío  virtual  ()  =  0;  vacío  
virtual  comer()  =  0;  ejecución  
de  vacío  virtual  ()  =  0;  tweet  
vacío  virtual  ()  =  0; };

Esta  interfaz  es  implementada  por  varios  pájaros  concretos,  por  ejemplo,  por  un  gorrión.

Listado  6­8.  La  clase  Sparrow  anula  e  implementa  todas  las  funciones  de  miembros  virtuales  puros  de  Bird

class  Sparrow :  public  Bird  { public:  
virtual  
void  fly()  override  { //...

}  virtual  void  eat()  override  { //...

}  virtual  void  run()  override  { //...

}  invalidación  de  tweet  virtual  vacío  ()  {
//...

} };

Hasta  ahora,  todo  bien.  Y  ahora  supongamos  que  tenemos  otro  pájaro  concreto:  un  pingüino.

Listado  6­9.  El  pinguino  de  la  clase

class  Penguin :  public  Bird  { public:  
virtual  
void  fly()  override  { // ???

} //... };

149
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Aunque  un  pingüino  es  inequívocamente  un  pájaro,  no  puede  volar.  Aunque  nuestra  interfaz  es  relativamente  
pequeña,  porque  declara  solo  cuatro  funciones  de  miembros  simples,  estos  servicios  declarados  no  pueden,  obviamente,  ser  
ofrecidos  por  cada  especie  de  ave.
El  Principio  de  Segregación  de  Interfaz  (ISP)  establece  que  una  interfaz  no  debe  estar  inflada  con  miembros
funciones  que  no  son  requeridas  por  las  clases  de  implementación,  o  que  estas  clases  no  pueden  implementar  de  manera  
significativa.  En  nuestro  ejemplo  anterior,  la  clase  Penguin  no  puede  proporcionar  una  implementación  significativa  
para  Bird::fly(),  pero  se  obliga  a  Penguin  a  sobrescribir  esa  función  miembro.
El  Principio  de  Segregación  de  la  Interfaz  dice  que  debemos  segregar  una  “interfaz  gorda”  en  partes  más  pequeñas  y
interfaces  altamente  cohesivas.  Las  pequeñas  interfaces  resultantes  también  se  conocen  como  interfaces  de  roles.

Listado  6­10.  Las  tres  interfaces  de  roles  como  una  mejor  alternativa  a  la  amplia  interfaz  de  Bird

class  Lifeform  { public:  
virtual  
void  eat()  =  0;  movimiento  vacío  
virtual  ()  =  0; };

clase  Flyable  { public:  
virtual  
void  fly()  =  0; };

class  Audible  { public:  
virtual  
void  makeSound()  =  0; };

Estas  interfaces  de  roles  pequeños  ahora  se  pueden  combinar  de  manera  muy  flexible.  Esto  significa  que  las  clases  
de  implementación  solo  necesitan  proporcionar  una  funcionalidad  significativa  para  aquellas  funciones  miembro  declaradas,  que  
pueden  implementar  de  manera  sensata.

Listado  6­11.  Las  clases  Sparrow  y  Penguin  implementan  respectivamente  las  interfaces  relevantes

clase  Sparrow:  forma  de  vida  pública ,  voladora  pública ,  audible  pública  {
//... };

clase  Pingüino:  forma  de  vida  pública ,  Audible  pública  {
//... };

Principio  de  dependencia  acíclica  A  veces  existe  la  

necesidad  de  que  dos  clases  se  “conozcan”  entre  sí.  Por  ejemplo,  supongamos  que  estamos  desarrollando  una  tienda  
web.  Para  que  se  puedan  implementar  ciertos  casos  de  uso,  la  clase  que  representa  a  un  cliente  en  esta  tienda  web  debe  
conocer  su  cuenta  relacionada.  Para  otros  casos  de  uso  es  necesario  que  la  cuenta  pueda  acceder  a  su  titular,  que  es  un  
cliente.
En  UML,  esta  relación  mutua  se  parece  a  la  de  la  figura  6­4.

150
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Figura  6­4.  Las  relaciones  de  asociación  entre  la  clase  Cliente  y  la  clase  Cuenta

Esto  se  conoce  como  una  dependencia  circular.  Ambas  clases,  ya  sea  directa  o  indirectamente,  dependen  una  de  la  otra.
Y  en  este  caso,  solo  hay  dos  clases.  Las  dependencias  circulares  también  pueden  ocurrir  con  varias  unidades  de  software  involucradas.

Veamos  cómo  se  puede  implementar  en  C++  la  dependencia  circular  que  se  muestra  en  la  figura  6­4.
Lo  que  definitivamente  no  funcionaría  en  C++  es  lo  siguiente:

Listado  6­12.  El  contenido  del  archivo  Customer.h

#ifndef  CLIENTE_H_  
#define  CLIENTE_H_

#include  "Cuenta.h"

class  Cliente  { // ...  
private:  
Cuenta  
cuentacliente; };

#terminara  si

Listado  6­13.  El  contenido  del  archivo  Account.h

#ifndef  CUENTA_H_  
#define  CUENTA_H_

#include  "Cliente.h"

clase  Cuenta  { privada:

propietario  del  
cliente; };

#terminara  si

Creo  que  el  problema  es  obvio  aquí.  Tan  pronto  como  alguien  usa  la  clase  Cuenta,  o  la  clase  Cliente,  él
desencadenaría  una  reacción  en  cadena  durante  la  compilación.  Por  ejemplo,  la  Cuenta  posee  una  instancia  de  Cliente  que  posee  
una  instancia  de  Cuenta  que  posee  una  instancia  de  Cliente,  y  así  sucesivamente...  Debido  al  estricto  orden  de  procesamiento  
de  los  compiladores  de  C++,  la  implementación  anterior  generará  errores  de  compilación.
Estos  errores  del  compilador  se  pueden  evitar,  por  ejemplo,  mediante  el  uso  de  referencias  o  punteros  en  combinación  con  
declaraciones  de  avance.  Una  declaración  directa  es  la  declaración  de  un  identificador  (por  ejemplo,  de  un  tipo,  como  una  clase)  
sin  definir  la  estructura  completa  de  ese  identificador.  Por  lo  tanto,  estos  tipos  a  veces  también  se  denominan  tipos  incompletos.  
Por  lo  tanto,  solo  se  pueden  usar  para  punteros  o  referencias,  pero  no  para  una  variable  de  miembro  de  instancia,  porque  el  
compilador  no  sabe  nada  sobre  su  tamaño.

151
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Listado  6­14.  El  Cliente  modificado  con  una  Cuenta  declarada  a  futuro

#ifndef  CLIENTE_H_  
#define  CLIENTE_H_

Cuenta  de  clase ;

clase  Cliente  

{ público: // ...
void  setCuenta(Cuenta*  cuenta)  
{ cuentacliente  =  cuenta; } // ...

privado:
Cuenta*  cuentacliente; };

#terminara  si

Listado  6­15.  La  cuenta  modificada  con  un  cliente  declarado  a  plazo

ifndef  CUENTA_H_  
#definir  CUENTA_H_

clase  Cliente;

class  Cuenta  

{ public: //...  void  setOwner(Cliente*  cliente)  
{ propietario  =  cliente;

} //...  
privado:  
Cliente*  propietario; };

#terminara  si

Mano  a  la  obra:  ¿te  sientes  un  poco  mal  con  esta  solución?  Si  es  así,  ¡es  por  buenas  razones!  Los  errores  del  
compilador  desaparecieron,  pero  esta  "solución"  produce  un  mal  presentimiento.  Veamos  cómo  se  usan  ambas  clases:

Listado  6­16.  Creando  las  instancias  de  Cliente  y  Cuenta,  y  conectándolas  circularmente  juntas
#incluye  "Cuenta.h"  #incluye  
"Cliente.h"
// ...
Cuenta*  cuenta  =  nueva  Cuenta  { };  Cliente*  
cliente  =  nuevo  Cliente  { };  cuenta­
>setOwner(cliente);  cliente­
>setAccount(cuenta); // ...

152
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Estoy  seguro  de  que  un  problema  grave  es  obvio:  ¿qué  sucede  si,  por  ejemplo,  se  eliminará  la  instancia  de  Cuenta,  
pero  la  instancia  de  Cliente  aún  existe?  Bueno,  la  instancia  de  Cliente  contendrá  un  puntero  colgante,  es  decir,  ¡un  puntero  
a  Tierra  de  nadie!  El  uso  o  la  anulación  de  la  referencia  de  un  puntero  de  este  tipo  puede  causar  problemas  graves,  como  
un  comportamiento  indefinido  o  bloqueos  de  la  aplicación.
Las  declaraciones  de  reenvío  son  bastante  útiles  para  ciertas  cosas,  pero  usarlas  para  lidiar  con  
dependencias  circulares  es  una  práctica  realmente  mala.  Es  una  solución  espeluznante  que  se  supone  que  oculta  un  
problema  de  diseño  fundamental.
El  problema  es  la  dependencia  circular  en  sí.  Este  es  un  mal  diseño.  Las  dos  clases  Cliente  y  Cuenta  no  
pueden  separarse.  Por  lo  tanto,  no  pueden  usarse  independientemente  uno  de  otro,  ni  pueden  probarse  independientemente  
uno  de  otro.  Esto  hace  que  las  pruebas  unitarias  sean  considerablemente  más  difíciles.
Y  el  problema  empeora  aún  más  si  tenemos  la  situación  representada  en  la  figura  6­5.

Figura  6­5.  El  impacto  de  las  dependencias  circulares  entre  clases  en  diferentes  componentes

Nuestras  clases  Cliente  y  Cuenta  están  ubicadas  cada  una  en  diferentes  componentes.  Quizás  haya  muchas  
más  clases  en  cada  uno  de  estos  componentes,  pero  estas  dos  clases  tienen  una  dependencia  circular.  La  consecuencia  
es  que  esta  dependencia  circular  tiene  también  un  impacto  negativo  a  nivel  arquitectónico.  La  dependencia  circular  en  
el  nivel  de  clase  conduce  a  una  dependencia  circular  en  el  nivel  de  componente.  CustomerManagement  y  Accounting  están  
estrechamente  relacionados  (recuerde  la  sección  sobre  acoplamiento  flexible  en  el  Capítulo  3)  y  no  se  pueden  (re)utilizar  
de  forma  independiente.  Y,  por  supuesto,  ya  no  es  posible  realizar  una  prueba  de  componentes  independientes.
La  modularización  a  nivel  de  arquitectura  se  ha  reducido  prácticamente  al  absurdo.
El  principio  de  dependencia  acíclica  establece  que  el  gráfico  de  dependencia  de  componentes  o  clases  no  debe  
tener  ciclos.  Las  dependencias  circulares  son  una  mala  forma  de  acoplamiento  estrecho  y  deben  evitarse  a  toda  costa.
¡No  te  preocupes!  Siempre  es  posible  romper  una  dependencia  circular,  y  la  siguiente  sección  mostrará  cómo  evitar,  
respectivamente,  romperlas.

Principio  de  inversión  de  dependencia  (DIP)
En  la  sección  anterior  experimentamos  que  las  dependencias  circulares  son  malas  y  deben  evitarse  bajo  todas  las  
circunstancias.  Y  como  con  muchos  otros  problemas  con  dependencias  no  deseadas,  el  concepto  de  la  interfaz  (en  C++,  
las  interfaces  se  simulan  usando  clases  abstractas)  es  nuestro  amigo  para  lidiar  con  problemas  como  en  el  caso  anterior.

153
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Por  lo  tanto,  el  objetivo  debe  ser  romper  la  dependencia  circular  sin  perder  la  posibilidad  necesaria  de  que  
la  clase  Cliente  pueda  acceder  a  Cuenta  y  viceversa.
El  primer  paso  es  que  ya  no  permitimos  que  una  de  las  dos  clases  tenga  acceso  directo  a  la  otra  clase.
En  cambio,  permitimos  dicho  acceso  solo  a  través  de  una  interfaz.  Básicamente,  no  importa  de  cuál  de  las  
dos  clases  (Cliente  o  Cuenta)  se  extrae  la  interfaz.  Decidí  extraer  una  interfaz  llamada  Propietario  de  Cliente.  
A  modo  de  ejemplo,  la  interfaz  de  propietario  declara  solo  una  función  de  miembro  virtual  pura  que  debe  ser  
anulada  por  las  clases  que  implementan  esta  interfaz.

Listado  6­17.  Una  implementación  ejemplar  de  la  nueva  interfaz  Owner  (Archivo:  Owner.h)

#ifndef  PROPIETARIO_H_  
#define  PROPIETARIO_H_

#include  <memoria>  
#include  <cadena>

propietario  de  
la  clase  
{ public:  virtual  ~Owner()  =  
predeterminado;  virtual  std::string  getName()  
const  =  0; };

usando  OwnerPtr  =  std::shared_ptr<Owner>;

#terminara  si

Listado  6­18.  La  clase  Cliente  que  implementa  la  interfaz  Propietario  (Archivo:  Cliente.h)

#ifndef  CLIENTE_H_  
#define  CLIENTE_H_

#incluye  "Propietario.h"  
#incluye  "Cuenta.h"

clase  Cliente :  public  Owner  { public:  
void  
setAccount(AccountPtr  cuenta)  
{ customerAccount  =  cuenta;
}

virtual  std::string  getName()  const  override  { //  devuelve  
aquí  el  nombre  del  Cliente...

} // ...

privado:  
AccountPtr  cuentacliente; // ... };

usando  CustomerPtr  =  std::shared_ptr<Cliente>;

#terminara  si

154
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Como  se  puede  ver  fácilmente  en  el  código  fuente  de  la  clase  Cliente  que  se  muestra  arriba,  el  Cliente  todavía  sabe
su  Cuenta.  Pero  cuando  echamos  un  vistazo  ahora  a  la  implementación  modificada  de  la  clase  Cuenta,  ya  no  hay  dependencia  con  el  
Cliente:

Listado  6­19.  La  implementación  modificada  de  la  clase  Cuenta  (Archivo:  Cuenta.h)

#ifndef  CUENTA_H_  #define  
CUENTA_H_

#include  "Propietario.h"

class  Cuenta  { public:  
void  
setOwner(OwnerPtr  propietario)  { este­>propietario  
=  propietario; } //...

privado:
PropietarioPtr  propietario; };

usando  AccountPtr  =  std::shared_ptr<Cuenta>;

#terminara  si

Representado  como  un  diagrama  de  clase  UML,  el  diseño  modificado  en  el  nivel  de  clase  es  como  se  muestra  en  la  Figura  6­6.

Figura  6­6.  La  introducción  de  la  interfaz  ha  eliminado  la  dependencia  circular  del  nivel  de  clase.

155
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

¡Excelente!  Con  este  primer  paso  en  el  rediseño,  ahora  hemos  logrado  que  no  haya  más  dependencias  
circulares  en  el  nivel  de  clase.  Ahora  la  clase  Cuenta  ya  no  sabe  absolutamente  nada  sobre  la  clase  Cliente.  Pero,  
¿cómo  se  ve  la  situación  cuando  subimos  al  nivel  del  componente  como  se  muestra  en  la  figura  6­7?

Figura  6­7.  La  dependencia  circular  entre  los  componentes  sigue  ahí.

Desafortunadamente,  la  dependencia  circular  entre  los  componentes  aún  no  se  ha  roto.  Los  dos
las  relaciones  de  asociación  todavía  van  de  un  elemento  en  un  componente  a  un  elemento  en  el  otro  componente.  
Sin  embargo,  el  paso  para  lograr  este  objetivo  es  increíblemente  fácil:  solo  necesitamos  reubicar  el  propietario  de  la  
interfaz  en  el  otro  componente,  como  se  muestra  en  la  figura  6­8.

156
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Figura  6­8.  La  reubicación  de  la  interfaz  también  soluciona  el  problema  de  dependencia  circular  a  nivel  de  arquitectura.

¡Excelente!  Ahora  las  dependencias  circulares  entre  los  componentes  han  desaparecido.  La  contabilidad
El  componente  ya  no  depende  de  CustomerManagement  y,  como  resultado,  la  calidad  de  la  modularización  ha  
mejorado  significativamente.  Además,  el  componente  Contabilidad  ahora  se  puede  probar  de  forma  independiente.

De  hecho,  la  mala  dependencia  entre  ambos  componentes  no  se  eliminó  literalmente.  Por  el  contrario,  a  través  de  la  
introducción  de  la  interfaz  Owner,  incluso  hemos  obtenido  una  dependencia  más  en  el  nivel  de  clase.  Lo  que  realmente  
habíamos  hecho  era  invertir  la  dependencia.
El  Principio  de  Inversión  de  Dependencia  (DIP)  es  un  principio  de  diseño  orientado  a  objetos  para  desacoplar  software
módulos.  El  principio  establece  que  la  base  de  un  diseño  orientado  a  objetos  no  son  las  propiedades  especiales  de  módulos  
de  software  concretos.  En  su  lugar,  sus  características  comunes  deben  consolidarse  en  una  abstracción  utilizada  
compartida  (por  ejemplo,  una  interfaz).  Robert  C.  Martin,  también  conocido  como  “Tío  Bob”,  formuló  el  principio  de  la  siguiente  manera:

A.  Los  módulos  de  alto  nivel  no  deben  depender  de  los  módulos  de  bajo  nivel.  Ambos  deberían  depender  de  abstracciones.

B.  Las  abstracciones  no  deben  depender  de  los  detalles.  Los  detalles  deben  depender  de  las  abstracciones.

—Robert  C.  Martín  [Martin03]

■  Nota  Los  términos  “módulos  de  alto  nivel”  y  “módulos  de  bajo  nivel”  en  esta  cita  pueden  ser  engañosos.  No  se  refieren  necesariamente  

a  su  posición  conceptual  dentro  de  una  arquitectura  en  capas.  Un  módulo  de  alto  nivel  en  este  caso  particular  es  un  módulo  de  

software  que  requiere  servicios  externos  de  otro  módulo,  el  llamado  módulo  de  bajo  nivel.

Los  módulos  de  alto  nivel  son  aquellos  en  los  que  se  invoca  una  acción,  los  módulos  de  bajo  nivel  son  aquellos  en  los  que  se  

realiza  la  acción.  En  algunos  casos,  estas  dos  categorías  de  módulos  también  pueden  ubicarse  en  diferentes  niveles  de  una  

arquitectura  de  software  (por  ejemplo,  capas)  o,  como  en  nuestro  ejemplo,  en  diferentes  componentes.

157
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

El  principio  de  inversión  de  dependencia  es  fundamental  para  lo  que  se  percibe  como  una  buena  orientación  a  objetos.
diseño.  Fomenta  el  desarrollo  de  módulos  de  software  reutilizables  al  definir  los  servicios  externos  proporcionados  y  requeridos  
únicamente  a  través  de  abstracciones  (por  ejemplo,  interfaces).  Aplicado  consistentemente  a  nuestro  caso  discutido  anteriormente,  
también  tendríamos  que  rediseñar  la  dependencia  directa  entre  el  Cliente  y  la  Cuenta  en  consecuencia,  como  se  muestra  
en  la  Figura  6­9.

Figura  6­9.  Principio  de  inversión  de  dependencia  aplicado

Las  clases  en  ambos  componentes  dependen  únicamente  de  las  abstracciones.  Por  lo  tanto,  ya  no  es  importante  para  
el  cliente  del  componente  Contabilidad  qué  clase  requiere  la  interfaz  de  Propietario  o  proporciona  la  interfaz  de  Cuenta  (recuerde  la  
sección  sobre  Ocultación  de  información  en  el  Capítulo  3).  He  insinuado  esta  circunstancia  al  presentar  una  clase  que  se  llama  
AnyClass. ,  que  implementa  Cuenta  y  utiliza  Propietario.
Por  ejemplo,  si  tenemos  que  cambiar  o  reemplazar  la  clase  Cliente  ahora,  por  ejemplo,  porque  queremos  montar  la  Contabilidad  
contra  un  dispositivo  de  prueba  para  la  prueba  de  componentes,  entonces  no  es  necesario  cambiar  nada  en  la  clase  AnyClass  para  
lograrlo.  Esto  también  se  aplica  al  caso  inverso.
El  principio  de  inversión  de  dependencia  permite  a  los  desarrolladores  de  software  diseñar  dependencias  entre  
módulos  a  propósito,  es  decir,  definir  en  qué  dirección  apuntan  las  dependencias.  ¿Desea  invertir  la  dependencia  entre  los  
componentes,  es  decir,  la  contabilidad  debe  depender  de  CustomerManagement?  No  hay  problema:  simplemente  reubique  
ambas  interfaces  de  Contabilidad  a  CustomerManagement  y  la  dependencia  cambiará.  Las  malas  dependencias,  
que  reducen  la  mantenibilidad  y  la  capacidad  de  prueba  del  código,  se  pueden  rediseñar  y  reducir  con  elegancia.

No  hables  con  extraños  (Ley  de  Deméter)
¿Recuerda  el  automóvil  del  que  hablé  anteriormente  en  este  capítulo?  Describí  este  automóvil  como  una  composición  de  varias  
partes,  por  ejemplo,  carrocería,  motor,  engranajes,  etc.  Y  he  explicado  que  estas  partes  pueden  consistir  nuevamente  en  partes,  
que  por  sí  mismas  también  pueden  consistir  en  varias  partes,  etc.  Esto  lleva  a  una  descomposición  jerárquica  de  arriba  hacia  
abajo  de  un  automóvil.  Y,  por  supuesto,  un  automóvil  puede  tener  un  conductor  que  quiera  usarlo.
Visualizado  como  un  diagrama  de  clase  UML,  un  extracto  de  la  descomposición  del  automóvil  puede  parecerse  a  lo  que  
se  muestra  en  la  figura  6­10.

158
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Figura  6­10.  La  descomposición  jerárquica  de  un  automóvil  simple.

De  acuerdo  con  el  Principio  de  Responsabilidad  Única  discutido  en  el  Capítulo  5,  todo  está  bien,  porque
cada  clase  tiene  una  responsabilidad  bien  definida.
Ahora  supongamos  que  el  conductor  quiere  conducir  el  automóvil.  Esto  podría  implementarse  de  la  siguiente  manera  en  el
controlador  de  clase:

Listado  6­20.  Un  extracto  de  la  implementación  de  la  clase  Driver

controlador  de  clase  

{ público: // ...
void  drive(Coche&coche)  const  {
Motor&  motor  =  coche.getEngine();  
BombaCombustible&  BombaCombustible  =  

motor.getBombaCombustible();  bombadecombustible.bomba();  
Encendido&  encendido  =  
motor.getIgnition();  encendido.powerUp();  Motor  de  
arranque&  motor  de  

arranque  =  motor.getStarter();  starter.revolve(); } // ...

¿Cuál  es  el  problema  aquí?  Bueno,  como  conductor  de  un  automóvil,  ¿esperaría  que  tuviera  que  acceder  directamente  
al  motor  de  su  automóvil,  encender  la  bomba  de  combustible,  encender  el  sistema  de  encendido  y  dejar  girar  el  motor  de  arranque?
Voy  aún  más  lejos:  ¿está  interesado  en  el  hecho  de  que  su  automóvil  consta  de  estas  partes  si  solo  quiere  conducirlo?

159
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Estoy  bastante  seguro  de  que  su  respuesta  clara  sería:  ¡ No!
Y  ahora  echemos  un  vistazo  a  la  Figura  6­11,  que  representa  la  parte  relevante  del  diagrama  de  clases  UML  para  ver
qué  impacto  tiene  esta  implementación  en  el  diseño.

Figura  6­11.  Las  malas  dependencias  de  la  clase  Driver

Como  se  puede  ver  fácilmente  en  el  diagrama  anterior,  la  clase  Driver  tiene  muchas  dependencias  incómodas.  El  controlador  no  
solo  depende  del  motor.  La  clase  también  tiene  varias  relaciones  de  dependencia  con  partes  del  motor.  Es  fácil  imaginar  que  esto  tiene  
algunas  consecuencias  desventajosas.
¿Qué  pasaría,  por  ejemplo,  si  se  sustituyera  el  motor  de  combustión  por  un  tren  de  potencia  eléctrico?  Un  motor  eléctrico  no  
tiene  bomba  de  combustible,  sistema  de  encendido  ni  motor  de  arranque.  Así,  las  consecuencias  serían  que  la  implementación  de  la  
clase  Driver  tiene  que  ser  adaptada.  Esto  viola  el  Principio  Abierto­Cerrado  (ver  sección  anterior).  Además,  todos  los  captadores  
públicos  que  exponen  las  entrañas  del  automóvil  y  el  motor  a  su  entorno  están  violando  el  principio  de  ocultación  de  información  
(consulte  el  Capítulo  3).
Esencialmente,  el  diseño  de  software  anterior  viola  la  Ley  de  Demeter  (LoD),  también  conocida  como  el  Principio
de  Menos  Conocimiento.  La  Ley  de  Deméter  puede  considerarse  como  un  principio  que  dice  algo  así  como  "No  hables  con  
extraños"  o  "Solo  habla  con  tus  vecinos  inmediatos".  Este  principio  establece  que  debes  hacer  una  programación  tímida,  y  el  objetivo  
es  gobernar  la  estructura  de  comunicación  dentro  de  un  diseño  orientado  a  objetos.

160
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

La  Ley  de  Deméter  postula  las  siguientes  reglas:

•  Una  función  miembro  puede  llamar  a  otras  funciones  miembro  en  su  propio  ámbito  de  clase
directamente.

•Se  permite  que  una  función  miembro  llame  a  funciones  miembro  en  variables  miembro  que
están  en  su  ámbito  de  clase  directamente.

• Si  una  función  miembro  tiene  parámetros,  la  función  miembro  puede  llamar  directamente  a  las  
funciones  miembro  de  estos  parámetros.
• Si  una  función  miembro  crea  objetos  locales,  la  función  miembro  puede  llamar  a  funciones  miembro  
en  esos  objetos  locales.

Si  uno  de  estos  cuatro  tipos  de  llamadas  a  funciones  miembro  antes  mencionadas  devuelve  un  objeto  que  es  estructuralmente
más  lejos  que  los  vecinos  inmediatos  de  la  clase,  está  prohibido  llamar  a  una  función  miembro  en  esos  objetos.

POR  QUÉ  ESTA  REGLA  SE  NOMBRA  LEY  DE  DEMÉTER

El  nombre  de  este  principio  se  remonta  al  Proyecto  Demeter  sobre  Desarrollo  de  Software  Orientado  a  
Aspectos,  donde  se  formularon  y  aplicaron  estrictamente  estas  reglas.  El  Proyecto  Demeter  fue  un  proyecto  
de  investigación  a  fines  de  la  década  de  1980  con  un  enfoque  principal  en  hacer  que  el  software  sea  más  fácil  de  
mantener  y  expandir  a  través  de  la  programación  adaptativa.  La  Ley  de  Deméter  fue  descubierta  y  propuesta  
por  Ian  M.  Holland  y  Karl  Lieberherr  quienes  trabajaron  en  ese  proyecto.  En  la  mitología  griega,  Deméter  es  la  
hermana  de  Zeus  y  la  diosa  de  la  agricultura.

Entonces,  ¿cuál  es  ahora  la  solución  en  nuestro  ejemplo  para  deshacerse  de  las  malas  dependencias?  Sencillamente,  deberíamos
preguntarnos:  ¿qué  es  lo  que  realmente  quiere  hacer  un  conductor?  La  respuesta  es  fácil:  ¡quiere  encender  el  auto!

class  Driver  

{ public: // ...  void  drive(Car&  car)  const  
{ car.start(); } // ... };

¿Y  qué  hace  el  coche  con  este  mando  de  arranque?  También  bastante  simple:  delega  esta  llamada  de  método  a  su  motor.

class  Car  

{ public: // ...  void  
start()  

{ motor.start(); } // ...  
privado:  motor  motor; };

161
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Y  por  último,  pero  no  menos  importante,  el  motor  sabe  cómo  puede  ejecutar  el  proceso  de  inicio  llamando  al  correspondiente
miembro  funciona  en  el  orden  correcto  en  sus  partes,  que  son  sus  vecinos  inmediatos  en  el  diseño  del  software.

class  Engine  
{ public: // ...  
void  
start()  
{ fuelPump.pump();  
encendido.powerUp();  
starter.revolve(); } // ...  

privado:  

bomba  de  
combustible  bomba  de  
combustible;  Encendido  
encendido;  Arrancador  
de  arranque; };

El  efecto  positivo  de  estos  cambios  en  el  diseño  orientado  a  objetos  se  puede  ver  muy  claramente  en  el  diagrama  de  clases  que  se  
muestra  en  la  figura  6­12.

Figura  6­12.  Menos  dependencias  tras  la  aplicación  de  la  Ley  de  Deméter

Se  desvanecen  las  molestas  dependencias  del  conductor  con  respecto  a  las  piezas  del  automóvil.  En  su  lugar,  el  conductor  puede  iniciar  el
coche,  independientemente  de  la  estructura  interna  del  coche.  La  clase  Conductor  ya  no  sabe  que  hay  un  Motor,  una  Bomba  de  Combustible,  
etc.  Todas  esas  malas  funciones  públicas  captadoras,  que  habían  revelado  las  entrañas  del  coche  o  el  motor  a  todas  las  demás  clases,  se  
han  ido.  Esto  también  significa  que  los  cambios  en  el  motor  y  sus  partes  solo  tienen  impactos  muy  locales  y  no  darán  como  resultado  cambios  
en  cascada  directamente  a  través  de  todo  el  diseño.
Seguir  la  Ley  de  Demeter  al  diseñar  software  puede  reducir  significativamente  el  número  de  dependencias.  Esto  conduce  a  un  
acoplamiento  débil  y  fomenta  tanto  el  Principio  de  ocultación  de  información  como  el  Principio  abierto  cerrado.  Al  igual  que  con  muchos  otros  
principios  y  reglas,  también  puede  haber  algunas  excepciones  justificadas  en  las  que  un  desarrollador  debe  desviarse  de  este  principio  por  muy  
buenas  razones.

162
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Evite  las  clases  anémicas
En  varios  proyectos  he  visto  clases  que  se  veían  de  la  siguiente  manera:

Listado  6­21.  Una  clase  sin  funcionalidad  que  solo  sirve  como  depósito  para  un  montón  de  datos

clase  Cliente  { public:  
void  
setId(const  unsigned  int  id);  unsigned  int  getId()  
const;  void  setForename(const  
std::string&  nombre);  std::string  getForename()  const;  void  
setSurname(const  std::string&surname);  
std::string  getApellido()  const; //...más  setters/getters  aquí...

privado:  id  
int  sin  firmar ;  std::string  
nombre;  std::string  
apellido; // ...más  atributos  
aquí... };

Esta  clase  de  dominio,  que  representa  a  un  cliente  en  un  sistema  de  software  arbitrario,  no  contiene  ninguna  lógica.
La  lógica  está  en  otro  lugar,  incluso  aquella  lógica  que  representa  una  funcionalidad  exclusiva  para  el  Cliente,  es  decir,  operando  
solo  sobre  atributos  del  Cliente.
Los  programadores  que  hicieron  esto  están  usando  objetos  como  bolsas  para  un  montón  de  datos.  Esto  es  solo  
programación  procedimental  con  estructuras  de  datos  y  no  tiene  nada  que  ver  con  la  orientación  a  objetos.  Además,  todos  esos  
setters/getters  son  totalmente  tontos  y  violan  severamente  el  principio  de  ocultación  de  información;  en  realidad,  podríamos  usar  
una  estructura  C  simple  (palabra  clave:  struct)  aquí.
Estas  clases  se  denominan  clases  anémicas  y  deben  evitarse  a  toda  costa.  A  menudo  se  pueden  encontrar  en  un  diseño  
de  software  que  es  un  Anti­Patrón  que  ha  sido  llamado  Modelo  de  Dominio  Anémico  por  Martin  Fowler  [Fowler03].  Es  exactamente  
lo  contrario  de  la  idea  básica  del  diseño  orientado  a  objetos,  que  es  combinar  datos  y  la  funcionalidad  que  trabaja  con  los  datos  en  
unidades  cohesivas.
Siempre  que  no  viole  la  Ley  de  Deméter,  debe  insertar  lógica  también  en  las  clases  (dominio),  si  esto
la  lógica  está  operando  en  los  atributos  de  esa  clase  o  colabora  solo  con  los  vecinos  inmediatos  de  la  clase.

¡Di,  no  preguntes!
El  principio  Di,  no  preguntes  tiene  algunas  similitudes  con  la  Ley  de  Deméter  discutida  anteriormente.  Este  principio  es  la  
“declaración  de  guerra”  a  todos  aquellos  métodos  get  públicos,  que  revela  algo  sobre  el  estado  interno  de  un  objeto.  También  Tell  
Don't  Ask  fomenta  la  encapsulación,  fortalece  la  ocultación  de  información  (consulte  el  Capítulo  3),  pero  ante  todo,  este  principio  
se  trata  de  una  fuerte  cohesión.
Examinemos  un  pequeño  ejemplo.  Supongamos  que  la  función  miembro  Engine::start()  del
ejemplo  anterior  se  implementa  de  la  siguiente  manera:

Listado  6­22.  Una  implementación  posible,  pero  no  recomendable,  de  la  función  miembro  Engine::start()

motor  de  clase  

{ público: // ...
void  start()  { if  (!  
fuelPump.isRunning())  {

163
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

bombadecombustible.powerUp();  
if  (bombadecombustible.getFuelPressure()  <  PRESIÓN_COMBUSTIBLE_NORMAL)  
{ bombadecombustible.setPresiónCombustible(PRESIÓN_COMBUSTIBLE_NORMAL); }

}  if  (!  encendido.esPoweredUp())  
{ encendido.powerUp(); }  if  

(!  starter.isRotating())  { starter.revolve(); }  si  
(motor.hasStarted())  {

motor  de  arranque.openClutchToEngine();  
starter.stop(); }

} // ...  
privado:  
bomba  de  combustible  
bomba  de  combustible;  
Encendido  encendido;  
Arrancador  de  arranque;  static  const  unsigned  int  NORMAL_FUEL_PRESSURE  
{ 120 }; };

Como  es  fácil  de  ver,  el  método  start()  de  la  clase  Engine  consulta  muchos  estados  de  sus  partes  y  responde  en  consecuencia.  
Además,  el  Motor  verifica  la  presión  de  combustible  de  la  bomba  de  combustible  y  la  ajusta  si  es  demasiado  baja.  Esto  también  significa  
que  el  motor  debe  conocer  el  valor  de  la  presión  normal  de  combustible.  Debido  a  las  numerosas  ramas  si,  la  complejidad  ciclomática  es  
alta.
El  principio  Diga,  no  pregunte,  nos  recuerda  que  no  debemos  pedirle  a  un  objeto  que  suelte  información  sobre  su  estado  interno  y  
que  decida  fuera  de  este  objeto  qué  hacer,  si  este  objeto  pudiera  decidirlo  por  sí  mismo.  Básicamente,  este  principio  nos  recuerda  que  en  la  
orientación  a  objetos,  los  datos  y  las  operaciones  que  operan  sobre  estos  datos  deben  combinarse  en  unidades  cohesivas.

Si  aplicamos  este  principio  a  nuestro  ejemplo,  el  método  Engine::start()  solo  le  diría  a  sus  partes  lo  que  deben  hacer:

Listado  6­23.  Delegación  de  etapas  del  procedimiento  de  arranque  a  las  partes  responsables  del  motor

motor  de  clase  

{ público: // ...
void  start()  

{ bombadecombustible.bomba();  
encendido.powerUp();  

starter.revolve(); } // ...
privado:
bomba  de  combustible  bomba  de  combustible;

Encendido  encendido;
Arrancador  de  arranque; };

164
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Las  partes  pueden  decidir  por  sí  mismas  cómo  quieren  ejecutar  este  comando,  porque  tienen  el  conocimiento  al  respecto,  
por  ejemplo,  FuelPump  puede  hacer  todo  lo  que  tiene  que  hacer  para  acumular  presión  de  combustible:

Listado  6­24.  Un  extracto  de  la  clase  FuelPump

bomba  de  combustible  
de  clase  
{ público: // ...

void  pump()  { if  (!  
isRunning)  { powerUp();  

setPresiónNormalCombustible(); } } // ...

privado:  
vacío  powerUp()  { //...

void  setPresiónCombustibleNormal()  { if  
(presión !=  PRESIÓN_COMBUSTIBLE_NORMAL)  
{ presión  =  PRESIÓN_COMBUSTIBLE_NORMAL; } }

bool  se  está  
ejecutando;  presión  int  sin  
firmar ;  static  const  unsigned  int  NORMAL_FUEL_PRESSURE  { 120 }; };

Por  supuesto,  no  todos  los  getters  son  intrínsecamente  malos.  A  veces  es  necesario  recuperar  información  de  un
objeto,  por  ejemplo,  si  esta  información  debe  mostrarse  en  una  interfaz  gráfica  de  usuario.

Evitar  miembros  de  clase  estáticos
Bien  puedo  imaginar  que  muchos  lectores  se  están  preguntando  ahora:  ¿qué  diablos  está  mal  con  las  variables  miembro  
estáticas  y,  respectivamente,  las  funciones  miembro  estáticas?
Bueno,  tal  vez  todavía  recuerdes  el  Anti­Patrón  de  Clase  Dios  que  he  descrito  en  la  sección  anterior  sobre  clases  pequeñas.  
Allí  describí  que  las  clases  de  utilidad  generalmente  tienden  a  convertirse  en  "Clases  de  Dios"  tan  grandes.
Además,  estas  clases  de  utilidad  generalmente  también  constan  de  muchas  funciones  miembro  estáticas,  a  menudo  incluso  sin  
excepción.  La  justificación  tranquila  y  comprensible  para  esto  es:  ¿por  qué  debo  obligar  a  los  usuarios  de  la  clase  de  utilidad  a  
crear  una  instancia  de  ella?  Y  debido  a  que  tales  clases  ofrecen  una  colorida  variedad  de  diferentes  funciones  para
propósito  diferente,  que  por  cierto  es  un  signo  de  cohesión  débil,  he  creado  un  nombre  de  patrón  especial  para  estas  cosas  
desordenadas:  el  antipatrón  de  la  tienda  de  chatarra.  Según  la  enciclopedia  en  línea  Wikipedia,  una  tienda  de  chatarra  es  un  punto  de  
venta  minorista  similar  a  una  tienda  de  segunda  mano  que  ofrece  una  amplia  variedad  de  productos  en  su  mayoría  usados  a  
precios  económicos.

165
Machine Translated by Google

Capítulo  6  ■  Orientación  a  objetos

Listado  6­25.  Extracto  de  alguna  clase  de  utilidad

class  JunkShop  

{ public: // ...muchas  funciones  de  utilidad  pública...  static  
int  oneOfManyUtilityFunctions(int  param); // ...más  funciones  de  utilidad  
pública... };

Listado  6­26.  Otra  clase  que  usa  la  clase  Utility

#include  "Tienda  de  chatarra.h"

class  Client  { // ...  
void  
hacerAlgo()  { // ...  y  =  

JunkShop::oneOfManyUtilityFunctions(x); // ...

} };

El  primer  problema  es  que  su  código  se  conecta  con  todas  esas  funciones  auxiliares  estáticas  en  estas  "Tiendas  de  chatarra".  
Como  se  puede  ver  fácilmente  en  el  ejemplo  anterior,  estas  funciones  estáticas  de  las  clases  de  utilidad  se  utilizan  en  algún  lugar  de  
la  implementación  de  otro  módulo  de  software.  Por  lo  tanto,  no  hay  una  manera  fácil  de  reemplazar  esta  llamada  de  función  con  otra  
cosa.  Pero  en  las  pruebas  unitarias  (consulte  el  Capítulo  2),  esto  es  exactamente  lo  que  desea  hacer.
Además,  las  funciones  miembro  estáticas  fomentan  un  estilo  de  programación  procedimental.  Su  uso  junto  con  
variables  estáticas  reduce  la  orientación  a  objetos  al  absurdo.  Compartir  el  mismo  estado  en  todas  las  instancias  de  una  clase  con  
la  ayuda  de  una  variable  miembro  estática  no  es  intrínsecamente  OOP,  porque  rompe  la  encapsulación,  porque  un  objeto  ya  no  tiene  
el  control  total  de  su  estado.
Por  supuesto,  C++  no  es  un  lenguaje  de  programación  orientado  a  objetos  puro  como  Java  o  C#,  y  es  básicamente
No  está  prohibido  escribir  código  de  procedimiento  en  C++.  Pero  cuando  quiera  hacer  eso,  debe  ser  honesto  consigo  mismo  y,  
en  consecuencia,  usar  procedimientos  independientes  simples,  respectivamente  funciones,  variables  globales  y  espacios  de  nombres.

Mi  consejo  es  evitar  las  variables  miembro  estáticas  respectivamente  y  las  funciones  miembro  en  gran  medida.
Una  excepción  a  esta  regla  son  las  constantes  privadas  de  una  clase,  porque  son  de  solo  lectura  y  no  representan  el  estado  
de  un  objeto.  Otra  excepción  son  los  métodos  de  fábrica,  es  decir,  funciones  miembro  estáticas  que  crean  instancias  de  un  objeto,  
generalmente  instancias  del  tipo  de  clase  que  también  sirve  como  espacio  de  nombres  de  la  función  miembro  estática.

166
Machine Translated by Google

CAPÍTULO  7

Programación  funcional

Durante  varios  años,  un  paradigma  de  programación  experimentó  un  renacimiento,  que  a  menudo  se  ve  como  una  especie  de  
contracorriente  de  la  orientación  a  objetos.  La  charla  es  sobre  Programación  Funcional.
Uno  de  los  primeros  lenguajes  de  programación  funcionales  fue  Lisp  (La  mayúscula  "LISP"  es  una  ortografía  más  antigua,
porque  el  nombre  del  lenguaje  es  una  abreviatura  de  “LISt  Processing”),  que  fue  diseñado  por  el  informático  y  científico  
cognitivo  estadounidense  John  McCarthy  en  1958  en  el  Instituto  Tecnológico  de  Massachusetts  (MIT).  McCarthy  también  acuñó  
el  término  "inteligencia  artificial" (IA),  y  usó  Lisp  como  lenguaje  de  programación  para  aplicaciones  de  IA.  Lisp  se  basa  en  el  
llamado  Lambda  Calculus  (cálculo  λ),  un  modelo  formal  que  fue  introducido  en  la  década  de  1930  por  el  matemático  
estadounidense  Alonzo  Church  (ver  la  siguiente  barra  lateral).

De  hecho,  Lisp  es  una  familia  de  lenguajes  de  programación  de  computadoras.  Varios  dialectos  de  Lisp  han  surgido  en  
el  pasado.  Por  ejemplo,  todos  los  que  alguna  vez  hayan  usado  un  miembro  de  la  famosa  familia  de  editores  de  texto  Emacs,  por  
ejemplo,  GNU  Emacs  o  X  Emacs,  conocen  el  dialecto  Emacs  Lisp  que  se  usa  como  lenguaje  de  secuencias  de  comandos  para  
extensión  y  automatización.
Los  lenguajes  de  programación  funcionales  dignos  de  mención,  que  se  han  desarrollado  más  allá  de  Lisp,  fueron,  entre  
otros:

•  Scheme:  un  dialecto  Lisp  con  enlace  estático  que  se  desarrolló  en  la  década  de  1970  en  el  MIT
Laboratorio  de  Inteligencia  Artificial  (AI  Lab).

•  Miranda:  el  primer  lenguaje  funcional  puro  y  perezoso  que  fue  comercialmente
soportado.

•  Haskell:  un  lenguaje  de  programación  puramente  funcional  y  de  propósito  general  que  lleva  el  nombre  
del  lógico  y  matemático  estadounidense  Haskell  Brooks  Curry.

•  Erlang:  desarrollado  por  la  compañía  sueca  de  telecomunicaciones  Ericsson  con  un  enfoque  
principal  en  la  construcción  de  sistemas  de  software  en  tiempo  real  masivos,  escalables  y  
altamente  confiables.

•  F#  (pronunciado  F  sostenido):  un  lenguaje  de  programación  multiparadigma  y  un
miembro  de  Microsoft .NET  Framework.  El  paradigma  principal  de  F#  es  la  programación  funcional,  
pero  permite  al  desarrollador  cambiar  también  al  mundo  imperativo/orientado  a  objetos  del  
ecosistema .NET.
• Clojure:  un  dialecto  moderno  del  lenguaje  de  programación  Lisp  creado  por  Rich  Hickey.  
Clojure  es  puramente  funcional  y  se  ejecuta  en  la  máquina  virtual  Java™  y  Common  Language  
Runtime  (CLR;  el  entorno  de  tiempo  de  ejecución  de  Microsoft .NET  framework).

©  Stephan  Roth  2017   167
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0_7
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

EL  CÁLCULO  LAMBDA

Es  difícil  encontrar  una  introducción  indolora  al  Cálculo  Lambda.  Muchos  ensayos  sobre  este  tema  están  
muy  científicamente  escritos  y  requieren  un  buen  conocimiento  de  las  matemáticas  y  la  lógica.  E  incluso  no  
trataré  de  explicar  el  Cálculo  Lambda  aquí,  porque  no  es  el  enfoque  principal  de  este  libro  hacer  esto.  Pero  
puedes  encontrar  innumerables  explicaciones  en  Internet;  solo  pregunta  al  buscador  de  tu  confianza,  y  
obtendrás  cientos  de  visitas.

Solo  eso:  Lambda  Calculus  puede  considerarse  como  el  lenguaje  de  programación  más  simple  y  
pequeño  que  es  posible.  Consta  sólo  de  dos  partes:  un  único  esquema  de  definición  de  funciones  y  una  
única  regla  de  transformación.  Estos  dos  componentes  son  suficientes  para  tener  un  modelo  genérico  para  
la  descripción  formal  de  lenguajes  de  programación  funcionales,  como  LISP,  Haskell,  Clojure,  etc.

A  día  de  hoy,  los  lenguajes  de  programación  funcionales  todavía  no  se  utilizan  tanto  como  sus  parientes  imperativos,  por  
ejemplo,  como  los  orientados  a  objetos,  pero  su  difusión  aumenta.  Algunos  ejemplos  son  JavaScript  y  Scala,  que  ciertamente  son  
lenguajes  multiparadigmáticos  (es  decir,  no  son  puramente  funcionales),  pero  que  se  hicieron  cada  vez  más  populares,  especialmente  
en  el  desarrollo  web,  entre  otros  debido  a  sus  capacidades  de  programación  funcional.

Esta  es  razón  suficiente  para  profundizar  en  este  tema  y  explorar  de  qué  se  trata  este  estilo  de  programación  y  qué  tiene  que  
ofrecer  el  C++  moderno  en  esta  dirección.

¿Qué  es  la  programación  funcional?
Es  difícil  encontrar  una  definición  generalmente  aceptada  para  Programación  Funcional  (a  veces  abreviada  como  FP).  A  menudo,  
uno  lee  que  la  Programación  Funcional  es  un  estilo  de  programación  en  el  que  todo  el  programa  se  construye  exclusivamente  a  
partir  de  funciones  puras.  Esto  plantea  inmediatamente  la  pregunta:  ¿qué  se  entiende  por  “función  pura”  en  este  contexto?  Bien,  
abordaremos  esta  pregunta  en  la  siguiente  sección.  Sin  embargo,  básicamente  es  correcto:  los  fundamentos  de  la  programación  
funcional  son  funciones  en  su  sentido  matemático.  Los  programas  se  construyen  mediante  una  composición  de  funciones  y  la  
evaluación  de  funciones  y  cadenas  de  funciones.
Al  igual  que  la  Orientación  a  Objetos  (ver  Capítulo  6),  también  la  Programación  Funcional  es  un  paradigma  de  programación.
Eso  significa  que  es  una  forma  de  pensar  sobre  la  construcción  de  software.  Sin  embargo,  el  paradigma  de  la  Programación  Funcional  
también  suele  definirse  por  todas  aquellas  propiedades  positivas  que  se  le  atribuyen.  Estas  propiedades,  que  se  consideran  
ventajosas  en  comparación  con  otros  paradigmas  de  programación,  especialmente  la  orientación  a  objetos,  son  las  
siguientes:

•  Sin  efectos  secundarios  al  evitar  un  estado  mutable  compartido  (globalmente).  En  la  programación  funcional  
pura,  una  llamada  de  función  no  tiene  ningún  efecto  secundario.  Esta  importante  propiedad  de  las  
funciones  puras  se  analiza  en  detalle  en  la  siguiente  sección,  "¿Qué  es  una  función?"

•  Datos  y  objetos  inmutables.  En  la  Programación  Funcional  pura,  todos  los  datos  son
inmutable,  es  decir,  una  vez  que  se  ha  creado  una  estructura  de  datos,  nunca  se  puede  cambiar.
En  cambio,  si  aplicamos  una  función  a  una  estructura  de  datos,  se  crea  como  resultado  una  nueva  
estructura  de  datos  que  es  nueva  o  una  variante  de  la  anterior.  Como  consecuencia  agradable,  los  datos  
inmutables  tienen  la  gran  ventaja  de  ser  seguros  para  subprocesos.

•  Composición  de  funciones  y  funciones  de  orden  superior.  En  Programación  Funcional,
Las  funciones  pueden  ser  tratadas  como  datos.  Puede  almacenar  una  función  en  una  variable.  Puede  
pasar  una  función  como  argumento  a  otras  funciones.  Las  funciones  se  pueden  devolver  como  
resultados  de  otras  funciones.  Las  funciones  se  pueden  encadenar  fácilmente.  En  otras  palabras:  las  
funciones  son  ciudadanos  de  primera  clase  del  lenguaje.

168
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

•  Mejor  y  más  fácil  paralelización.  La  concurrencia  es  básicamente  difícil.  Un  software
El  diseñador  debe  prestar  atención  a  muchas  cosas  en  un  entorno  de  múltiples  subprocesos  de  las  
que  normalmente  no  tiene  que  preocuparse  cuando  solo  hay  un  único  subproceso  de  ejecución.  Y  
encontrar  errores  en  dicho  programa  puede  ser  muy  doloroso.  Pero  si  las  llamadas  a  funciones  
nunca  tienen  efectos  secundarios,  si  no  hay  estados  globales  y  si  tratamos  únicamente  con  
estructuras  de  datos  inmutables,  es  mucho  más  fácil  hacer  una  pieza  de  software  paralela.  En  cambio,  
con  los  lenguajes  imperativos,  como  los  orientados  a  objetos,  y  sus  estados  a  menudo  mutables,  
necesita  mecanismos  de  bloqueo  y  sincronización  para  proteger  los  datos  y  evitar  que  varios  
subprocesos  accedan  a  ellos  y  los  manipulen  simultáneamente  (consulte  la  sección  "El  
poder  de  la  inmutabilidad"  en  el  Capítulo  9) .  sobre  cómo  crear  una  clase  inmutable  
respectivamente  objeto  en  C++).

•  Fácil  de  probar.  Si  las  funciones  puras  tienen  todas  las  propiedades  positivas  mencionadas  anteriormente,  
también  son  muy  fáciles  de  probar.  No  es  necesario  considerar  estados  mutables  globales  u  otros  efectos  
secundarios  en  los  casos  de  prueba.

Veremos  que  la  programación  en  un  estilo  funcional  en  C++  no  puede  garantizar  completamente  todos  estos  aspectos  
positivos  automáticamente.  Por  ejemplo,  si  necesitamos  un  tipo  de  datos  inmutable,  tenemos  que  diseñarlo  de  esa  manera,  como  
se  explica  en  el  Capítulo  9.  Pero  ahora  profundicemos  en  este  tema  y  discutamos  la  pregunta  central:  ¿qué  es  una  función  en  la  
Programación  Funcional?

¿Qué  es  una  función?
En  el  desarrollo  de  software  podemos  encontrar  muchas  cosas  que  se  denominan  “función”.  Por  ejemplo,  algunas  de  las  
funciones  que  una  aplicación  de  software  ofrece  a  sus  usuarios  a  menudo  también  se  denominan  funciones  del  programa.  En  C++,  
los  métodos  de  una  clase  a  veces  se  denominan  funciones  miembro.  Las  subrutinas  de  un  programa  de  computadora  generalmente  
se  consideran  funciones.  Sin  duda,  estos  ejemplos  también  son  "funciones"  en  cierto  modo,  pero  no  las  funciones  que  tratamos  
en  Programación  Funcional.
Cuando  hablamos  de  funciones  en  Programación  Funcional,  estamos  hablando  de  verdaderas  funciones  matemáticas.  Eso  
significa  que  consideramos  una  función  como  una  relación  entre  un  conjunto  de  parámetros  de  entrada  y  un  conjunto  de  
parámetros  de  salida  permisibles,  donde  cada  conjunto  de  parámetros  de  entrada  está  relacionado  exactamente  con  un  conjunto  
de  parámetros  de  salida.  Representada  como  una  fórmula  simple  y  general,  una  función  es  una  expresión  como  se  muestra  en  la  
figura  7­1.

Figura  7­1.  La  función  f  asigna  x  a  y

Esta  sencilla  fórmula  define  el  patrón  básico  de  cualquier  función.  Expresa  que  el  valor  de  y  depende,  y  únicamente,  del  valor  
de  x.  Y  otro  punto  importante  es  que  para  los  mismos  valores  de  x,  ¡también  el  valor  de  y  es  siempre  el  mismo!  En  otras  palabras,  la  
función  f  asigna  cualquier  valor  posible  de  x  a  exactamente  un  valor  único  de  y.  En  matemáticas  y  programación  informática,  esto  
también  se  conoce  como  transparencia  referencial.

169
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

TRANSPARENCIA  REFERENCIAL

Una  ventaja  esencial  que  se  menciona  a  menudo  junto  con  la  programación  funcional  es  que  las  funciones  puras  
siempre  son  referencialmente  transparentes.

El  término  “Transparencia  referencial”  tiene  su  origen  en  la  filosofía  analítica,  que  es  un  término  general  para  
ciertos  movimientos  filosóficos  que  se  desarrollan  desde  principios  del  siglo  XX.  La  filosofía  analítica  se  basa  en  
una  tradición  que  inicialmente  operaba  principalmente  con  lenguajes  ideales  (lógicas  formales)  o  analizando  
el  lenguaje  cotidiano  de  uso  cotidiano.  El  término  “Transparencia  referencial”  se  atribuye  al  filósofo  y  lógico  
estadounidense  Willard  Van  Orman  Quine  (1908  –  2000).

Si  una  función  es  referencialmente  transparente,  significa  que  cada  vez  que  llamamos  a  la  función  con  los  
mismos  valores  de  entrada,  siempre  recibiremos  la  misma  salida.  Una  función  escrita  en  un  lenguaje  
verdaderamente  funcional,  que  evalúa  una  expresión  y  devuelve  su  valor,  no  hace  nada  más.  En  otras  
palabras,  teóricamente  podemos  sustituir  la  llamada  de  función  directamente  con  su  valor  de  resultado,  y  
este  cambio  no  tendrá  ningún  impacto.  Esto  nos  permite  encadenar  funciones  como  si  fueran  cajas  negras.

La  transparencia  referencial  nos  lleva  directamente  al  concepto  de  función  pura.

Funciones  puras  frente  a  funciones  impuras  Aquí  

hay  un  ejemplo  simple  de  una  función  pura  en  C++:

Listado  7­1.  Un  ejemplo  simple  de  una  función  pura  en  C++

cuadrado  doble  ( valor  doble  const )  noexcept  { valor  de  retorno  
*  valor; };

Como  se  puede  ver  fácilmente,  el  valor  de  salida  de  square()  depende  únicamente  del  valor  del  argumento  que  se  pasa  
a  la  función,  por  lo  que  llamar  a  square()  dos  veces  con  el  mismo  valor  de  argumento  producirá  el  mismo  resultado  cada  vez.  
No  tenemos  efectos  secundarios,  porque  si  se  completa  alguna  llamada  de  esta  función,  no  deja  ninguna  "suciedad"  que  pueda  
influir  en  las  llamadas  posteriores  de  square().  Tales  funciones,  que  son  completamente  independientes  de  un  estado  externo,  
que  no  tienen  efectos  secundarios  y  que  producirán  siempre  la  misma  salida  para  las  mismas  entradas,  en  concreto:  que  son  
referencialmente  transparentes,  se  denominan  funciones  puras .
Por  el  contrario,  los  paradigmas  de  programación  imperativa,  como  la  programación  orientada  a  objetos  o  de  procedimientos,
no  proporcione  esta  garantía  de  ausencia  de  efectos  secundarios,  como  muestra  el  siguiente  ejemplo:

Listado  7­2.  Un  ejemplo  que  demuestra  que  las  funciones  miembro  de  las  clases  pueden  causar  efectos  secundarios

#incluir  <iostream>

class  Clazz  
{ public:  
int  functionWithSideEffect(const  int  value)  noexcept  { return  value  *  value  +  
someKindOfMutualState++; }

privado:  int  
someKindOfMutualState  { 0 }; };

170
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

int  main()  { Clazz  
instanciaDeClazz  { };  std::cout  <<  
instanciaDeClazz.functionWithSideEffect(3)  <<  std::endl; //  Salida:  "9"  std::cout  <<  instanceOfClazz.functionWithSideEffect(3)  
<<  std::endl; //  Salida:  "10"  std::cout  <<  instanciaDeClazz.functionWithSideEffect(3)  <<  std::endl; //  Salida:  "11"  devuelve  
0; }

En  este  caso,  cada  llamada  de  la  función  miembro  con  el  nombre  revelador  Clazz::functionWithSideEffect()  alterará  un  estado  
interno  de  la  instancia  de  la  clase  Clazz.  Como  consecuencia,  cada  llamada  de  esta  función  miembro  devuelve  un  resultado  
diferente,  aunque  el  argumento  dado  para  el  parámetro  de  la  función  es  siempre  el  mismo.  Puede  tener  efectos  similares  en  la  
programación  de  procedimientos  con  variables  globales  que  son  manipuladas  por  procedimientos.
Las  funciones  que  pueden  producir  diferentes  salidas  incluso  si  se  llaman  siempre  con  los  mismos  argumentos,  se  denominan  
funciones  impuras.  Otro  indicador  claro  de  que  una  función  es  impura  es  cuando  tiene  sentido  llamarla  sin  usar  su  valor  de  retorno.  
Si  puede  hacer  eso,  esta  función  debe  tener  algún  tipo  de  efecto  secundario.
En  un  entorno  de  ejecución  de  subproceso  único,  los  estados  globales  pueden  causar  pocos  problemas  y  molestias.  Pero  
ahora  imagine  que  tiene  un  entorno  de  ejecución  de  subprocesos  múltiples,  donde  se  ejecutan  varios  subprocesos,  llamando  a  
funciones  en  un  orden  no  determinista.  En  un  entorno  de  este  tipo,  los  estados  globales,  o  los  estados  de  instancias  de  
todo  el  objeto,  a  menudo  son  problemáticos  y  pueden  causar  un  comportamiento  impredecible  o  errores  sutiles.

Programación  funcional  en  C++  moderno
Lo  crea  o  no,  ¡pero  la  programación  funcional  siempre  ha  sido  parte  de  C++!  Con  este  lenguaje  multiparadigma,  siempre  
podías  programar  en  un  estilo  funcional,  incluso  con  C++98.  La  razón  por  la  que  puedo  afirmar  esto  con  la  mejor  conciencia  
es  la  existencia  de  la  metaprogramación  de  plantilla  conocida  (TMP)  desde  el  comienzo  de  C  ++  (TMP  es,  por  cierto,  un  tema  
muy  complicado  y,  por  lo  tanto,  un  desafío  para  muchos,  incluso  los  expertos  y  desarrolladores  experimentados).

Programación  funcional  con  plantillas  de  C++  Lo  que  saben  muchos  desarrolladores  

de  C++  es  que  la  metaprogramación  de  plantillas  es  una  técnica  en  la  que  un  compilador  utiliza  las  denominadas  plantillas  para  
generar  código  fuente  de  C++  en  un  paso  antes  de  que  el  compilador  traduzca  el  código  fuente  a  código  objeto.  Lo  que  
muchos  programadores  pueden  no  saber  es  el  hecho  de  que  la  metaprogramación  de  plantilla  es  programación  
funcional  y  que  es  Turing  Complete.

TOTALIDAD  DE  TURING

El  término  Turing  Complete,  llamado  así  por  el  conocido  informático,  matemático,  lógico  y  criptoanalista  inglés  
Alan  Turing  (1912  ­  1954),  se  usa  a  menudo  para  definir  qué  hace  que  un  lenguaje  sea  un  lenguaje  de  programación  
"real".  Un  lenguaje  de  programación  se  caracteriza  como  Turing  Completo,  si  puede  resolver  cualquier  problema  
posible  con  él  que  pueda  ser  computado  teóricamente  por  una  Máquina  de  Turing.  Una  máquina  de  Turing  es  una  
máquina  abstracta  y  teórica  inventada  por  Alan  Turing  que  sirve  como  modelo  idealizado  para  los  cálculos.

En  la  práctica,  ningún  sistema  informático  es  realmente  Turing  Completo.  La  razón  es  que  la  Completitud  de  Turing  
ideal  requiere  memoria  ilimitada  y  recurrencias  ilimitadas,  lo  que  los  sistemas  informáticos  actuales  no  pueden  ofrecer.
Por  lo  tanto,  algunos  sistemas  se  aproximan  a  la  integridad  de  Turing  modelando  una  memoria  ilimitada,  pero  restringida  
por  una  limitación  física  en  el  hardware  subyacente.

171
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Como  prueba,  calcularemos  el  máximo  común  divisor  (MCD)  de  dos  enteros  utilizando  únicamente  TMP.
El  MCD  de  dos  enteros,  que  no  son  cero,  es  el  entero  positivo  más  grande  que  divide  a  los  dos  enteros  dados.

Listado  7­3.  Cálculo  del  máximo  común  divisor  usando  metaprogramación  de  plantilla

01  #incluye  <iostream>
02
03  template<  sin  signo  int  x,  sin  signo  int  y  >  04  struct  
GreatestCommonDivisor  { static  const  unsigned  
05 int  resultado  =  GreatestCommonDivisor<  y,  x  %  y  >::result;  06 };  07

08  template<  unsigned  int  x  >  09  struct  
GreatestCommonDivisor<  x,  0  >  { static  const  unsigned  
10 int  result  =  x;  11};

12
13  int  main()  { std::cout  
"
<<  "El  GCD  de  40  y  10  es:  14  std::endl;  std::cout  <<   <<  MayorDivisorComún<40u,  10u>::resultado  <<
15  
"El  GCD  de  366  y  60  es:  
"
16 std::endl;  devolver  0; <<  MayorDivisorComún<366u,  60u>::resultado  <<
17  
18
19 }

Esta  es  la  salida  que  genera  nuestro  programa:

El  MCD  de  40  y  10  es:  10
El  MCD  de  366  y  60  es:  6

Lo  notable  de  este  estilo  de  calcular  el  GCD  en  tiempo  de  compilación  usando  plantillas  es  que  es  una  programación  
funcional  real.  Las  dos  plantillas  de  clase  utilizadas  están  completamente  libres  de  estados.  No  hay  variables  mutables,  
lo  que  significa  que  ninguna  variable  puede  cambiar  su  valor  una  vez  que  se  ha  inicializado.  Durante  la  instanciación  de  
la  plantilla,  se  inicia  un  proceso  recursivo  que  se  detiene  cuando  entra  en  juego  la  plantilla  de  clase  especializada  en  la  
línea  9  ­  11.  Y,  como  ya  se  mencionó  anteriormente,  tenemos  Completitud  de  Turing  en  la  metaprogramación  de  plantillas,  
lo  que  significa  que  cualquier  cálculo  concebible  se  puede  realizar  en  tiempo  de  compilación  utilizando  esta  técnica.

Bueno,  la  metaprogramación  de  plantillas  es  sin  duda  una  herramienta  poderosa,  pero  también  tiene  algunas  desventajas.
En  particular,  la  legibilidad  y  la  comprensión  del  código  pueden  sufrir  drásticamente  si  se  utiliza  una  gran  cantidad  de  
metaprogramación  de  plantilla.  La  sintaxis  y  las  expresiones  idiomáticas  de  TMP  son  difíciles  de  entender,  sin  mencionar  
esos  mensajes  de  error  extensos  y,  a  menudo,  crípticos  cuando  algo  sale  mal.  Y,  por  supuesto,  el  tiempo  de  compilación  
también  aumenta  con  un  uso  extensivo  de  la  metaprogramación  de  plantillas.  Por  lo  tanto,  TMP  es  sin  duda  una  forma  
adecuada  de  diseñar  y  desarrollar  bibliotecas  genéricas  (consulte  Biblioteca  estándar  de  C++),  pero  solo  debe  usarse  en  
código  de  aplicación  moderno  y  bien  elaborado  si  se  requiere  este  tipo  de  programación  genérica  (p.  ej.,  para  minimizar  la  
duplicación  de  código) .
Por  cierto,  desde  C++11  ya  no  es  necesario  usar  metaprogramación  de  plantillas  para  los  cálculos  en  tiempo  de  
compilación.  Con  la  ayuda  de  expresiones  constantes  (constexpr;  consulte  la  sección  sobre  cálculos  durante  el  tiempo  de  
compilación  en  el  Capítulo  5),  el  GCD  se  puede  implementar  fácilmente  como  una  función  recursiva  habitual,  como  en  el  
siguiente  ejemplo:

172
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Listado  7­4.  Una  función  GCD  que  usa  recursividad  que  se  puede  evaluar  en  tiempo  de  compilación

constexpr  unsigned  int  greatCommonDivisor(const  unsigned  int  x,
const  unsigned  int  y)  noexcept  { return  y  ==  
0 ?  x :  mayorDivisorComún(y,  x  %  y); }

Por  cierto,  el  algoritmo  matemático  detrás  de  esto  se  llama  algoritmo  de  Euclides,  o  algoritmo  de  Euclides,
lleva  el  nombre  del  antiguo  matemático  griego  Euclides.
Y  con  C++17,  el  algoritmo  numérico  std::gcd()  se  ha  convertido  en  parte  de  la  biblioteca  estándar  de  C++
(definido  en  el  encabezado  <numérico>),  por  lo  tanto,  ya  no  es  necesario  implementarlo  por  su  cuenta.

Listado  7­5.  Usando  la  función  std::gcd  del  encabezado  <numeric>

#include  <iostream>  
#include  <numérico>

int  main()  
{ constexpr  resultado  automático  =  std::gcd(40,  10);  
"
std::cout  <<  "El  MCD  de  40  y  10  es:  <<  resultado  <<  std::endl;  return  0;

Objetos  similares  a  funciones  (funtores)
Lo  que  siempre  fue  posible  en  C++  desde  el  principio  es  la  definición  y  el  uso  de  los  llamados  objetos  similares  a  funciones,  
también  conocidos  como  funtores  (otro  sinónimo  es  funcional)  en  resumen.  Técnicamente  hablando,  un  Functor  es  más  o  
menos  una  clase  que  define  el  operador  de  paréntesis,  es  decir,  el  operador().  Después  de  la  creación  de  instancias  de  estas  
clases,  se  pueden  usar  prácticamente  como  funciones.
Dependiendo  de  si  el  operador  ()  no  tiene  ninguno,  uno  o  dos  parámetros,  el  Funtor  se  llama  Generador,  Función  
unaria  o  Función  binaria.  Veamos  primero  un  Generador.

Generador
Como  revela  el  nombre  "Generador",  este  tipo  de  Funtor  se  utiliza  para  producir  algo.

Listado  7­6.  Un  ejemplo  de  un  generador,  un  funtor  que  se  llama  sin  argumento

clase  GeneradorNumeroCreciente  { public:  int  
operator()
()  noexcept  { return  numero++; }

privado:  
número  int  { 0 }; };

173
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

El  principio  de  funcionamiento  es  bastante  simple:  cada  vez  que  se  llama  a  AumentarNúmeroGenerador::operador(),  el  
valor  real  de  la  variable  miembro  número  se  devuelve  a  la  persona  que  llama,  y  luego  el  valor  de  esta  variable  miembro  se  
incrementa  en  1.  El  siguiente  ejemplo  de  uso  se  imprime  una  secuencia  de  los  números  0  a  2  en  la  salida  estándar:

int  principal()  {
Generador  de  números  crecientes  generador  de  números  { };  
std::cout  <<  numberGenerator()  <<  std::endl;  std::cout  <<  
numberGenerator()  <<  std::endl;  std::cout  <<  numberGenerator()  
<<  std::endl;  devolver  0;

Recuerde  la  cita  de  Sean  Parent  que  presenté  en  la  sección  sobre  algoritmos  en  el  Capítulo  5:  ¡nada  de  bucles  sin  
procesar!  Para  llenar  un  std::vector<T>  con  una  cierta  cantidad  de  valores  crecientes,  no  debemos  implementar  un  bucle  hecho  
a  mano.  En  su  lugar,  podemos  usar  std::generate  definido  en  el  encabezado  <algoritmo>,  una  plantilla  de  función  que  asigna  
a  cada  elemento  en  un  cierto  rango  un  valor  generado  por  un  objeto  Generador  dado.
Por  lo  tanto,  podemos  escribir  el  siguiente  código  simple  y  bien  legible  para  llenar  un  vector  con  una  secuencia  de  números  
crecientes  usando  nuestro  Generador  de  Números  Crecientes:

Listado  7­7.  Llenar  un  vector  con  una  secuencia  numérica  creciente  usando  std::generate

#include  <algoritmo>  
#include  <vector>

usando  Números  =  std::vector<int>;

int  main()  { const  
std::size_t  CANTIDAD_DE_NUMEROS  { 100 };  números  
números  (AMOUNT_OF_NUMBERS);  
std::generate(std::begin(números),  std::end(números),  GeneradorDeNúmeroCreciente()); // ...ahora  los  'números'  
contienen  valores  del  0  al  99...  devuelve  0; }

Como  uno  puede  imaginar  fácilmente,  este  tipo  de  Funtores  no  cumplen  con  los  requisitos  estrictos  de  las  
funciones  puras.  Los  generadores  suelen  tener  un  estado  mutable,  es  decir,  cuando  se  llama  a  operator(),  estos  Functors  suelen  
tener  algún  efecto  secundario.  En  nuestro  caso,  el  estado  mutable  está  representado  por  la  variable  miembro  privada  
GeneradorDeNúmeroCreciente::número,  que  se  incrementa  después  de  cada  llamada  del  operador  de  paréntesis.

■  Sugerencia  El  encabezado  <numeric>  ya  contiene  una  plantilla  de  función  std::iota(),  denominada  así  por  el  símbolo  
funcional     (Iota)  del  lenguaje  de  programación  APL,  que  no  es  un  funtor  generador,  pero  se  puede  usar  para  llenar  
un  contenedor  con  una  secuencia  ascendente  de  valores  de  forma  elegante.

Otro  ejemplo  de  un  objeto  similar  a  una  función  de  tipo  Generador  es  la  siguiente  plantilla  de  funtor  generador  de  números  
aleatorios.  Este  Functor  encapsula  todo  lo  necesario  para  la  inicialización  y  el  uso  de  un  generador  de  números  pseudoaleatorios  
(PRNG)  basado  en  el  llamado  algoritmo  Mersenne  Twister  (definido  en  el  encabezado  <random>).

174
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Listado  7­8.  Una  plantilla  de  clase  de  funtor  generador,  que  encapsula  un  generador  de  números  pseudoaleatorios

#incluir  <aleatorio>

template  <typename  NUMTYPE>  
class  RandomNumberGenerator  
{ public:  
RandomNumberGenerator()  
{ mersenneTwisterEngine.seed(randomDevice()); }

NUMTYPE  operator()()  
{ distribución  de  retorno  (mersenneTwisterEngine); }

privado:  
std::random_device  randomDevice;  
std::uniform_int_distribution<NUMTYPE>  distribución;  std::mt19937_64  
mersenneTwisterEngine; };

Y  así  es  como  se  podría  usar  el  Functor  RandomNumberGenerator:

Listado  7­9.  Llenar  un  vector  con  100  números  aleatorios

#include  "RandomGenerator.h"  
#include  <algoritmo>  
#include  <funcional>  
#include  <iostream>  
#include  <vector>

usando  Números  =  std::vector<short>;  const  
std::size_t  CANTIDAD_DE_NUMEROS  { 100 };

Números  createVectorFilledWithRandomNumbers()  {
RandomNumberGenerator<corto>  randomNumberGenerator  { };
Números  números  aleatorios  (AMOUNT_OF_NUMBERS);  
std::generate(begin(randomNumbers),  end(randomNumbers),  std::ref(randomNumberGenerator));  devuelve  números  
aleatorios;
}

void  printNumbersOnStdOut(const  Numbers&  randomNumbers)  { for  (const  
auto&  number :  randomNumbers)  { std::cout  <<  number  
<<  std::endl;
}
}

int  principal()  {
Números  números  aleatorios  =  createVectorFilledWithRandomNumbers();  
imprimirNúmerosEnStdOut(NúmerosAleatorios);  
devolver  0;
}

175
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Función  unaria  A  

continuación,  veamos  un  ejemplo  de  un  objeto  similar  a  una  función  unaria,  que  es  un  Functor  cuyo  operador  de  
paréntesis  tiene  un  parámetro.

Listado  7­10.  Un  ejemplo  de  un  funtor  unario

class  ToSquare  
{ public:  
constexpr  int  operator()(const  int  value)  const  noexcept  { return  value  *  value; } };

Como  sugiere  su  nombre,  este  Functor  eleva  al  cuadrado  los  valores  que  se  le  pasan  en  el  operador  de  paréntesis.  
El  operator()  se  declara  como  const,  que  es  un  indicador  de  que  se  comporta  como  una  función  pura,  es  decir,  una  llamada  no  
tendrá  efectos  secundarios.  Esto  no  necesariamente  tiene  que  ser  siempre  el  caso,  porque,  por  supuesto,  también  un  Functor  
unario  puede  tener  variables  miembro  privadas  y,  por  lo  tanto,  un  estado  mutable.
Con  el  ToSquare  Functor,  ahora  podemos  extender  el  ejemplo  anterior  y  aplicarlo  al  vector  con  la  secuencia  de  enteros  
ascendentes.

Listado  7­11.  Los  100  números  de  un  vector  están  elevados  al  cuadrado

#include  <algoritmo>  
#include  <vector>

usando  Números  =  std::vector<int>;

int  main()  { const  
std::size_t  CANTIDAD_DE_NUMEROS  =  100;  números  
números  (AMOUNT_OF_NUMBERS);  
std::generate(std::begin(números),  std::end(números),  GeneradorDeNúmeroCreciente());  
std::transform(std::begin(números),  std::end(números),  std::begin(números),  ToSquare()); // ...

devolver  0; }

El  algoritmo  usado  std::transform  (definido  en  el  encabezado  <algoritmo>)  aplica  la  función  u  objeto  de  función  
dado  a  un  rango  (definido  por  los  dos  primeros  parámetros)  y  almacena  el  resultado  en  otro  rango  (definido  por  el  tercer  
parámetro).  En  nuestro  caso,  ambos  rangos  son  iguales.

predicados

Un  tipo  especial  de  funtores  son  los  predicados.  Un  functor  unario  se  llama  predicado  unario  si  tiene  un  parámetro  y  un  valor  
de  retorno  booleano  que  indica  el  resultado  verdadero  o  falso  de  alguna  prueba,  como  en  el  siguiente  ejemplo:

Listado  7­12.  Un  ejemplo  de  predicado

class  IsAnOddNumber  
{ public:  
constexpr  bool  operator()(const  int  value)  const  noexcept  { return  (valor  %  2) !=  0; } };

176
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Este  Predicado  ahora  se  puede  aplicar  a  nuestra  secuencia  numérica  usando  el  algoritmo  std::remove_if  para  deshacerse  
de  todos  los  números  impares.  El  problema  es  que  el  nombre  de  este  algoritmo  es  engañoso.  En  realidad,  no  elimina  nada.  
Cualquier  elemento  que  no  coincida  con  el  Predicado  (en  nuestro  caso,  todos  los  números  pares),  se  mueve  al  principio  del  
contenedor  para  que  los  elementos  que  se  eliminen  estén  al  final.  Luego,  std::remove_if  devuelve  un  iterador  que  apunta  al  
comienzo  del  rango  que  se  eliminará.  Este  iterador  puede  ser  utilizado  por  la  función  miembro  std::vector::erase()  para  eliminar  
verdaderamente  los  elementos  no  deseados  del  vector.  Esta,  por  cierto,  técnica  muy  eficiente  se  llama  la  expresión  Erase­remove.

Listado  7­13.  Todos  los  números  impares  del  vector  se  eliminan  usando  la  expresión  Borrar­eliminar

#include  <algoritmo>  
#include  <vector>

usando  Números  =  std::vector<int>;

int  main()  { const  
std::size_t  CANTIDAD_DE_NUMEROS  =  100;  números  
números  (AMOUNT_OF_NUMBERS);  
std::generate(std::begin(números),  std::end(números),  GeneradorDeNúmeroCreciente());  std::transform(std::begin(números),  
std::end(números),  std::begin(números),  ToSquare());  números.erase(std::remove_if(std::begin(numbers),  std::end(numbers),  
IsAnOddNumber()),  std::end(numbers)); // ...

devolver  0; }

Para  poder  usar  un  Functor  de  una  manera  más  flexible  y  genérica,  generalmente  se  implementa  como  una  plantilla  de  
clase.  Por  lo  tanto,  podemos  refactorizar  nuestro  funtor  unario  IsAnOddNumber  en  una  plantilla  de  clase  para  que  pueda  usarse  
con  todos  los  tipos  integrales,  como  short,  int,  unsigned  int,  etc.  Y  desde  C++  11,  el  lenguaje  proporciona  los  llamados  rasgos  
de  tipo  (definido  en  el  encabezado  <type_traits>),  podemos  asegurar  que  la  plantilla  se  use  únicamente  con  tipos  integrales,  
como  se  muestra  en  el  siguiente  ejemplo:

Listado  7­14.  Asegurarse  de  que  el  parámetro  de  la  plantilla  sea  un  tipo  de  datos  integral

#incluir  <tipo_rasgos>

template  <typename  INTTYPE>  class  
IsAnOddNumber  { public:  

static_assert(std::is_integral<INTTYPE>::value,  "IsAnOddNumber  
requiere  un  tipo  entero  para  su  parámetro  de  plantilla  INTTYPE!");  constexpr  bool  operator()(const  INTTYPE  
value)  const  noexcept  { return  (value  %  2) !=  0; } };

Desde  C++11,  el  lenguaje  proporciona  static_assert(),  una  verificación  de  afirmación  que  se  realiza  en  tiempo  de  compilación.
En  nuestro  caso,  static_assert()  se  utiliza  para  comprobar  durante  la  instanciación  de  la  plantilla  que  el  parámetro  de  plantilla  
INTTYPE  es  de  tipo  integral  utilizando  el  rasgo  de  tipo  std::is_integral<T>.  La  ubicación  dentro  del  cuerpo  de  la  función  
main(),  donde  se  usa  el  predicado  (la  construcción  borrar­eliminar),  ahora  debe  ajustarse  un  poco:

// ...  
números.erase(std::remove_if(std::begin(numbers),  std::end(numbers),  
IsAnOddNumber<Numbers::value_type>()),  std::end(numbers)); // ...

177
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Si  ahora  usamos  inadvertidamente  la  plantilla  con  un  tipo  de  datos  no  integral,  como  doble,  obtenemos  un
convincente  mensaje  de  error  del  compilador:

[...] ../
src/Functors.h:  En  instanciación  de  'class  IsAnOddNumber<double>': ../src/Main.cpp:13:94:  
requerido  desde  aquí ../src/Functors.h:  42:3:  error:  la  
aserción  estática  falló:  ¡IsAnOddNumber  requiere  un  tipo  entero  para  su  parámetro  de  plantilla  INTTYPE!  [...]

RASGOS  DEL  TIPO

Las  plantillas  son  la  base  de  la  programación  genérica.  Los  contenedores  de  la  biblioteca  estándar  de  C++,  pero  también  
los  iteradores  y  los  algoritmos,  son  ejemplos  destacados  de  programación  genérica  muy  flexible  que  utiliza  el  concepto  de  
plantilla  de  C++.  Sin  embargo,  desde  un  punto  de  vista  técnico,  solo  se  lleva  a  cabo  un  simple  procedimiento  textual  
de  búsqueda  y  reemplazo  si  se  crea  una  instancia  de  una  plantilla  con  argumentos  de  plantilla.  Por  ejemplo,  si  un  
parámetro  de  plantilla  se  llama  T,  cada  aparición  de  T  se  reemplaza  por  el  tipo  de  datos  que  se  pasa  como  argumento  de  
plantilla  durante  la  instanciación  de  la  plantilla.

El  problema  es  este:  no  todos  los  tipos  de  datos  son  adecuados  para  la  creación  de  instancias  de  cada  plantilla.  
Por  ejemplo,  si  ha  definido  una  operación  matemática  como  una  plantilla  de  C++  Funtor  para  que  pueda  
usarse  para  diferentes  tipos  de  datos  numéricos  (cortos,  enteros,  dobles,  etc.),  no  tiene  ningún  sentido  crear  una  
instancia  de  esta  plantilla  con  std ::cadena.

El  encabezado  de  la  biblioteca  estándar  de  C++  <type_traits>  (disponible  desde  C++11)  proporciona  una  colección  
completa  de  comprobaciones  para  recuperar  información  sobre  los  tipos  pasados  como  argumentos  de  plantilla  en  tiempo  
de  compilación.  En  otras  palabras,  con  la  ayuda  de  los  rasgos  de  tipo,  puede  definir  requisitos  verificables  por  el  compilador  
que  deben  cumplir  los  argumentos  de  la  plantilla.

Por  ejemplo,  puede  asegurarse  de  que  el  tipo  que  se  usa  para  la  creación  de  instancias  de  plantilla  debe  ser  
construible  por  copia  combinado  con  la  garantía  de  seguridad  de  excepción  de  no  lanzamiento  (consulte  la  sección  
"La  garantía  de  no  lanzamiento"  en  el  Capítulo  5)  usando  el  rasgo  de  tipo  std : :is_nothrow_copy_construible<T>.

template  <typename  T>  
class  Clazz  
{ static_assert(std::is_nothrow_copy_construtible<T>::value,
"¡El  tipo  dado  para  T  debe  ser  construible  por  copia  y  no  puede  arrojar!"); // ...

Los  rasgos  de  tipo  no  solo  se  pueden  usar  junto  con  static_assert()  para  cancelar  la  compilación  con  un  mensaje  de  error.  
Por  ejemplo,  también  se  pueden  usar  para  una  expresión  idiomática  llamada  SFINAE  (la  falla  de  sustitución  no  es  un  
error)  que  se  analiza  con  más  detalle  en  la  sección  sobre  expresiones  idiomáticas  en  el  Capítulo  9 .

Por  último,  pero  no  menos  importante,  echemos  un  vistazo  al  Binary  Funtor.

178
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Funtores  binarios
Como  ya  se  mencionó  anteriormente,  un  Binary  Funtor  es  un  objeto  similar  a  una  función  que  toma  dos  parámetros.  Si  
dicho  Functor  opera  sobre  sus  dos  parámetros  para  realizar  algún  cálculo  (por  ejemplo,  una  suma)  y  devuelve  el  resultado  de  
esta  operación,  se  denomina  Operador  binario.  Si  dicho  Functor  tiene  un  valor  de  retorno  booleano  como  resultado  de  alguna  
prueba,  como  se  muestra  en  el  siguiente  ejemplo,  se  denomina  Predicado  binario.

Listado  7­15.  Un  ejemplo  de  un  predicado  binario  que  compara  sus  dos  parámetros

class  IsGreaterOrEqual  { public:  
bool  
operator()(const  auto&  value1,  const  auto&  value2)  const  noexcept  {
devolver  valor1  >=  valor2;

} };

■  Nota  Hasta  C++11,  era  una  buena  práctica  que  los  funtores,  dependiendo  de  su  número  de  parámetros,  
se  derivaran  de  las  plantillas  std::unary_function  respectivamente  std::binary_function  (ambas  
definidas  en  el  encabezado  <funcional>).  Estas  plantillas  se  etiquetaron  como  obsoletas  con  C++  11  y  se  
eliminaron  de  la  biblioteca  estándar  con  el  estándar  C++  17  reciente.

Carpetas  y  envoltorios  de  funciones
El  próximo  paso  de  desarrollo  en  términos  de  programación  funcional  en  C++  se  realizó  con  la  publicación  del  borrador  
del  Informe  técnico  1  de  C++  (TR  1)  en  2005,  que  es  el  nombre  común  para  las  extensiones  de  biblioteca  estándar  ISO/
IEC  TR  19768:2007  C++.  El  TR  1  especifica  una  serie  de  extensiones  de  la  biblioteca  estándar  de  C++,  incluidas,  entre  
otras  cosas,  extensiones  para  la  programación  funcional.  Este  informe  técnico  fue  la  propuesta  de  extensión  de  biblioteca  para  
el  estándar  C++11  posterior  y,  de  hecho,  12  de  las  13  bibliotecas  propuestas  (con  ligeras  modificaciones)  también  se  
incorporaron  al  nuevo  estándar  de  lenguaje  que  se  publicó  en  2011.
En  términos  de  programación  funcional,  el  TR  1  introdujo  las  dos  plantillas  de  funciones  std::bind  y
std::function,  que  se  definen  en  el  encabezado  de  la  biblioteca  <funcional>.
La  plantilla  de  función  std::bind  es  un  contenedor  de  carpetas  para  funciones  y  sus  argumentos.  Puedes  tomar
una  función  (o  un  puntero  de  función,  o  un  Funtor)  y  "vincular"  los  valores  reales  a  uno  o  todos  los  parámetros  de  la  
función.  En  otras  palabras,  puede  crear  nuevos  objetos  similares  a  funciones  a  partir  de  funciones  o  Funtores  existentes.
Comencemos  con  un  ejemplo  simple:

Listado  7­16.  Usando  std::bind  para  envolver  la  función  binaria  multiplicar()

#include  <funcional>  #include  
<iostream>

constexpr  double  multiplicand(const  doble  multiplicando,  const  doble  multiplicador)  noexcept  {
devuelve  multiplicando  *  multiplicador; }

179
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

int  main()  
{ const  auto  resultado1  =  multiplicar(10.0,  5.0);  
autoboundMultiplyFunctor  =  std::bind(multiply,  10.0,  5.0);  const  auto  result2  
=boundMultiplyFunctor();

" "
std::cout  <<  "resultado1  =   <<  resultado1  <<  ",  resultado2  = <<  resultado2  <<  std::endl;
devolver  
0; }

En  este  ejemplo,  la  función  multiplicar()  está  envuelta,  junto  con  dos  literales  numéricos  de  coma  flotante  (10.0  y  
5.0),  usando  std::bind.  Los  literales  numéricos  representan  los  parámetros  reales  que  están  vinculados  a  los  dos  
argumentos  de  función  multiplicando  y  multiplicador.  Como  resultado,  obtenemos  un  nuevo  objeto  similar  a  una  función  
que  se  almacena  en  la  variableboundMultiplyFunctor.  Luego  se  puede  llamar  como  un  Functor  ordinario  usando  el  
operador  de  paréntesis.
Tal  vez  te  preguntes  ahora:  Bien,  pero  no  lo  entiendo.  ¿Cuál  es  el  propósito  de  eso?  Cuál  es  el
beneficio  práctico  de  la  plantilla  de  función  de  carpeta?
Bueno,  std::bind  permite  algo  que  se  conoce  como  aplicación  parcial  (o  aplicación  de  función  parcial)  en  
programación.  La  aplicación  parcial  es  un  proceso  en  el  que  solo  un  subconjunto  de  los  parámetros  de  la  función  está  
vinculado  a  valores  o  variables,  mientras  que  la  otra  parte  aún  no  está  vinculada.  Los  parámetros  independientes  se  
reemplazan  por  los  marcadores  de  posición  _1,  _2,  _3,  etc.,  que  se  definen  en  el  espacio  de  nombres  std::placeholders.

Listado  7­17.  Un  ejemplo  de  aplicación  de  función  parcial

#incluir  <funcional>
#incluir  <iostream>

constexpr  double  multiplicand(const  doble  multiplicando,  const  doble  multiplicador)  noexcept  {
devuelve  multiplicando  *  multiplicador; }

int  main()  
{ usando  el  espacio  de  nombres  std::placeholders;

auto  multiplicar  con  10  =  std::bind(multiplicar,  _1,  10.0);  <<  
"
<<  "resultado  = multiplicarCon10(5.0)  <<  std::endl;  estándar::cout  
devolver  
0; }

En  el  ejemplo  anterior,  el  segundo  parámetro  de  la  función  multiplicar()  está  vinculado  al  número  de  punto  flotante  
literal  10.0,  pero  el  primer  parámetro  está  vinculado  a  un  marcador  de  posición.  El  objeto  similar  a  una  función,  que  es  el  
valor  de  retorno  de  std::bind(),  se  almacena  en  la  variable  multiplicarCon10.  Esta  variable  ahora  se  puede  usar  como  
una  función,  pero  solo  necesitamos  pasar  un  parámetro:  el  valor  que  se  multiplicará  por  10.0.
La  aplicación  de  función  parcial  es  una  técnica  de  adaptación  que  nos  permite  utilizar  una  función  o  un  Functor  en
varias  situaciones,  donde  necesitamos  su  funcionalidad,  pero  donde  solo  podemos  proporcionar  algunos  pero  no  
todos  los  argumentos.  Además,  con  la  ayuda  de  los  marcadores  de  posición,  el  orden  de  los  parámetros  de  las  
funciones  se  puede  adaptar  al  orden  que  espera  el  código  del  cliente.  Por  ejemplo,  la  posición  del  multiplicando  y  el  
multiplicador  en  la  lista  de  parámetros  se  pueden  intercambiar  asignándolos  a  un  nuevo  objeto  similar  a  una  función  de  
la  siguiente  manera:

multiplicación  automática  con  la  posición  del  parámetro  intercambiado  =  std::bind(multiply,  _2,  _1);

180
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

En  nuestro  caso  con  la  función  multiplicar(),  esto  obviamente  no  tiene  sentido  (recuerde  la  propiedad  conmutativa  
de  la  multiplicación),  porque  el  nuevo  objeto  de  función  producirá  exactamente  los  mismos  resultados  que  la  función  
multiplicar()  original,  pero  en  otras  situaciones  una  adaptación  de  la  función  El  orden  de  los  parámetros  puede  mejorar  
la  usabilidad  de  una  función.  La  aplicación  de  función  parcial  es  una  herramienta  para  la  adaptación  de  la  interfaz.
Por  cierto,  especialmente  en  conjunto  con  funciones  como  parámetros  de  retorno,  la  deducción  automática  de  tipos  con  
su  palabra  clave  auto  (vea  la  sección  “Deducción  automática  de  tipos”  en  el  Capítulo  5 )  puede  proporcionar  servicios  
valiosos,  porque  si  inspeccionamos  lo  que  devuelve  el  compilador  GCC  de  lo  anterior  llamada  de  std::bind(),  es  un  objeto  
del  siguiente  tipo  complejo:

std::_Bind_helper<bool0,doble  (&)(doble,  doble),const  _Marcador<int2>  &,  const  _Marcador<int1>  &>::tipo

Aterrador,  ¿no?  Escribir  dicho  tipo  explícitamente  en  el  código  fuente  no  solo  es  un  poco  útil,  sino  que,  aparte  de  
eso,  la  legibilidad  del  código  también  sufre  considerablemente.  Gracias  a  la  palabra  clave  auto  no  es  necesario  definir  
estos  tipos  explícitamente.  Pero  en  esos  casos  excepcionales,  en  los  que  debe  hacerlo,  entra  en  juego  la  plantilla  de  clase  
std::function,  que  es  un  contenedor  de  funciones  polimórficas  de  propósito  general.  Esta  plantilla  puede  envolver  un  objeto  
invocable  arbitrario  (una  función  ordinaria,  un  Functor,  un  puntero  de  función,  etc.)  y  administra  la  memoria  utilizada  para  
almacenar  ese  objeto.  Por  ejemplo,  para  envolver  nuestra  función  de  multiplicación  multiplicar()  en  un  objeto  std::function,  
el  código  tiene  el  siguiente  aspecto:

std::function<doble(doble,  doble)>  multiplicarFunc  =  multiplicar;  resultado  automático  
=  multiplicarFunc(10.0,  5.0);

Ahora  que  hemos  discutido  std::bind,  std::function  y  la  técnica  de  aplicación  parcial,  tengo  un  mensaje  posiblemente  
decepcionante  para  usted:  desde  C++  11  y  la  introducción  de  expresiones  lambda,  la  mayoría  de  estas  plantillas  provienen  
de  C++  La  biblioteca  estándar  rara  vez  se  requiere.

Expresiones  lambda  Con  la  llegada  

de  C++11,  el  lenguaje  se  ha  ampliado  con  una  característica  nueva  y  notable:  ¡las  expresiones  lambda!  Otros  términos  
de  uso  frecuente  para  ellos  son  funciones  lambda,  literales  de  funciones  o  simplemente  lambdas.
A  veces  también  se  les  llama  Closures,  que  en  realidad  es  un  término  general  de  la  programación  funcional,  y  que,  por  cierto,  
tampoco  es  del  todo  correcto.

CIERRE

En  los  lenguajes  de  programación  imperativos,  estamos  acostumbrados  al  hecho  de  que  una  variable  deja  de  
estar  disponible  cuando  la  ejecución  del  programa  sale  del  ámbito  en  el  que  se  define  la  variable.  Por  ejemplo,  
si  se  realiza  una  función  y  vuelve  a  la  persona  que  la  llamó,  todas  las  variables  locales  de  esa  función  se  eliminan  
de  la  pila  de  llamadas  y  se  eliminan  de  la  memoria.

Por  otro  lado,  en  Programación  Funcional,  podemos  construir  un  Closure,  que  es  un  objeto  de  función  con  un  ámbito  
de  variable  local  persistente.  En  otras  palabras,  los  cierres  permiten  que  un  ámbito  con  algunas  o  todas  sus  variables  
locales  esté  vinculado  a  una  función,  y  que  este  objeto  de  ámbito  persista  mientras  exista  esa  función.

En  C++,  dichos  cierres  se  pueden  crear  con  la  ayuda  de  expresiones  lambda  debido  a  su  lista  de  captura  en  el  
introductor  lambda.  Un  Closure  no  es  lo  mismo  que  una  expresión  lambda,  así  como  un  objeto  (instancia)  en  
orientación  a  objetos  no  es  lo  mismo  que  su  clase.

181
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Lo  especial  de  las  expresiones  lambda  es  que  generalmente  se  implementan  en  línea,  es  decir,  en  el  punto  de  su  aplicación.  
Esto  a  veces  puede  mejorar  la  legibilidad  del  código  y  los  compiladores  pueden  aplicar  sus  estrategias  de  optimización  de  manera  aún  
más  eficiente.  Por  supuesto,  las  funciones  lambda  también  pueden  tratarse  como  datos,  por  ejemplo,  almacenarse  en  variables  o  
pasarse  como  un  argumento  de  función  a  una  llamada  función  de  orden  superior  (consulte  la  siguiente  sección  sobre  este  
tema).
La  estructura  básica  de  una  expresión  lambda  es  la  siguiente:

[lista  de  captura]  (lista  de  parámetros)  ­>  return_type_declaration  {cuerpo  lambda}

Dado  que  este  libro  no  es  una  introducción  al  lenguaje  C++,  no  explicaré  aquí  todos  los  conceptos  básicos  sobre  las  
expresiones  lambda.  Incluso  si  está  viendo  algo  como  esto  por  primera  vez,  debe  quedar  relativamente  claro  que  el  tipo  de  retorno,  
la  lista  de  parámetros  y  el  cuerpo  lambda  son  prácticamente  los  mismos  que  con  las  funciones  ordinarias.  Lo  que  puede  parecer  
inusual  a  primera  vista  son  dos  cosas.  Por  ejemplo,  una  expresión  lambda  no  tiene  nombre  como  una  función  ordinaria  o  un  
objeto  similar  a  una  función.  Esta  es  la  razón  por  la  que  se  habla  en  este  contexto  también  de  funciones  anónimas.  El  otro  elemento  
llamativo  es  el  corchete  al  principio,  que  también  se  denomina  introductor  lambda.  Como  sugiere  el  nombre,  el  introductor  lambda  
marca  el  comienzo  de  una  expresión  lambda.  Además,  el  introductor  también  contiene  opcionalmente  algo  que  se  denomina  
lista  de  captura.

Lo  que  hace  que  esta  lista  de  captura  sea  tan  importante  es  que  aquí  se  enumeran  todas  las  variables  del  ámbito  externo,  
que  deben  estar  disponibles  dentro  del  cuerpo  lambda,  y  si  deben  capturarse  por  valor  (copia)  o  por  referencia.  En  otras  palabras,  
estos  son  los  cierres  de  la  expresión  lambda.
Un  ejemplo  de  expresión  lambda  se  define  de  la  siguiente  manera:

[](const  doble  multiplicando,  const  doble  multiplicador)  { return  multiplicando  *  multiplicador; }

Esta  es  nuestra  buena  y  antigua  función  de  multiplicación  como  lambda.  El  introductor  tiene  una  lista  de  captura  en  blanco,  que
significa  que  no  se  utiliza  nada  del  alcance  circundante.  Además,  el  tipo  de  retorno  no  se  especifica  en  este  caso,  porque  el  
compilador  puede  deducirlo  fácilmente.
Al  asignar  la  expresión  lambda  a  una  variable,  se  crea  un  objeto  de  tiempo  de  ejecución  correspondiente,  el  llamado  cierre.  Y  esto  
es  realmente  cierto:  el  compilador  genera  una  clase  de  funtor  de  un  tipo  no  especificado  a  partir  de  una  expresión  lambda,  que  se  
instancia  en  tiempo  de  ejecución  y  se  asigna  a  la  variable.  Las  capturas  en  la  lista  de  capturas  se  convierten  en  parámetros  de  
constructor  y  variables  miembro  del  objeto  funtor.  Los  parámetros  en  la  lista  de  parámetros  de  lambda  se  convierten  en  parámetros  
para  el  operador  de  paréntesis  del  funtor  (operador()).

Listado  7­18.  Usando  la  expresión  lambda  para  multiplicar  dos  dobles

#incluir  <iostream>

int  main()  { auto  
multiplicar  =  [](const  doble  multiplicando,  const  doble  multiplicador)  { return  multiplicando  *  multiplicador; };  
std::cout  <<  multiplicar(10.0,  50.0)  <<  std::endl;  

devolver  0; }

Sin  embargo,  todo  se  puede  hacer  más  corto,  porque  una  expresión  lambda  se  puede  llamar  directamente  en
el  lugar  de  su  definición  agregando  paréntesis  con  argumentos  detrás  del  cuerpo  lambda.

182
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Listado  7­19.  Definición  y  llamada  de  una  expresión  lambda  de  una  sola  vez

int  main()  
{ std::cout  <<  []
(const  doble  multiplicando,  const  doble  multiplicador)  {
devuelve  multiplicando  *  multiplicador; }
(50.0,  10.0)  <<  estándar::endl;  
devolver  
0; }

El  ejemplo  anterior  es,  por  supuesto,  solo  para  fines  de  demostración,  ya  que  el  uso  de  una  lambda  en  este
El  estilo  definitivamente  no  tiene  sentido.  El  siguiente  ejemplo  utiliza  dos  expresiones  lambda.  Uno  es  usado  por  el  
algoritmo  std::transform  para  envolver  las  palabras  en  la  comilla  del  vector  de  cadena  con  paréntesis  angulares  y  
almacenarlas  en  otro  vector  llamado  resultado.  La  otra  expresión  lambda  es  utilizada  por  std::for_each  para  generar  el  
contenido  de  resultado  en  la  salida  estándar.

Listado  7­20.  Poner  cada  palabra  en  una  lista  entre  paréntesis  angulares

#include  <algoritmo>  
#include  <iostream>  
#include  <cadena>  
#include  <vector>

int  main()  
{ std::vector<std::string>  quote  { "Eso  es",  "uno",  "pequeño",  "paso",  "para",  "un",  "hombre",  "uno",
"Un  gran  salto  para  la  humanidad." };
std::vector<std::string>  resultado;

std::transform(begin(quote),  end(quote),  back_inserter(resultado),  [](const  std::string&  
word)  { return  "<"  +  word  +  ">"; });  std::for_each(begin(resultado),  
end(resultado),  [](const  std::string&  palabra)  { std::cout  
<<  palabra  <<  "  "; });

devolver  0;
}

La  salida  de  este  pequeño  programa  es:

<Eso  es>  <uno>  <pequeño>  <paso>  <para>  <un>  <hombre,>  <uno>  <gigante>  <salto>  <para>  <la  humanidad.>

Expresiones  lambda  genéricas  (C++14)
Con  la  publicación  de  C++14,  las  expresiones  lambda  experimentaron  algunas  mejoras  adicionales.  Desde  C++14  se  
permite  usar  auto  (consulte  la  sección  sobre  la  deducción  automática  de  tipos  en  el  Capítulo  5)  como  el  tipo  de  retorno  de  
una  función,  o  una  lambda.  En  otras  palabras,  el  compilador  deducirá  el  tipo.  Estas  expresiones  lambda  se  denominan  
expresiones  lambda  genéricas.

183
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Aquí  hay  un  ejemplo:

Listado  7­21.  Aplicar  una  expresión  lambda  genérica  en  valores  de  diferentes  tipos  de  datos

#include  <complejo>  
#include  <iostream>

int  main()  { auto  
cuadrado  =  [](const  auto&  value)  noexcept  { return  value  *  value; };

const  auto  resultado1  =  cuadrado  (12.56);  const  
auto  resultado2  =  cuadrado(25u);  const  auto  
resultado3  =  cuadrado(­6);  const  auto  result4  
=  cuadrado(std::complejo<doble>(4.0,  2.5));

"
std::cout  <<  "result1  es  std::cout   <<  resultado1  <<  "\n";  <<  
"
<<  "result2  es  std::cout  <<  "result3   resultado2  <<  "\n";  <<  
"
es  std::cout  <<  "result4  es resultado3  <<  "\n";  <<  
" resultado4  <<  std::endl;

devolver  0; }

El  tipo  de  parámetro,  así  como  el  tipo  de  resultado,  se  derivan  automáticamente  según  el  tipo  del  parámetro  concreto  
(literal)  cuando  se  compila  la  función  (en  el  ejemplo  anterior,  double,  unsigned  int,  int  y  un  número  complejo  de  tipo  std::complex  
<T>).  Las  lambdas  generalizadas  son  extremadamente  útiles  en  la  interacción  con  los  algoritmos  de  biblioteca  estándar,  porque  
son  de  aplicación  universal.

Funciones  de  orden  superior
Un  concepto  central  en  la  Programación  Funcional  son  las  llamadas  funciones  de  orden  superior.  Son  el  colgante  para  las  funciones  
de  primera  clase.  Una  función  de  orden  superior  es  una  función  que  toma  una  o  más  funciones  como  argumentos,  o  pueden  
devolver  una  función  como  resultado.  En  C++,  cualquier  objeto  invocable,  por  ejemplo,  una  instancia  del  envoltorio  std::function,  un  
puntero  de  función,  un  cierre  creado  a  partir  de  una  expresión  lambda,  un  Functor  hecho  a  mano  y  cualquier  otra  cosa  que  
implemente  operator()  se  puede  pasar  como  argumento.  a  una  función  de  orden  superior.
Podemos  mantener  esta  introducción  relativamente  breve,  porque  ya  hemos  visto  y  usado  varias  funciones  de  orden  superior.  
Muchos  de  los  algoritmos  (consulte  la  sección  sobre  algoritmos  en  el  Capítulo  5)  en  la  biblioteca  estándar  de  C++  son  este  tipo  
de  funciones.  Dependiendo  de  su  propósito,  toman  un  Operador  Unario,  Predicado  Unario  u  Operador  Binario  para  aplicarlo  
a  un  contenedor,  o  a  un  sub­rango  de  elementos  en  un  contenedor.
Por  supuesto,  a  pesar  de  que  el  encabezado  <algoritmo>  y  también  el  encabezado  <numérico>  brindan  una
selección  de  potentes  funciones  de  orden  superior  para  diferentes  propósitos,  también  puede  implementar  funciones  de  orden  
superior,  respectivamente,  o  plantillas  de  funciones  de  orden  superior  por  sí  mismo,  como  en  el  siguiente  ejemplo:

Listado  7­22.  Un  ejemplo  de  funciones  de  orden  superior  hechas  a  sí  mismas

#include  <funcional>  #include  
<iostream>  #include  <vector>

template<tipo  de  nombre  CONTAINERTYPE,  tipo  de  nombre  UNARYFUNCTIONTYPE>

184
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

void  myForEach(const  CONTAINERTYPE&  container,  UNARYFUNCTIONTYPE  unaryFunction)  { for  
(const  auto&  element :  container)  
{ unaryFunction(element); } }

template<typename  CONTAINERTYPE,  typename  UNARYOPERATIONTYPE>  
void  myTransform(CONTAINERTYPE&  container,  UNARYOPERATIONTYPE  unaryOperator)  { for  
(auto&  element :  container)  {
elemento  =  OperadorUnario(elemento); }

template<typename  NUMBERTYPE>  
class  ToSquare  
{ public:
NUMBERTYPE  operator()(const  NUMBERTYPE&  número)  const  noexcept  {
número  de  retorno  *  número;

} };

template<typename  TYPE>  
void  printOnStdOut(const  TYPE&  cosa)  { std::cout  
<<  cosa  <<  ",  "; }

int  main()  
{ std::vector<int>  números  { 1,  2,  3,  4,  5,  6,  7,  8,  9,  10 };  
myTransform(numbers,  ToSquare<int>());  
std::function<void(int)>  printNumberOnStdOut  =  printOnStdOut<int>;  
myForEach(numbers,  printNumberOnStdOut);  
devolver  0;
}

En  este  caso,  nuestras  dos  plantillas  de  funciones  de  orden  superior  hechas  por  nosotros  mismos,  myTransform()  y  myForEach(),  
solo  son  aplicables  a  contenedores  completos  porque,  a  diferencia  de  los  algoritmos  de  biblioteca  estándar,  no  tienen  una  interfaz  de  
iterador.  Sin  embargo,  el  punto  crucial  es  que  los  desarrolladores  pueden  proporcionar  funciones  personalizadas  de  orden  superior  que  
no  existen  en  la  biblioteca  estándar  de  C++.
Ahora  veremos  tres  de  estas  funciones  de  alto  orden  con  mayor  detalle,  porque  juegan  un  papel  importante
papel  en  la  programación  funcional.

Mapear,  filtrar  y  reducir  Cada  lenguaje  

de  programación  funcional  serio  debe  proporcionar  al  menos  tres  funciones  útiles  de  orden  superior:  mapear,  
filtrar  y  reducir  (sinónimo:  plegar).  Incluso  si  a  veces  pueden  tener  nombres  diferentes  según  el  lenguaje  de  
programación,  puede  encontrar  este  triunvirato  en  Haskell,  Erlang,  Clojure,  JavaScript,  Scala  y  muchos  otros  
lenguajes  con  capacidades  de  programación  funcional.  Por  lo  tanto,  podemos  afirmar  justificadamente  que  estas  
tres  funciones  de  orden  superior  forman  un  patrón  de  diseño  de  programación  funcional  muy  común.
Por  lo  tanto,  no  debería  sorprenderle  que  estas  funciones  de  orden  superior  también  estén  contenidas  en  
la  biblioteca  estándar  de  C++.  Y  quizás  tampoco  te  sorprenda  que  ya  hayamos  utilizado  algunas  de  estas  
funciones.

185
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Echemos  un  vistazo  consecutivo  a  cada  una  de  estas  funciones.

Mapa
El  mapa  podría  ser  el  más  fácil  de  entender  de  los  tres.  Con  la  ayuda  de  esta  función  de  orden  superior,  podemos  aplicar  
una  función  de  operador  a  cada  elemento  individual  de  una  lista.  En  C++,  esta  función  la  proporciona  el  algoritmo  de  
biblioteca  estándar  std::transform  (definido  en  el  encabezado  <algoritmo>)  que  ya  ha  visto  en  algunos  ejemplos  de  
código  anteriores.

Filtrar

También  el  filtro  es  fácil.  Como  sugiere  el  nombre,  esta  función  de  orden  superior  toma  un  Predicado  (consulte  la  
sección  sobre  Predicados  anteriormente  en  este  capítulo)  y  una  lista,  y  elimina  cualquier  elemento  de  la  lista  que  no  satisfaga  
la  condición  del  Predicado.  En  C++,  esta  función  la  proporciona  el  algoritmo  de  biblioteca  estándar  std::remove_if  (definido  
en  el  encabezado  <algoritmo>)  que  ya  ha  visto  en  algunos  ejemplos  de  código  anteriores.
Sin  embargo,  aquí  hay  otro  buen  ejemplo  para  filtrar  respectivamente  std::remove_if.  Si  padece  una  enfermedad  
llamada  "aibofobia",  que  es  un  término  humorístico  para  el  miedo  irracional  a  los  palíndromos,  debe  filtrar  los  palíndromos  
de  las  listas  de  palabras  de  la  siguiente  manera:

Listado  7­23.  Eliminar  todos  los  palíndromos  de  un  vector  de  palabras

#include  <algoritmo>  
#include  <iostream>  
#include  <cadena>  
#include  <vector>

class  IsPalindrome  { public:  
bool  
operator()(const  std::string&  word)  const  {
const  auto  middleOfWord  =  begin(word)  +  word.size() /  2;  return  
std::equal(begin(word),  middleOfWord,  rbegin(word)); } };

int  main()  
{ std::vector<std::string>  someWords  { "papá",  "hola",  "radar",  "vector",  "desanivelado",  "foo",  "bar",  "carro  de  carreras",  "  
ROTOR",  "",  "C++",  "aibofobia" };
algunasPalabras.erase(std::remove_if(begin(algunasPalabras),  fin(algunasPalabras),  EsPalindromo()),
end(algunasPalabras));  
std::for_each(begin(someWords),  end(someWords),  [](const  auto&  word)  {
std::cout  <<  palabra  <<  ","; });  

devolver  0; }

La  salida  de  este  programa  es:

hola,vector,foo,bar,C++,

186
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Reducir  (Doblar)
Reducir  (sinónimos:  Fold,  Collapse,  Aggregate)  es  la  más  poderosa  de  las  tres  funciones  de  orden  superior  y  puede  
ser  un  poco  difícil  de  entender  a  primera  vista.  Reduce  respectivamente  fold  es  una  función  de  orden  superior  para  
obtener  un  valor  de  resultado  único  aplicando  un  operador  binario  en  una  lista  de  valores.  En  C++,  esta  función  la  
proporciona  el  algoritmo  de  biblioteca  estándar  std::accumulate  (definido  en  el  encabezado  <numeric>).  
Algunos  dicen  que  std::accumulate  es  el  algoritmo  más  poderoso  de  la  biblioteca  estándar.
Para  comenzar  con  un  ejemplo  simple,  puede  obtener  fácilmente  la  suma  de  todos  los  números  enteros  en  un  vector  de  esta  manera:

Listado  7­24.  Construyendo  la  suma  de  todos  los  valores  en  un  vector  usando  std::accumulate

#include  <numérico>  
#include  <iostream>  
#include  <vector>

int  main()  
{ std::vector<int>  números  { 12,  45,  ­102,  33,  78,  ­8,  100,  2017,  ­110 };

const  int  sum  =  std::accumulate(begin(numbers),  end(numbers),  0);  std::cout  <<  "La  
"
suma  es:  <<  sum  <<  std::endl;  return  0;

Aquí  se  usó  la  versión  de  std::accumulate  que  no  espera  un  operador  binario  explícito  en  la  lista  de  parámetros.  
Usando  esta  versión  de  la  función,  simplemente  se  calcula  la  suma  de  todos  los  valores.  Por  supuesto,  puede  
proporcionar  un  operador  binario  propio,  como  en  el  siguiente  ejemplo  a  través  de  una  expresión  lambda:

Listado  7­25.  Encontrar  el  número  más  alto  en  un  vector  usando  std::accumulate

int  main()  
{ std::vector<int>  números  { 12,  45,  ­102,  33,  78,  ­8,  100,  2017,  ­110 };

const  int  maxValue  =  std::accumulate(begin(numbers),  end(numbers),  0,
[](const  int  value1,  const  int  value2)  { return  value1  
>  value2 ?  valor1 :  valor2; });  std::cout  <<  "El  número  

"
más  alto  es:  return  0; } <<  maxValor  <<  std::endl;

PLEGADO  IZQUIERDO  Y  DERECHO

La  programación  funcional  a  menudo  distingue  entre  dos  formas  de  plegar  una  lista  de  elementos:  un  pliegue  a  la  izquierda  y  un  
pliegue  a  la  derecha.

Si  combinamos  el  primer  elemento  con  el  resultado  de  combinar  recursivamente  el  resto,  esto  se  llama  un  pliegue  a  la  derecha.  En  
cambio,  si  combinamos  el  resultado  de  combinar  recursivamente  todos  los  elementos  excepto  el  último,  con  el  último  elemento,  esta  
operación  se  denomina  pliegue  izquierdo.

187
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Si,  por  ejemplo,  tomamos  una  lista  de  valores  que  se  van  a  doblar  con  un  operador  +  a  una  suma,  entonces  los  paréntesis  son  los  

siguientes  para  una  operación  de  doblado  a  la  izquierda:  ((A  +  B)  +  C)  +  D.  En  cambio,  con  un  pliegue  a  la  derecha,  los  paréntesis  
quedarían  así:  A  +  (B  +  (C  +  D)).  En  el  caso  de  una  operación  asociativa  simple  +,  el  resultado  no  hace  ninguna  diferencia  si  se  forma  
con  un  pliegue  a  la  izquierda  o  un  pliegue  a  la  derecha.
Pero  en  el  caso  de  funciones  binarias  no  asociativas,  el  orden  en  que  se  combinan  los  elementos  puede  influir  en  el  valor  del  resultado  final.

También  en  C++,  podemos  distinguir  entre  un  pliegue  a  la  izquierda  y  un  pliegue  a  la  derecha.  Si  usamos  std::accumulate  con  iteradores  
normales,  obtenemos  un  pliegue  a  la  izquierda:

std::accumulate(begin,  end,  init_value,  binary_operator)

En  cambio,  si  usamos  std::accumulate  con  un  iterador  inverso,  obtenemos  un  pliegue  a  la  derecha:

std::accumulate(rbegin,  rend,  init_value,  binary_operator)

Expresiones  de  plegado  en  C++17
Comenzando  con  C++17,  el  lenguaje  ha  ganado  una  característica  nueva  e  interesante  llamada  expresiones  de  pliegue.  Las  
expresiones  de  pliegue  de  C++17  se  implementan  como  las  denominadas  plantillas  variádicas  (disponibles  desde  C++11),  es  decir,  
como  plantillas  que  pueden  tomar  un  número  variable  de  argumentos  de  forma  segura.  Este  número  arbitrario  de  argumentos  se  
mantiene  en  un  llamado  paquete  de  parámetros.
Lo  que  se  ha  agregado  con  C++17  es  la  posibilidad  de  reducir  el  paquete  de  parámetros  directamente  con  la  ayuda  de  
un  operador  binario,  es  decir,  realizar  un  plegado.  La  sintaxis  general  de  las  expresiones  de  pliegue  de  C++17  es  la  siguiente:

( ...  operator  parampack )   //  pliegue  a  la  

( parampack  operator ... )  ( initvalue   izquierda //  pliegue  a  la  derecha

operator ...  operator  parampack ) //  doblar  a  la  izquierda  con  un  valor  inicial  ( parampack  operator ...  
operator  initvalue ) //  doblar  a  la  derecha  con  un  valor  inicial

Veamos  un  ejemplo,  un  pliegue  a  la  izquierda  con  un  valor  inicial:

Listado  7­26.  Un  ejemplo  de  un  pliegue  a  la  izquierda

#incluir  <iostream>

template<typename...  PACK>  int  
subtractFold(int  minuendo,  PACK...  sustraendos)  { ­  sustraendos);
retorno  (minuendo  ­ } ...

int  main()  { const  
int  resultado  =  subtractFold(1000,  55,  12,  333,  1,  12);  std::cout  <<  "El  resultado  
"
es:  <<  resultado  <<  std::endl;  return  0; }

Tenga  en  cuenta  que  en  este  caso  no  se  puede  utilizar  un  pliegue  por  la  derecha  debido  a  la  falta  de  asociatividad  del  operador–.  Doblar
Las  expresiones  son  compatibles  con  32  operadores,  incluidos  operadores  lógicos  como  ==,  &&  y  ||.

188
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Aquí  hay  otro  ejemplo  que  prueba  que  un  paquete  de  parámetros  contiene  al  menos  un  número  par:

Listado  7­27.  Comprobar  si  un  paquete  de  parámetros  contiene  un  valor  par

#incluir  <iostream>

template  <typename...  TYPE>  bool  
containsEvenValue(const  TYPE&...  argumento)  { return  ((argumento  
%  2  ==  0)  || ...); }

int  main()  { const  
bool  resultado1  =  contieneEvenValue(10,  7,  11,  9,  33,  14);  const  bool  result2  =  
contieneEvenValue(17,  7,  11,  9,  33,  29);

estándar::cout  <<  estándar::boolalpha;  
"
std::cout  <<  "resultado1  es  <<  resultado1  <<  "\n";  std::cout  <<  
"
"resultado2  es  <<  resultado2  <<  std::endl;  devolver  0; }

La  salida  de  este  programa  es:

resultado1  es  
verdadero  resultado2  es  falso

Código  limpio  en  programación  funcional
Sin  duda,  el  movimiento  de  la  programación  funcional  no  se  ha  detenido  antes  de  C++,  y  eso  es  básicamente  bueno.
Muchos  conceptos  útiles  se  han  incorporado  a  nuestro  lenguaje  de  programación  algo  antiguo.
Pero  el  código  que  está  escrito  en  un  estilo  funcional  no  es  automáticamente  un  código  bueno  o  limpio.  El  aumento
La  popularidad  de  los  lenguajes  de  programación  funcional  durante  los  últimos  años  podría  hacerle  creer  que  el  código  
funcional  es  per  se  mejor  mantenible,  mejor  legible,  mejor  comprobable  y  menos  propenso  a  errores  que,  por  ejemplo,  el  código  
orientado  a  objetos.  ¡Pero  eso  no  es  cierto!  Por  el  contrario,  el  código  funcional  ingeniosamente  elaborado  que  hace  cosas  no  
triviales  puede  ser  muy  difícil  de  entender.
Tomemos,  por  ejemplo,  una  operación  de  plegado  simple  que  es  muy  similar  a  uno  de  los  ejemplos  anteriores:

//  Crea  la  suma  de  todos  los  precios  de  los  productos  
const  Money  sum  =  std::accumulate(begin(productPrices),  end(productPrices),  0.0);

Si  leyera  esto  sin  el  comentario  explicativo  del  código  fuente...  ¿esta  intención  revela  el  código?
Recuerde  lo  que  hemos  aprendido  en  el  Capítulo  4  Acerca  de  los  comentarios:  siempre  que  sienta  la  necesidad  de  escribir  un  
comentario  sobre  el  código  fuente,  primero  debe  pensar  en  cómo  mejorar  el  código  para  que  el  comentario  se  vuelva  superfluo.

Entonces,  lo  que  realmente  queremos  leer  o  escribir  respectivamente  es  algo  como  esto:

const  Dinero  totalPrice  =  buildSumOfAllPrices(productPrices);

189
Machine Translated by Google

Capítulo  7  ■  Programación  funcional

Entonces,  primero  hagamos  una  declaración  fundamental:

¡Los  principios  de  un  buen  diseño  de  software  aún  se  aplican,  independientemente  del  estilo  de  programación  que  utilice!

¿Prefiere  el  estilo  de  programación  funcional  sobre  OO?  Está  bien,  pero  estoy  seguro  de  que  estarás  de  acuerdo  en  que  KISS,  DRY  y  
YAGNI  (ver  Capítulo  3)  también  son  muy  buenos  principios  en  la  programación  funcional.  ¿Cree  que  puede  ignorar  el  principio  de  responsabilidad  
única  (consulte  el  capítulo  6)  en  la  programación  funcional?  ¡Olvídalo!  Si  una  función  hace  más  de  una  cosa,  conducirá  a  problemas  similares  a  
los  de  la  orientación  a  objetos.  Y  creo  que  no  hace  falta  mencionar  ese  buen  y  expresivo  naming  (ver  Capítulo  4  sobre  buenos  nombres)  
también  es  enormemente  importante  para  la  comprensibilidad  y  mantenibilidad  del  código  en  un  entorno  funcional.

Siempre  tenga  en  cuenta  que  los  desarrolladores  pasan  mucho  más  tiempo  leyendo  código  que  escribiendo  código.
Por  lo  tanto,  podemos  concluir  que  la  mayoría  de  los  principios  de  diseño  utilizados  por  los  diseñadores  y  programadores  de  software  
orientado  a  objetos  también  pueden  ser  utilizados  por  programadores  funcionales.
Personalmente,  prefiero  una  combinación  equilibrada  de  ambos  estilos  de  programación.  Hay  muchos  desafíos  de  diseño  que  se  pueden  
resolver  perfectamente  utilizando  paradigmas  orientados  a  objetos.  El  polimorfismo  es  un  gran  beneficio  de  OO.  Puedo  aprovechar  el  Principio  
de  Inversión  de  Dependencias  (consulte  la  sección  homónima  en  el  Capítulo  6),  que  me  permite  invertir  las  dependencias  del  código  fuente  y  del  
tiempo  de  ejecución.
En  cambio,  los  cálculos  matemáticos  complejos  se  pueden  resolver  mejor  utilizando  una  programación  funcional.
estilo.  Y  si  se  deben  cumplir  altos  y  ambiciosos  requisitos  de  rendimiento  y  eficiencia,  lo  que  inevitablemente  requerirá  una  paralelización  
de  ciertas  tareas,  la  programación  funcional  puede  jugar  su  carta  de  triunfo.
Independientemente  de  si  prefiere  escribir  software  de  forma  orientada  a  objetos,  en  un  estilo  funcional  o
en  una  mezcla  apropiada  de  ambos,  siempre  debe  recordar  la  siguiente  cita:

Codifica  siempre  como  si  el  tipo  que  acabará  manteniendo  tu  código  fuera  un  psicópata  violento  que  sabe  dónde  vives.

—John  F.  Woods,  1991,  en  una  publicación  en  el  grupo  de  noticias  comp.lang.c++

190
Machine Translated by Google

CAPÍTULO  8

Desarrollo  basado  en  pruebas

El  Proyecto  Mercury  se  ejecutó  con  iteraciones  muy  cortas  (medio  día)  que  estaban  limitadas  en  el  
tiempo.  El  equipo  de  desarrollo  realizó  una  revisión  técnica  de  todos  los  cambios  y,  curiosamente,  
aplicó  la  práctica  de  programación  extrema  de  desarrollo  de  prueba  primero,  planificación  y  redacción  
de  pruebas  antes  de  cada  microincremento.

—Craig  Larman  y  Victor  R.  Basili,  Desarrollo  iterativo  e  incremental:  una  breve  historia.  
IEEE,  2003

En  la  sección  “Pruebas  unitarias” (ver  Capítulo  2)  hemos  aprendido  que  un  buen  conjunto  de  pruebas  pequeñas  y  rápidas  
puede  asegurar  que  nuestro  código  funcione  correctamente.  Hasta  ahora,  todo  bien.  Pero,  ¿qué  tiene  de  especial  el  desarrollo  
basado  en  pruebas  (TDD)  y  por  qué  justifica  un  capítulo  adicional  en  este  libro?
Especialmente  en  los  últimos  años,  la  disciplina  del  desarrollo  basado  en  pruebas  ha  ganado  popularidad.  TDD  se  ha  
convertido  en  un  ingrediente  importante  de  la  caja  de  herramientas  de  los  artesanos  de  software.  Eso  es  un  poco  sorprendente,  
porque  la  idea  básica  de  los  enfoques  Test  First  no  es  nada  nuevo.  El  Proyecto  Mercurio,  que  se  menciona  en  la  cita  anterior,  
fue  el  primer  programa  de  vuelo  espacial  tripulado  de  los  Estados  Unidos  y  se  llevó  a  cabo  bajo  la  dirección  de  la  NASA  desde  
1958  hasta  1963.  Aunque  lo  que  se  practicaba  hace  unos  50  años  como  un  enfoque  de  Prueba  Primero  ciertamente  es  no  es  
exactamente  el  tipo  de  TDD  como  lo  conocemos  hoy,  podemos  decir  que  la  idea  básica  estuvo  presente  bastante  temprano  en  
el  desarrollo  de  software  profesional.
Pero  luego  parece  que  este  enfoque  ha  caído  en  el  olvido  durante  décadas.  En  innumerables  proyectos  con  miles  de  
millones  de  líneas  de  código,  las  pruebas  se  pospusieron  al  final  del  proceso  de  desarrollo.  Las  consecuencias  a  veces  
devastadoras  de  este  desplazamiento  a  la  derecha  de  las  pruebas  importantes  en  los  cronogramas  del  proyecto  son  conocidas:  
si  el  tiempo  se  está  acortando  en  el  proyecto,  lo  primero  que  suele  abandonar  el  equipo  de  desarrollo  son  las  pruebas  importantes.

Con  la  creciente  popularidad  de  las  prácticas  ágiles  en  el  desarrollo  de  software  y  la  aparición  de  un  nuevo  método  
llamado  Programación  eXtreme  (XP)  a  principios  de  la  década  de  2000,  se  redescubrió  el  desarrollo  basado  en  pruebas.  
Kent  Beck  escribió  su  famoso  libro  Test­Driven  Development:  By  Example  [Beck02],  y  los  enfoques  Test  First  como  TDD  
experimentaron  un  renacimiento  y  se  convirtieron  en  herramientas  cada  vez  más  importantes  en  la  caja  de  herramientas  de  los  
creadores  de  software.
En  este  capítulo,  no  solo  explicaré  que,  aunque  el  término  "Prueba"  se  incluye  en  el  desarrollo  basado  en  
pruebas,  no  se  trata  principalmente  de  garantía  de  calidad.  TDD  ofrece  muchos  más  beneficios  que  una  simple  validación  de  la  
corrección  del  código.  Más  bien,  explicaré  las  diferencias  de  TDD  con  lo  que  a  veces  se  denomina  Prueba  unitaria  simple  
(POUT),  seguido  de  la  discusión  detallada  del  flujo  de  trabajo  de  TDD,  respaldado  por  un  ejemplo  práctico  detallado  que  muestra  
cómo  hacerlo  en  C++.

©  Stephan  Roth  2017   191
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0_8
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Los  inconvenientes  de  las  pruebas  unitarias  simples  (POUT)
Sin  duda,  como  hemos  visto  en  el  Capítulo  2  un  conjunto  de  pruebas  unitarias  es  básicamente  una  situación  mucho  mejor  que  
no  tener  ninguna  prueba.  Pero  en  muchos  proyectos,  las  pruebas  unitarias  se  escriben  de  alguna  manera  en  paralelo  a  la  
implementación  del  código  a  probar,  a  veces  incluso  completamente  después  de  la  finalización  del  módulo  a  desarrollar.  El  
diagrama  de  actividad  representado  en  la  Figura  8­1  visualiza  este  proceso.

Figura  8­1.  La  secuencia  típica  en  desarrollo  con  pruebas  unitarias  tradicionales

192
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Este  enfoque  generalizado  también  se  conoce  ocasionalmente  como  prueba  unitaria  simple  (POUT).
Básicamente,  POUT  significa  que  el  software  se  desarrollará  "Codificando  primero"  y  no  probando  primero;  por  ejemplo,  con  este  
enfoque,  las  pruebas  unitarias  se  escriben  siempre  después  de  que  se  haya  escrito  el  código  que  se  va  a  probar.  Y  para  muchos  
desarrolladores,  este  orden  parece  ser  la  única  secuencia  lógica.  Argumentan  que  para  probar  algo,  obviamente  la  cosa  a  probar  
necesita  haber  sido  construida  previamente.  Y  en  algunas  organizaciones  de  desarrollo,  este  enfoque  incluso  se  denomina  
erróneamente  como  "desarrollo  basado  en  pruebas",  lo  cual  es  totalmente  incorrecto.

Como  dije,  las  pruebas  unitarias  simples  son  mejores  que  ninguna  prueba  unitaria.  Sin  embargo,  este  enfoque  tiene  algunas  
desventajas:

•No  hay  obligación  de  escribir  las  pruebas  unitarias  después.  Una  vez  que  una  función  funciona
(...  o  parece  funcionar),  hay  poca  motivación  para  actualizar  el  código  con  pruebas  unitarias.  No  es  
divertido,  y  la  tentación  de  pasar  a  lo  siguiente  es  demasiado  grande  para  muchos  desarrolladores.

•El  código  resultante  puede  ser  difícil  de  probar.  A  menudo,  no  es  tan  fácil  adaptar  el  código  existente  con  
pruebas  unitarias,  porque  se  le  dio  poca  importancia  a  la  capacidad  de  prueba  del  código  que  se  originó.  
Esto  permitió  que  surgiera  un  código  fuertemente  acoplado.

• No  es  fácil  alcanzar  una  cobertura  de  prueba  bastante  alta  con  pruebas  unitarias  adaptadas.  La  escritura  
de  pruebas  unitarias  después  del  código  tiene  la  tendencia  de  que  se  escapen  algunos  problemas  
o  errores.

Desarrollo  basado  en  pruebas  como  factor  de  cambio
Test­Driven  Development  (TDD)  cambia  por  completo  el  desarrollo  tradicional.  Para  los  desarrolladores  que  aún  no  se  han  ocupado  
de  TDD,  este  enfoque  representa  un  cambio  de  paradigma.
Como  un  enfoque  llamado  Test  First  y  en  contraste  con  POUT,  TDD  no  permite  que  ninguna  producción
el  código  se  escribe  antes  de  que  se  haya  escrito  la  prueba  asociada.  En  otras  palabras:  TDD  significa  que  escribimos  la  prueba  
para  una  nueva  característica  o  función  siempre  antes  de  escribir  el  código  de  producción  correspondiente.  Esto  se  hace  estrictamente  
paso  a  paso:  después  de  cada  prueba  implementada,  se  escribe  solo  el  código  de  producción  suficiente  para  que  la  prueba  pase.  Y  
se  hace  siempre  y  cuando  aún  existan  requerimientos  no  realizados  para  el  módulo  a  desarrollar.

A  primera  vista,  parece  paradójico,  y  también  un  poco  absurdo,  escribir  una  prueba  unitaria  para  algo.
que  aún  no  existe.  ¿Cómo  puede  funcionar  esto?
No  te  preocupes,  funciona.  Después  de  haber  discutido  el  proceso  detrás  de  TDD  en  detalle  en  la  siguiente  sección,  todos
ojalá  se  eliminen  las  dudas.

El  flujo  de  trabajo  de  TDD
Al  realizar  el  desarrollo  basado  en  pruebas,  los  pasos  que  se  muestran  en  la  figura  8­2  se  ejecutan  repetidamente  hasta  que  se  
cumplen  todos  los  requisitos  conocidos  para  que  la  unidad  se  desarrolle.

193
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Figura  8­2.  El  flujo  de  trabajo  detallado  de  TDD  como  un  diagrama  de  actividad

En  primer  lugar,  es  notable  que  la  primera  acción  después  del  nodo  inicial  que  está  etiquetado  con  "INICIO"  es  que  el  
desarrollador  debe  pensar  en  lo  que  quiere  hacer.  Y  vemos  un  llamado  Pin  de  entrada  en  la  parte  superior  de  esta  acción  que  
acepta  "requisitos".  ¿A  qué  requisitos  se  refiere  aquí?
Bueno,  ante  todo  hay  requisitos  que  debe  cumplir  un  sistema  de  software.  esto  se  aplica
tanto  a  los  requisitos  de  los  stakeholders  del  negocio  en  el  nivel  superior  con  respecto  a  todo  el  sistema,  como  a  los  requisitos  
que  residen  en  los  niveles  de  abstracción  inferiores,  es  decir,  requisitos  para  componentes,  clases  y  funciones,  que  se  derivaron  
de  los  requisitos  de  los  stakeholders  del  negocio.  Con  TDD  y  su  enfoque  Test  First,  los  requisitos  se  establecen  firmemente  
mediante  pruebas  unitarias,  de  hecho,  antes  de  que  se  escriba  el  código  de  producción.  En  nuestro  caso  de  un  enfoque  de  
Prueba  Primero  para  el  desarrollo  de  unidades,  es  decir,  en  el  nivel  más  bajo  de  la  Pirámide  de  Prueba  (ver  Figura  2­1  en  el  Capítulo  
2),  por  supuesto,  los  requisitos  en  el  nivel  más  bajo  se  refieren  aquí.
A  continuación,  se  escribirá  una  prueba,  mediante  la  cual  se  diseñará  la  interfaz  pública  (API).  Esto  puede  resultar  
sorprendente,  porque  en  la  primera  ejecución  de  este  ciclo  todavía  no  hemos  escrito  ningún  código  de  producción.  Entonces,  
¿qué  interfaz  se  puede  diseñar  aquí  si  tenemos  un  papel  en  blanco?
Bueno,  la  respuesta  simple  es  esta:  ese  "papel  en  blanco"  es  exactamente  lo  que  queremos  completar  ahora,  pero  desde  
una  perspectiva  diferente  a  la  habitual.  Tomamos  ahora  la  perspectiva  de  un  futuro  cliente  externo  de  la  unidad  a  desarrollar.  
Usamos  una  pequeña  prueba  para  definir  cómo  queremos  usar  la  unidad  a  desarrollar.  En  otras  palabras,  este  es  el  paso  que  
debería  conducir  a  unidades  de  software  bien  comprobables  y,  por  lo  tanto,  también  bien  utilizables.

194
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Después  de  haber  escrito  las  líneas  apropiadas  en  la  prueba,  también  debemos,  por  supuesto,  satisfacer  al  compilador  y  proporcionar  la  
interfaz  solicitada  por  la  prueba.
Luego,  inmediatamente,  la  siguiente  sorpresa:  la  prueba  unitaria  recién  escrita  debe  fallar  (inicialmente).  ¿Por  qué?
Respuesta  simple:  tenemos  que  asegurarnos  de  que  la  prueba  pueda  fallar  en  absoluto.  Incluso  una  prueba  unitaria  
puede  implementarse  de  forma  incorrecta  y,  por  ejemplo,  pasar  siempre,  sin  importar  lo  que  estemos  haciendo  en  el  código  de  producción.  
Así  que  tenemos  que  asegurarnos  de  que  la  prueba  recién  escrita  esté  armada.
Ahora  estamos  llegando  al  clímax  de  este  pequeño  flujo  de  trabajo:  escribimos  solo  el  código  de  producción  suficiente,  ¡y  ni  una  sola  
línea  más!  –  ¡que  se  supere  la  nueva  prueba  unitaria  (…  y,  en  su  caso,  todas  las  pruebas  existentes  anteriormente)!  Y  es  muy  importante  
ser  disciplinado  en  este  punto  y  no  escribir  más  código  del  requerido  (recuerde  el  principio  KISS  del  Capítulo  3).  Depende  del  desarrollador  
decidir  qué  es  apropiado  aquí  en  cada  situación.  A  veces,  una  sola  línea  de  código,  o  incluso  una  sola  declaración,  es  suficiente;  en  otros  
casos,  debe  llamar  a  una  función  de  biblioteca.  Si  esto  último  es  el  caso,  ha  llegado  el  momento  de  pensar  en  cómo  integrar  y  usar  esta  
biblioteca,  y  especialmente  cómo  poder  reemplazarla  con  un  Test  Double  (ver  la  sección  sobre  Test  Doubles  (Mock  Objects)  en  el  Capítulo  2 ) .

Si  ahora  ejecutamos  las  pruebas  unitarias  y  lo  hemos  hecho  todo  bien,  las  pruebas  pasarán.
Ahora  hemos  llegado  a  un  punto  notable  en  el  proceso.  Si  las  pruebas  pasan  ahora,  siempre  tendremos  una  cobertura  de  prueba  
unitaria  del  100  %  en  este  paso.  ¡Siempre!  No  solo  100  %  en  el  sentido  de  una  métrica  de  cobertura  de  prueba  técnica,  como  cobertura  de  
funciones,  cobertura  de  sucursales  o  cobertura  de  extractos.  ¡No,  mucho  más  importante  es  que  tenemos  una  cobertura  de  prueba  unitaria  
del  100%  con  respecto  a  los  requisitos  que  ya  se  implementaron  en  este  punto!  Y  sí,  en  este  punto  posiblemente  aún  queden  algunos  o  
muchos  requisitos  no  implementados  para  la  unidad  a  desarrollar.
Esto  está  bien,  porque  pasaremos  por  el  ciclo  TDD  una  y  otra  vez  hasta  que  se  cumplan  todos  los  requisitos.  Pero  para  un  subconjunto  de  
requisitos  que  ya  se  cumplen  en  este  punto,  tenemos  una  cobertura  de  prueba  unitaria  del  100  %.
¡Este  hecho  nos  da  un  poder  tremendo!  Con  esta  red  de  seguridad  sin  interrupciones  de  pruebas  unitarias,  ahora  podemos  llevar  
a  cabo  refactorizaciones  sin  miedo.  Los  olores  de  código  (por  ejemplo,  código  duplicado)  o  los  problemas  de  diseño  se  pueden  solucionar  
ahora.  No  debemos  tener  miedo  de  romper  la  funcionalidad,  porque  las  pruebas  unitarias  ejecutadas  regularmente  nos  darán  una  respuesta  
inmediata  al  respecto.  Y  lo  agradable  es  esto:  si  una  o  más  pruebas  fallan  durante  la  fase  de  refactorización,  el  cambio  de  código  que  condujo  
a  esto  fue  muy  pequeño.
Después  de  que  se  haya  completado  la  refactorización,  ahora  podemos  implementar  otro  requisito  que  aún  no  se  ha
cumplido  al  continuar  el  ciclo  TDD.  Si  no  hay  más  requisitos,  estamos  listos.
La  Figura  8­2  muestra  el  ciclo  TDD  con  muchos  detalles.  Resumido  en  sus  tres  pasos  principales  esenciales  como
representado  en  la  Figura  8­3,  el  ciclo  TDD  a  menudo  se  denomina  "ROJO  ­  VERDE  ­  REFACTOR".

Figura  8­3.  El  flujo  de  trabajo  central  de  TDD
195
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

•  ROJO:  Escribimos  una  prueba  unitaria  fallida.

•  VERDE:  escribimos  suficiente  código  de  producción  para  que  la  nueva  prueba  y  todas  las  pruebas  escritas  
anteriormente  pasen.

•  REFACTOR:  Se  eliminan  la  duplicación  de  código  y  otros  olores  de  código,  tanto  del
código  de  producción,  así  como  de  las  pruebas  unitarias.

Los  términos  ROJO  y  VERDE  se  refieren  a  las  integraciones  típicas  de  Unit  Test  Framework  que  están  disponibles  para  un
variedad  de  IDE,  donde  las  pruebas  que  pasaron  se  muestran  en  verde  y  las  pruebas  que  fallaron  se  muestran  en  rojo.

LAS  TRES  REGLAS  DE  TDD  DEL  TÍO  BOB

En  su  gran  libro  The  Clean  Coder  [Martin11],  Robert  C.  Martin,  también  conocido  como  Uncle  Bob,  recomienda  que  sigamos  las  tres  reglas  de  TDD:

•  No  se  le  permite  escribir  ningún  código  de  producción  hasta  que  primero  haya  escrito  un  error
prueba  de  unidad.

•  No  se  le  permite  escribir  más  de  una  prueba  unitaria  de  lo  suficiente  para  fallar,  y  no

la  compilación  está  fallando.

•  No  está  permitido  escribir  más  código  de  producción  del  suficiente  para  pasar  el

actualmente  fallando  la  prueba  unitaria.

Martin  argumenta  que  el  cumplimiento  estricto  de  estas  tres  reglas  obliga  al  desarrollador  a  trabajar  en  ciclos  muy  cortos.  Como  resultado,  el  

desarrollador  nunca  estará  a  más  de  unos  segundos  o  solo  unos  minutos  de  una  situación  cómoda  en  la  que  el  código  era  correcto  y  todo  

funcionaba.

Suficiente  de  teoría,  ahora  explicaré  un  desarrollo  completo  de  una  pieza  de  software  usando  TDD  con  un  pequeño  ejemplo.

TDD  por  ejemplo:  el  código  de  números  romanos  Kata
La  idea  básica  de  lo  que  hoy  en  día  se  llama  Code  Kata  fue  descrita  por  primera  vez  por  Dave  Thomas,  uno  de  los  dos  autores  
del  notable  libro  The  Pragmatic  Programmer  [Hunt99].  Dave  era  de  la  opinión  de  que  los  desarrolladores  deberían  practicar  
repetidamente  en  una  base  de  código  pequeña,  no  relacionada  con  el  trabajo,  para  que  puedan  dominar  su  profesión  como  un  
músico.  Dijo  que  los  desarrolladores  deben  aprender  y  mejorar  constantemente,  y  para  ello  necesitan  sesiones  de  práctica  para  
aplicar  la  teoría  una  y  otra  vez,  utilizando  la  retroalimentación  para  mejorar  cada  vez.

Un  código  kata  es  un  pequeño  ejercicio  de  programación,  que  sirve  exactamente  para  este  propósito.  El  término  kata  se  
hereda  de  las  artes  marciales.  En  los  deportes  de  combate  del  Lejano  Oriente,  usan  katas  para  practicar  sus  movimientos  básicos  
una  y  otra  vez.  El  objetivo  es  llevar  el  curso  del  movimiento  a  la  perfección.
Este  tipo  de  práctica  se  transfirió  al  desarrollo  de  software.  Para  mejorar  sus  habilidades  de  programación,  los  desarrolladores  deben  
practicar  su  oficio  con  la  ayuda  de  pequeños  ejercicios.  Katas  se  convirtió  en  una  faceta  importante  del  movimiento  Software  Craftsmanship.  
Pueden  abordar  diferentes  habilidades  que  debe  tener  un  desarrollador,  por  ejemplo,  conocer  los  métodos  abreviados  de  teclado  del  IDE,  
aprender  un  nuevo  lenguaje  de  programación,  centrarse  en  ciertos  principios  de  diseño  o  practicar  TDD.  En  Internet  existen  varios  catálogos  
con  katas  adecuados  para  diferentes  propósitos,  por  ejemplo,  la  colección  de  Dave  Thomas  en  http://codekata.com.

Para  nuestros  primeros  pasos  con  TDD  usamos  un  código  kata  con  un  énfasis  algorítmico:  el  conocido  código  kata  de  números  
romanos.

196
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

TDD  KATA:  CONVERTIR  NÚMEROS  ÁRABES  A  NÚMEROS  ROMANOS

Los  romanos  escribieron  números  usando  letras.  Por  ejemplo,  escribieron  “V”  para  el  número  arábigo  5.

Su  tarea  es  desarrollar  una  pieza  de  código  utilizando  el  enfoque  de  desarrollo  basado  en  pruebas  (TDD)  que  traduce  los  números  
arábigos  entre  1  y  3999  en  su  respectiva  representación  romana.

Los  números  en  el  sistema  romano  se  representan  mediante  combinaciones  de  letras  del  alfabeto  latino.
Los  números  romanos,  tal  como  se  usan  hoy  en  día,  se  basan  en  siete  caracteres:

1     yo
5     V  
10     X
50     L  
100     C
500     D  
1000     M

Los  números  se  forman  combinando  caracteres  y  sumando  los  valores.  Por  ejemplo,  el  número  arábigo  12  está  representado  por  
“XII” (10  +  1  +  1).  Y  el  número  2017  es  “MMXVII”  en  su  equivalente  romano.

Las  excepciones  son  4,  9,  40,  90,  400  y  900.  Para  evitar  eso,  se  deben  repetir  cuatro  caracteres  iguales  en  sucesión,  el  
número  4,  por  ejemplo,  no  se  representa  con  “IIII”,  sino  con  “IV”.  Esto  se  conoce  como  notación  sustractiva,  es  decir,  el  
número  que  está  representado  por  el  carácter  anterior  I  se  resta  de  V  (5  ­  1  =  4).  Otro  ejemplo  es  "CM",  que  es  900  (1000  ­  100).

Por  cierto:  los  romanos  no  tenían  equivalente  para  el  0,  además  no  conocían  los  números  negativos.

Preparativos
Antes  de  que  podamos  escribir  nuestra  primera  prueba,  debemos  hacer  algunos  preparativos  y  configurar  el  entorno  
de  prueba.
Como  marco  de  prueba  de  unidad  para  este  kata,  uso  Google  Test  (https://github.com/google/googletest),  un  marco  de  
prueba  de  unidad  C++  independiente  de  la  plataforma  publicado  bajo  la  nueva  licencia  BSD.  Por  supuesto,  también  se  puede  usar  
cualquier  otro  marco  de  pruebas  unitarias  de  C++  para  este  kata.
También  se  recomienda  encarecidamente  utilizar  un  sistema  de  control  de  versiones.  Aparte  de  algunas  excepciones,  
realizaremos  un  compromiso  con  el  sistema  de  control  de  versiones  después  de  cada  transferencia  del  ciclo  TDD.  Esto  tiene  la  gran  
ventaja  de  que  somos  capaces  de  retroceder  y  hacer  retroceder  decisiones  posiblemente  equivocadas.
Además,  tenemos  que  pensar  en  cómo  se  van  a  organizar  los  archivos  de  código  fuente.  Mi  sugerencia
ya  que  este  kata  debe  comenzar  inicialmente  con  un  solo  archivo,  el  archivo  que  ocupará  todas  las  pruebas  
unitarias  futuras:  ArabicToRomanNumeralsConverterTestCase.cpp.  Dado  que  TDD  nos  guía  de  manera  incremental  a  
través  del  proceso  de  formación  de  una  unidad  de  software,  es  posible  decidir  más  tarde  si  se  requieren  archivos  adicionales.
Para  una  verificación  de  función  fundamental,  escribimos  una  función  principal  que  inicializa  Google  Test  y  ejecuta  todas  
las  pruebas,  y  escribimos  una  prueba  unitaria  simple  (llamada  Preparaciones  Completadas)  que  siempre  falla  intencionalmente,  
como  se  muestra  en  el  siguiente  ejemplo  de  código.

197
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Listado  8­1.  El  contenido  inicial  de  ArabicToRomanNumeralsConverterTestCase.cpp

#incluir  <gtest/gtest.h>

int  main(int  argc,  char**  argv)  {
pruebas::InitGoogleTest(&argc,  argv);  devolver  
EJECUTAR_TODAS_PRUEBAS(); }

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  preparaciones  completadas)  {
GTEST_FAIL(); }

Después  de  compilar  y  vincular,  ejecutamos  el  archivo  binario  resultante  para  ejecutar  la  prueba.  La  salida  de  nuestro  
pequeño  programa  en  la  salida  estándar  (stdout)  debería  ser  la  siguiente:

Listado  8­2.  El  resultado  de  la  prueba  de  funcionamiento

[==========]  Ejecutando  1  prueba  de  1  caso  de  prueba.
[­­­­­­­­­­]  Configuración  del  entorno  de  prueba  global.  [­­­­­­­­­­]  
1  prueba  de  ArabicToRomanNumeralsConverterTestCase  [EJECUTAR]  
ArabicToRomanNumeralsConverterTestCase.PreparationsCompleted ../  
ArabicToRomanNumeralsConverterTestCase.cpp:9:  Error  fallido  [FALLO]  

ArabicToRomanNumeralsConverterTestCase.PreparationsCompleted  (0  ms)  [­­­­­­  ­­­­]  1  prueba  de  
ArabicToRomanNumeralsConverterTestCase  (2  ms  en  total)

[­­­­­­­­­­]  Desmontaje  del  entorno  de  prueba  global  [==========]  
Se  ejecutó  1  prueba  de  1  caso  de  prueba.  (16  ms  en  total)
[PASADO]  0  pruebas.
[FALLIDO]  1  prueba,  enumerada  a  continuación:  
[FALLIDO]  ArabicToRomanNumeralsConverterTestCase.PreparationsCompleted

1  PRUEBA  FALLIDA

Como  era  de  esperar,  la  prueba  falla.  La  salida  en  stdout  es  bastante  útil  para  imaginar  qué  salió  mal.  Especifica  
el  nombre  de  las  pruebas  fallidas,  el  nombre  del  archivo,  el  número  de  línea  y  la  razón  por  la  que  falló  la  prueba.  En  este  caso,  
es  una  falla  impuesta  por  una  macro  especial  de  prueba  de  Google.
Si  ahora  intercambiamos  la  macro  GTEST_FAIL()  con  la  macro  GTEST_SUCCEED()  dentro  de  la  prueba,  después  de  un
recompilación  la  prueba  debe  pasar:

Listado  8­3.  El  resultado  de  la  ejecución  de  prueba  exitosa

[==========]  Ejecutando  1  prueba  de  1  caso  de  prueba.
[­­­­­­­­­­]  Configuración  del  entorno  de  prueba  global.  [­­­­­­­­­­]  
1  prueba  de  ArabicToRomanNumeralsConverterTestCase  [ EJECUTAR  [ [­­­­­­­­­­]  1  
prueba   ]  ArabicToRomanNumeralsConverterTestCase.PreparationsCompleted  OK ]  
ArabicToRomanNumeralsConverterTestCase.PreparationsCompleted  (0  ms)
de  ArabicToRomanNumeralsConverterTestCase  (0  ms  en  total)

[­­­­­­­­­­]  Desmontaje  del  entorno  de  prueba  global  [==========]  
Se  ejecutó  1  prueba  de  1  caso  de  prueba.  (4  ms  en  total)
[PASADO]  1  prueba.

198
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Eso  es  bueno,  porque  ahora  sabemos  que  todo  está  preparado  correctamente  y  podemos  comenzar  con  nuestro  kata.

la  primera  prueba

El  primer  paso  es  decidir  qué  primer  pequeño  requisito  queremos  implementar.  Luego  escribiremos  una  prueba  fallida  para  
ello.  Para  nuestro  ejemplo,  hemos  decidido  comenzar  convirtiendo  un  solo  número  arábigo  en  un  número  romano:  
queremos  convertir  el  número  arábigo  1  en  una  "I".
Por  lo  tanto,  tomamos  la  prueba  ficticia  ya  existente  y  la  convertimos  en  una  prueba  unitaria  real,  que  puede  probar  el  
cumplimiento  de  este  pequeño  requisito.  Por  lo  tanto,  también  debemos  considerar  cómo  debería  ser  la  interfaz  para  la  función  
de  conversión.

Listado  8­4.  La  primera  prueba  (se  omitieron  partes  irrelevantes  del  código  fuente)

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  1_esConvertedTo_I)  {
ASSERT_EQ("I",  convertNumberArabicToRomanNumeral(1)); }

Como  puede  ver,  nos  hemos  decidido  por  una  función  simple  que  toma  un  número  arábigo  como  parámetro  y  tiene  una  
cadena  como  valor  de  retorno.
Pero  el  código  no  se  puede  compilar  sin  errores  de  compilación,  porque  la  función  
convertArabicNumberToRomanNumeral()  aún  no  existe.  Recordemos  la  segunda  de  las  tres  reglas  de  TDD  del  tío  Bob:  "No  
se  le  permite  escribir  más  de  una  prueba  unitaria  de  lo  suficiente  para  fallar,  y  no  compilar  es  fallar".

Eso  significa  que  ahora  tenemos  que  dejar  de  escribir  código  de  prueba  para  escribir  suficiente  código  de  producción  
para  que  pueda  compilarse  sin  errores.  Así  que  vamos  a  crear  la  función  de  conversión  ahora,  e  incluso  escribiremos  esa  
función  directamente  en  el  archivo  de  código  fuente,  que  también  contiene  la  prueba.  Por  supuesto,  somos  conscientes  de  
que  no  puede  quedar  así.

Listado  8­5.  El  stub  de  la  función  satisface  al  compilador

#include  <gtest/gtest.h>  #include  
<cadena>

int  main(int  argc,  char**  argv)  {
pruebas::InitGoogleTest(&argc,  argv);  devolver  
EJECUTAR_TODAS_PRUEBAS(); }

std::string  convertArabicNumberToRomanNumeral(const  unsigned  int  arabicNumber)  {
devolver  "";
}

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  1_esConvertedTo_I)  {
ASSERT_EQ("I",  convertNumberArabicToRomanNumeral(1)); }

Ahora  el  código  se  puede  compilar  de  nuevo  sin  errores.  Y  por  el  momento  la  función  devuelve  solo  una  cadena  vacía.

Además,  ahora  tenemos  nuestra  primera  prueba  ejecutable,  que  debe  fallar  (ROJO),  porque  la  prueba  espera  una  "I",  pero  
la  función  devuelve  una  cadena  vacía:

199
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Listado  8­6.  El  resultado  de  Google  Test  después  de  ejecutar  la  prueba  unitaria  que  falló  deliberadamente  (RED)

[==========]  Ejecutando  1  prueba  de  1  caso  de  prueba.
[­­­­­­­­­­]  Configuración  del  entorno  de  prueba  global.  [­­­­­­­­­­]  
1  prueba  de  ArabicToRomanNumeralsConverterTestCase  [ EJECUTAR ../
]  ArabicToRomanNumeralsConverterTestCase.1_isConvertedTo_I
ArabicToRomanNumeralsConverterTestCase.cpp:14:  Valor  de  error  de:  
convertArabicNumberToRomanNumeral(1)
""
Real:  
Esperado:  "Yo"
[FALLIDO]  ArabicToRomanNumeralsConverterTestCase.1_isConvertedTo_I  (0  ms)  [­­­­­­­­­­]  1  prueba  de  
ArabicToRomanNumeralsConverterTestCase  (0  ms  en  total)

[­­­­­­­­­­]  Desmontaje  del  entorno  de  prueba  global  [==========]  
Se  ejecutó  1  prueba  de  1  caso  de  prueba.  (6  ms  en  total)
[PASADO]  0  pruebas.
[FALLIDO]  1  prueba,  enumerada  a  continuación:  
[FALLIDO]  ArabicToRomanNumeralsConverterTestCase.1_isConvertedTo_I

1  PRUEBA  FALLIDA

Bien,  eso  es  lo  que  esperábamos.

■  Nota  Según  la  versión  de  Google  Test  utilizada,  el  resultado  del  marco  de  prueba  puede  ser  ligeramente  
diferente  al  que  se  muestra  aquí.

Ahora  necesitamos  cambiar  la  implementación  de  la  función  convertArabicNumberToRomanNumeral()  para  que  pase  la  
prueba.  La  regla  es  esta:  haz  lo  más  simple  que  pueda  funcionar.  ¿Y  qué  podría  ser  más  fácil  que  devolver  una  "I"  de  la  función?

Listado  8­7.  La  función  modificada  (se  omitieron  partes  irrelevantes  del  código  fuente)

std::string  convertArabicNumberToRomanNumeral(const  unsigned  int  arabicNumber)  {
devuelve  "yo"; }

Probablemente  dirás:  “¡Espera  un  minuto!  Eso  no  es  un  algoritmo  para  convertir  números  arábigos  en  su
equivalentes  romanos.  ¡Eso  es  hacer  trampa!"
Por  supuesto,  el  algoritmo  aún  no  está  listo.  Tienes  que  cambiar  de  opinión.  Las  reglas  de  TDD  establecen  que  debemos  
escribir  el  código  más  simple  que  pase  la  prueba  actual.  Es  un  proceso  incremental,  y  estamos  apenas  al  principio.

[==========]  Ejecutando  1  prueba  de  1  caso  de  prueba.
[­­­­­­­­­­]  Configuración  del  entorno  de  prueba  global.  [­­­­­­­­­­]  
1  prueba  de  ArabicToRomanNumeralsConverterTestCase  [ EJECUTAR ]  
ArabicToRomanNumeralsConverterTestCase.1_isConvertedTo_I  [ OK ]  
ArabicToRomanNumeralsConverterTestCase.1_isConvertedTo_I  (0  ms)  [­­­­­­­­­­]  1  prueba  de  
ArabicToRomanNumeralsConverterTestCase  (0  ms  total)

[­­­­­­­­­­]  Desmontaje  del  entorno  de  prueba  global  [==========]  
Se  ejecutó  1  prueba  de  1  caso  de  prueba.  (1  ms  en  total)
[PASADO]  1  prueba.

200
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

¡Excelente!  La  prueba  pasó  (VERDE)  y  podemos  ir  al  paso  de  refactorización.  En  realidad,  todavía  no  es  necesario  refactorizar  algo,  por  
lo  que  podemos  continuar  con  la  siguiente  ejecución  del  ciclo  TDD.  Pero  primero  tenemos  que  confirmar  nuestros  cambios  en  el  repositorio  del  
código  fuente.

La  segunda  prueba
Para  nuestra  segunda  prueba  unitaria,  tomaremos  un  2,  que  debe  convertirse  en  "II".

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  2_isConvertedTo_II)  {
ASSERT_EQ("II",  convertNumberArabicToRomanNumeral(2)); }

Como  era  de  esperar,  esta  prueba  debe  fallar  (RED),  porque  nuestra  función  convertArabicNumberToRomanNumeral()
devuelve  siempre  un  "I".  Después  de  haber  verificado  que  la  prueba  falla,  complementamos  la  implementación  para  que  la  prueba  pueda  pasar.  
Una  vez  más,  hacemos  lo  más  simple  que  podría  funcionar.

Listado  8­8.  Agregamos  algo  de  código  para  pasar  la  nueva  prueba

std::string  convertArabicNumberToRomanNumeral(const  unsigned  int  arabicNumber)  { if  (arabicNumber  ==  2)  { return  "II"; }  
devuelve  "yo"; }

Ambas  pruebas  pasan  (VERDE).
¿Deberíamos  refactorizar  algo  ahora?  Tal  vez  todavía  no,  pero  es  posible  que  tenga  la  sospecha  de  que  lo  haremos.
necesita  una  refactorización  pronto.  De  momento  seguimos  con  nuestra  tercera  prueba…

La  tercera  prueba  y  el  orden  posterior
Como  era  de  esperar,  nuestra  tercera  prueba  evaluará  la  conversión  del  número  3:

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  3_isConvertedTo_III)  {
ASSERT_EQ("III",  convertNumberArabicToRomanNumeral(3)); }

Por  supuesto,  esta  prueba  fallará  (RED).  El  código  para  pasar  esta  prueba,  y  todas  las  pruebas  anteriores  (VERDE),  tiene  el  siguiente  
aspecto:

std::string  convertArabicNumberToRomanNumeral(const  unsigned  int  arabicNumber)  { if  (arabicNumber  ==  3)  { return  "III"; }  if  
(número  arábigo  ==  2)  { return  "II"; }  
devuelve  "yo"; }

201
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

El  mal  presentimiento  sobre  el  diseño  emergente,  que  ya  tuvimos  en  la  segunda  prueba,  no  fue  infundado.  Al  menos  ahora  
deberíamos  estar  completamente  insatisfechos  con  la  obvia  duplicación  de  código.  Es  bastante  evidente  que  no  podemos  continuar  por  
este  camino.  Una  secuencia  interminable  de  sentencias  if  no  puede  ser  una  solución,  porque  terminaremos  con  un  diseño  horrible.  
Es  hora  de  refactorizar,  y  podemos  hacerlo  sin  miedo,  porque  la  cobertura  de  prueba  unitaria  del  100  %  crea  una  cómoda  
sensación  de  seguridad.
Si  echamos  un  vistazo  al  código  dentro  de  la  función  convertArabicNumberToRomanNumeral(),  se  puede  reconocer  un  
patrón.  El  número  arábigo  es  como  un  contador  de  los  caracteres  I  de  su  equivalente  romano.  En  otras  palabras:  siempre  que  el  
número  a  convertir  se  pueda  disminuir  en  1  antes  de  que  llegue  a  0,  se  agrega  una  "I"  a  la  cadena  de  números  romanos.

Bueno,  esto  se  puede  hacer  de  una  manera  elegante  usando  un  ciclo  while  y  una  concatenación  de  cadenas,  como  esta:

Listado  8­9.  La  función  de  conversión  después  de  la  refactorización

std::string  convertArabicNumberToRomanNumeral(unsigned  int  arabicNumber)  {
std::string  númeroromano;  while  
(número  arábigo  >=  1)  { númeroromano  
+=  "I";  número  arábigo­­; }  
return  númeroromano; }

Eso  se  ve  bastante  bien.  Eliminamos  la  duplicación  de  código  y  encontramos  una  solución  compacta.  También  tuvimos  que  eliminar  
la  declaración  const  del  parámetro  arabicNumber  porque  tenemos  que  manipular  el  número  arábigo  en  la  función.  Y  aún  se  superan  las  
tres  pruebas  unitarias  existentes.
Podemos  pasar  a  la  siguiente  prueba.  Por  supuesto,  también  puede  continuar  con  el  5,  pero  me  decidí  por  "10­is­X".
Tengo  la  esperanza  de  que  el  grupo  de  diez  revele  un  patrón  similar  al  1,  2  y  3.  El  número  arábigo  5,  por  supuesto,  será  tratado  más  
adelante.

Listado  8­10.  La  prueba  de  la  cuarta  unidad.

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  10_isConvertedTo_X)  {
ASSERT_EQ("X",  convertNumberArabicToRomanNumeral(10)); }

Bueno,  no  debería  sorprender  a  nadie  que  esta  prueba  falle  (RED).  Esto  es  lo  que  Google  Test  escribe  en  stdout  sobre  esta  
nueva  prueba:

]  ArabicToRomanNumeralsConverterTestCase.10_isConvertedTo_X
[ EJECUTAR ../ArabicToRomanNumeralsConverterTestCase.cpp:31:  Valor  de  error  
de:  convertArabicNumberToRomanNumeral(10)
Real:  "IIIIIIIIII"
Esperado:  "X"
[FALLIDO]  ArabicToRomanNumeralsConverterTestCase.10_isConvertedTo_X  (0  ms)

La  prueba  falla  porque  10  no  es  "IIIIIIIIII",  sino  "X".  Sin  embargo,  si  vemos  la  salida  de  Google  Test,  podríamos  hacernos  una  
idea.  ¿Quizás  el  mismo  enfoque  que  hemos  usado  para  los  números  arábigos  1,  2  y  3  podría  usarse  también  para  10,  20  y  30?

¡DETENER!  Bueno,  eso  es  imaginable,  pero  aún  no  deberíamos  crear  algo  para  el  futuro  sin  pruebas  unitarias.
que  nos  llevan  a  tal  solución.  Ya  no  trabajaríamos  basados  en  pruebas  si  implementamos  el  código  de  producción  para  20  y  30  de  
una  sola  vez  con  el  código  para  10.  Entonces,  hacemos  nuevamente  lo  más  simple  que  podría  funcionar.

202
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Listado  8­11.  La  función  de  conversión  ahora  también  puede  convertir  10

std::string  convertArabicNumberToRomanNumeral(unsigned  int  arabicNumber)  {
if  (número  arábigo  ==  10)  { return  
"X"; }  más  
{ std::string  
romanNumeral;  while  (número  
arábigo  >=  1)  { númeroromano  +=  "I";  
número  arábigo­­; }  return  
númeroromano; } }

OK,  la  prueba  y  todas  las  pruebas  anteriores  han  sido  aprobadas  (VERDE).  Podemos  agregar  paso  a  paso  una  prueba  
para  el  número  arábigo  20  y  luego  para  el  30.  Después  de  ejecutar  el  ciclo  TDD  para  ambos  casos,  nuestra  función  de  conversión  se  ve  
de  la  siguiente  manera:

Listado  8­12.  El  resultado  durante  el  sexto  ciclo  TDD  antes  de  la  refactorización

std::string  convertArabicNumberToRomanNumeral(unsigned  int  arabicNumber)  {
if  (número  arábigo  ==  10)  { return  
"X"; }  else  if  
(número  arábigo  ==  20)  { return  "XX"; }  else  if  
(número  arábigo  
==  30)  { return  "XXX"; }  más  { std::string  
romanNumeral;  
while  
(número  arábigo  >=  1)  { númeroromano  
+=  "I";  número  arábigo­­;

}  return  númeroromano;
}
}

Al  menos  ahora  se  requiere  urgentemente  una  refactorización.  El  código  surgido  tiene  algunos  malos  olores,  como  algunas  
redundancias  y  una  alta  complejidad  ciclomática.  Sin  embargo,  también  se  ha  confirmado  nuestra  sospecha  de  que  el  procesamiento  
de  los  números  10,  20  y  30  sigue  un  patrón  similar  al  procesamiento  de  los  números  1,  2  y  3.
Vamos  a  intentarlo:

Listado  8­13.  Después  de  la  refactorización,  todas  las  decisiones  if­else  desaparecen

std::string  convertArabicNumberToRomanNumeral(unsigned  int  arabicNumber)  {
std::string  númeroromano;  while  
(número  arábigo  >=  10)  { númeroromano  
+=  "X";  numero  arabe  ­=  10; }  
while  (número  arábigo  >=  1)  
{

203
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

númeroromano  +=  "yo";  número  
arábigo­­;
}  
return  númeroromano;
}

Excelente,  todas  las  pruebas  pasaron  de  inmediato!  Parece  que  vamos  por  el  buen  camino.
Sin  embargo,  debemos  tener  en  mente  el  objetivo  del  paso  de  refactorización  en  el  ciclo  TDD.  Más  arriba  en  esta  sección  se  puede  leer  
lo  siguiente:  Se  elimina  la  duplicación  de  código  y  otros  olores  de  código,  tanto  del  código  de  producción  como  de  las  pruebas  unitarias.

Deberíamos  echar  un  vistazo  crítico  a  nuestro  código  de  prueba.  Actualmente  se  ve  así:

Listado  8­14.  Las  pruebas  unitarias  surgidas  tienen  muchas  duplicaciones  de  código.

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  1_esConvertedTo_I)  {
ASSERT_EQ("I",  convertNumberArabicToRomanNumeral(1)); }

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  2_isConvertedTo_II)  {
ASSERT_EQ("II",  convertNumberArabicToRomanNumeral(2)); }

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  3_isConvertedTo_III)  {
ASSERT_EQ("III",  convertNumberArabicToRomanNumeral(3)); }

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  10_isConvertedTo_X)  {
ASSERT_EQ("X",  convertNumberArabicToRomanNumeral(10)); }

PRUEBA  (caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  20_isConvertedTo_XX)  {
ASSERT_EQ("XX",  convertNumberArabicToRomanNumeral(20)); }

PRUEBA  (caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  30_isConvertedTo_XXX)  {
ASSERT_EQ("XXX",  convertNumberArabicToRomanNumeral(30)); }

Recuerde  lo  que  escribí  sobre  la  calidad  del  código  de  prueba  en  el  Capítulo  2:  la  calidad  del  código  de  prueba  debe  ser
tan  alto  como  la  calidad  del  código  de  producción.  En  otras  palabras,  nuestras  pruebas  deben  refactorizarse  porque  contienen  muchas  
duplicaciones  y  deben  diseñarse  de  manera  más  elegante.  Además,  queremos  aumentar  su  legibilidad  y  mantenibilidad.  ¿Pero  que  podemos  
hacer?
Eche  un  vistazo  a  las  seis  pruebas  anteriores.  La  verificación  en  las  pruebas  es  siempre  la  misma  y  se  podría  leer  más
generalmente  como:  "Afirmar  que  el  número  arábigo  <x>  se  convierte  al  número  romano  <cadena>".
Una  solución  podría  ser  proporcionar  una  aserción  dedicada  (también  conocida  como  aserción  personalizada  o  aserción  personalizada) .
matcher)  para  ese  propósito,  que  se  puede  leer  de  la  misma  manera  que  la  oración  anterior:

afirmar  que(x).isConvertedToRomanNumeral("cadena");

204
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Pruebas  más  sofisticadas  con  una  afirmación  personalizada
Para  implementar  nuestra  aserción  personalizada,  primero  escribimos  una  prueba  unitaria  que  falla,  pero  diferente  a  las  pruebas  unitarias  
que  hemos  escrito  antes:

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  33_isConvertedTo_XXXIII)  
{afirmar  que  (33).isConvertedToRomanNumeral  ("XXXII"); }

La  probabilidad  es  muy  alta  de  que  la  conversión  de  33  ya  funcione.  Por  lo  tanto,  forzamos  la  prueba  a  fallar  (RED)  al  especificar  
un  resultado  incorrecto  intencional  como  el  valor  esperado  ("XXXII").  Pero  esta  nueva  prueba  también  falla  debido  a  otra  razón:  el  compilador  
no  puede  compilar  la  prueba  unitaria  sin  errores.  Una  función  llamada  afirmar  que  aún  no  existe,  igualmente  no  hay  
isConvertedToRomanNumeral.  Recuerda  siempre  a  Robert  C.
La  segunda  regla  de  TDD  de  Martin  (ver  arriba):  "No  se  le  permite  escribir  más  de  una  prueba  unitaria  de  lo  suficiente  para  fallar,  y  no  compilar  
es  fallar".
Entonces,  primero  debemos  satisfacer  al  compilador  escribiendo  la  aserción  personalizada.  Este  constará  de  dos  partes:

•  Una  función  libre  de  afirmación  de  eso  (<parámetro>),  que  devuelve  una  instancia  de  un
clase  de  afirmación.

•La  clase  de  aserción  personalizada  que  contiene  el  método  de  aserción  real,  verificando  una  o  varias  propiedades  
del  objeto  ensayado.

Listado  8­15.  Una  afirmación  personalizada  para  números  romanos

clase  RomanNumeralAssert  { público:

RomanNumeralAssert()  =  eliminar;  explícito  
RomanNumeralAssert  (const  unsigned  int  arabicNumber):
NúmeroArabeParaConvertir(NúmeroArábigo)  { }
void  isConvertedToRomanNumeral(const  std::string&  ExpectedRomanNumeral)  const  {
ASSERT_EQ(número  romano  esperado,  convertir  número  arábigo  en  número  romano  (número  arábigo  en  conversión)); }

privado:  
const  unsigned  int  arabicNumberToConvert; };

RomanNumeralAssert  afirmar  que  (const  unsigned  int  arabicNumber)  {
RomanNumeralAssert  afirmar  { arabicNumber };  volver  afirmar; }

■  Nota  En  lugar  de  una  función  libre  assertThat,  también  se  puede  utilizar  un  método  de  clase  público  y  estático  en  
la  clase  de  aserción.  Esto  puede  ser  necesario  cuando  enfrenta  violaciones  de  espacio  de  nombres,  por  ejemplo,  
conflictos  de  nombres  de  funciones  idénticos.  Por  supuesto,  el  nombre  del  espacio  de  nombres  debe  anteponerse  
al  usar  el  método  de  clase:  RomanNumeralAssert::assertThat(33).isConvertedToRomanNumeral("XXXIII");

205
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Ahora  el  código  se  puede  compilar  sin  errores,  pero  la  nueva  prueba  fallará  como  se  esperaba  durante  la  ejecución.

Listado  8­16.  Un  extracto  de  la  salida  de  Google­Test  en  stdout

[ EJECUTAR ]  ArabicToRomanNumeralsConverterTestCase.33_isConvertedTo_XXXIII ../
ArabicToRomanNumeralsConverterTestCase.cpp:30:  Valor  de  error  de:  
convertArabicNumberToRomanNumeral(arabicNumberToConvert)
Real:  "XXXIII"
Esperado:  número  romano  esperado  que  es:  
"XXXII"
[FALLIDO]  ArabicToRomanNumeralsConverterTestCase.33_isConvertedTo_XXXIII  (0  ms)

Entonces  necesitamos  modificar  la  prueba  y  corregir  el  número  romano  que  esperamos  como  resultado.

Listado  8­17.  Nuestro  Custom  Asserter  permite  una  ortografía  más  compacta  del  código  de  prueba

PRUEBA  (Caso  de  prueba  del  convertidor  de  números  árabes  a  números  romanos,  33_isConvertedTo_XXXIII)  
{afirmar  que  (33).isConvertedToRomanNumeral  ("XXXIII"); }

Ahora  podemos  resumir  todas  las  pruebas  anteriores  en  una  sola.

Listado  8­18.  Todos  los  controles  se  pueden  agrupar  elegantemente  en  una  función  de  prueba

TEST(ArabicToRomanNumeralsConverterTestCase,  conversionOfArabicNumeralsToRomanNumerals_Works)  

{ assertThat(1).isConvertedToRomanNumeral("I");  afirmar  que  (2).  se  
convierte  en  número  romano  ("II");  afirmar  que  (3).  se  convierte  en  
número  romano  ("III");  afirmar  que  (10).  se  convierte  en  número  romano  
("X");  afirmar  que  (20).  se  convierte  en  número  romano  ("XX");  afirmar  
que  (30).  se  convierte  en  número  romano  ("XXX");  afirmar  que  (33).  se  
convierte  en  número  romano  ("XXXIII"); }

Eche  un  vistazo  a  nuestro  código  de  prueba  ahora:  libre  de  redundancia,  limpio  y  fácil  de  leer.  La  franqueza  de  nuestra  afirmación  
hecha  por  nosotros  mismos  es  bastante  elegante.  Y  es  deslumbrantemente  fácil  agregar  más  pruebas  ahora,  porque  solo  tenemos  que  
escribir  una  sola  línea  de  código  para  cada  nueva  prueba.
Puede  quejarse  de  que  esta  refactorización  también  tiene  una  pequeña  desventaja.  El  nombre  del  método  de  prueba  ahora  es  
menos  específico  que  el  nombre  de  todos  los  métodos  de  prueba  antes  de  la  refactorización  (consulte  la  sección  Nombres  de  pruebas  
unitarias  en  el  Capítulo  2).  ¿Podemos  tolerar  estos  pequeños  inconvenientes?  Creo  que  sí.  Hemos  hecho  un  compromiso  aquí:  esta  
pequeña  desventaja  se  compensa  con  los  beneficios  en  términos  de  mantenibilidad  y  extensibilidad  de  nuestras  pruebas.
Ahora  podemos  continuar  con  el  ciclo  TDD  e  implementar  el  código  de  producción  sucesivamente  para  las  siguientes  tres  pruebas:

afirmar  que  (100).  se  convierte  en  número  romano  ("C");  afirmar  que  
(200).  se  convierte  en  número  romano  ("CC");  afirmar  que  (300).  se  
convierte  en  número  romano  ("CCC");

206
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Después  de  tres  iteraciones,  el  código  se  verá  así  antes  del  paso  de  refactorización:

Listado  8­19.  Nuestra  función  de  conversión  en  el  noveno  ciclo  TDD  antes  de  la  refactorización

std::string  convertArabicNumberToRomanNumeral(unsigned  int  arabicNumber)  {
std::string  númeroromano;  if  
(número  arábigo  ==  100)  
{ númeroromano  =  "C"; }  
else  if  (número  arábigo  ==  200)  { númeroromano  
=  "CC"; }  else  if  (número  
arábigo  ==  300)  { númeroromano  =  "CCC"; }  
else  { while  (número  arábigo  
>=  10)  
{ númeroromano  +=  "X";  numero  arabe  
­=  10; }  while  (número  
arábigo  >=  1)  

{ númeroromano  +=  "I";  número  
arábigo­­; } }  return  
númeroromano; }

Y  de  nuevo  surge  el  mismo  patrón  que  antes  con  1,  2,  3;  y  10,  20  y  30.  También  podemos  usar  un  ciclo  similar  para  las  
centenas:

Listado  8­20.  El  patrón  emergente,  así  como  qué  partes  del  código  son  variables  y  cuáles  son  idénticas,  es  claramente  reconocible

std::string  convertArabicNumberToRomanNumeral(unsigned  int  arabicNumber)  {
std::string  númeroromano;  while  
(número  arábigo  >=  100)  { númeroromano  
+=  "C";  numero  arabe  ­=  
100; }  while  (número  arábigo  

>=  10)  { númeroromano  +=  "X";  numero  
arabe  ­=  10; }  while  (número  
arábigo  >=  1)  

{ númeroromano  +=  "I";  número  
arábigo­­; }  return  
númeroromano; }

Es  hora  de  limpiar  de  nuevo
En  este  punto  deberíamos  volver  a  echar  un  vistazo  crítico  a  nuestro  código.  Si  continuamos  así,  el  código  contendrá  muchas  
duplicaciones  de  código,  porque  las  tres  declaraciones  while  se  ven  muy  similares.  Sin  embargo,  podemos  aprovechar  estas  
similitudes  abstrayendo  las  partes  del  código  que  son  iguales  en  los  tres  bucles  while.

207
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

¡Es  tiempo  de  refactorización!  Las  únicas  partes  del  código  que  son  diferentes  en  los  tres  bucles  while  son  el  número  arábigo
y  su  correspondiente  número  romano.  La  idea  es  separar  estas  partes  variables  del  resto  del  ciclo.
En  un  primer  paso,  presentamos  una  estructura  que  asigna  números  arábigos  a  su  equivalente  romano.  Además,  
necesitamos  una  matriz  (aquí  usaremos  std::array  de  la  biblioteca  estándar  de  C++)  de  esa  estructura.  Inicialmente,  solo  agregaremos  
un  elemento  a  la  matriz  que  asigna  la  letra  "C"  al  número  100.

Listado  8­21.  Presentamos  una  matriz  que  contiene  asignaciones  entre  números  arábigos  y  su  equivalente  romano

struct  ArabicToRomanMapping  { unsigned  
int  arabicNumber;  std::string  
númeroromano; };

const  std::size_t  numberOfMappings  =  1;  usando  
ArabicToRomanMappings  =  std::array<ArabicToRomanMapping,  numberOfMappings>;

const  ArabicToRomanMappings  arabicToRomanMappings  =  {
{ 100,  "C" } };

Después  de  estos  preparativos,  modificamos  el  primer  ciclo  while  en  la  función  de  conversión  para  verificar  si  la  base
idea  funcionará.

Listado  8­22.  Reemplazar  los  literales  con  entradas  de  la  nueva  matriz

std::string  convertArabicNumberToRomanNumeral(unsigned  int  arabicNumber)  {
std::string  númeroromano;  while  
(número  arábigo  >=  arabicToRomanMappings[0].rabicNumeral)  { romanNumeral  +=  
arabicToRomanMappings[0].romanNumeral;  arabicNumber  ­=  
arabicToRomanMappings[0].arabicNumber; }  while  (número  arábigo  >=  10)  

{ númeroromano  +=  "X";  numero  arabe  
­=  10; }  while  (número  
arábigo  >=  1)  

{ númeroromano  +=  "I";  número  
arábigo­­; }  return  
númeroromano; }

Todas  las  pruebas  pasan.  Entonces  podemos  continuar  llenando  la  matriz  con  las  asignaciones  "10­is­X"  y  "1­is­I" (no  olvides
para  ajustar  el  tamaño  de  la  matriz  en  consecuencia!).

Listado  8­23.  Nuevamente  surge  un  patrón:  la  redundancia  de  código  obvia  puede  eliminarse  mediante  un  bucle

const  std::size_t  numberOfMappings  { 3 }; // ...  const  

ArabicToRomanMappings  arabicToRomanMappings  =  { { { 100,  "C" },  { 10,  
"X" },  { 1,  "I" } } };

208
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

std::string  convertArabicNumberToRomanNumeral(unsigned  int  arabicNumber)  {
std::string  númeroromano;  while  
(número  arábigo  >=  arabicToRomanMappings[0].rabicNumeral)  { romanNumeral  +=  
arabicToRomanMappings[0].romanNumeral;  arabicNumber  ­=  
arabicToRomanMappings[0].arabicNumber; }  while  (Número  arábigo  >=  

Asignaciones  árabes  a  romanos[1].  Número  árabe)  { Número  romano  +=  Asignaciones  
árabes  a  romanos  [1].  Número  romano;  arabicNumber  ­=  
arabicToRomanMappings[1].arabicNumber; }  while  (Número  arábigo  >=  

Asignaciones  árabes  a  romanos[2].Número  árabe)  { Número  romano  +=  Asignaciones  
árabes  a  romanos[2].  Número  romano;  arabicNumber  ­=  
arabicToRomanMappings[2].arabicNumber; }  return  númeroromano;

Y  de  nuevo,  se  pasan  todas  las  pruebas.  ¡Excelente!  Pero  todavía  hay  mucho  código  duplicado,  por  lo  que  
debemos  continuar  con  nuestra  refactorización.  La  buena  noticia  es  que  ahora  podemos  ver  que  la  única  diferencia  en  los  
tres  bucles  while  es  solo  el  índice  de  la  matriz.  Esto  significa  que  podemos  arreglárnoslas  con  solo  un  bucle  while  si  
iteráramos  a  través  de  la  matriz.

Listado  8­24.  A  través  del  bucle  for  basado  en  rango,  el  principio  DRY  ya  no  se  viola

std::string  convertArabicNumberToRomanNumeral(unsigned  int  arabicNumber)  {
std::string  númeroromano;  for  
(const  auto&  mapeo:  arabicToRomanMappings)  { while  (número  
arábigo  >=  mapeo.número  arábigo)  { númeroromano  +=  
mapeo.númeroromano;  numeroArabe  ­=  
mapeo.numeroArabe; } }  return  númeroromano; }

Todas  las  pruebas  pasan.  ¡Wow  eso  es  genial!  Solo  eche  un  vistazo  a  este  fragmento  de  código  compacto  y  fácil  de  
leer.  Ahora  se  pueden  admitir  más  asignaciones  de  números  arábigos  a  sus  equivalentes  romanos  al  agregarlos  a  la  matriz.
Probaremos  esto  por  1,000,  que  debe  convertirse  en  una  "M".  Aquí  está  nuestra  próxima  prueba:

afirmar  que  (1000).  se  convierte  en  número  romano  ("M");

La  prueba  falló  como  se  esperaba.  Al  agregar  otro  elemento  para  "1000­is­M"  a  la  matriz,  la  nueva  prueba  y,  por  supuesto,  
todas  las  pruebas  anteriores  deberían  pasar.

const  ArabicToRomanMappings  arabicToRomanMappings  =  { {
{1000,  "M"},
{100,  "C"},  {
10,  "X" },  {
1,  "yo" } } };

209
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Una  prueba  exitosa  después  de  este  pequeño  cambio  confirma  nuestra  suposición:  ¡funciona!  Eso  fue  bastante  fácil.
Podemos  agregar  más  pruebas  ahora,  por  ejemplo,  para  2000  y  3000.  E  incluso  3333  debería  funcionar  inmediatamente:

afirmar  que  (2000).  se  convierte  en  número  romano  ("MM");  afirmar  
que  (3000).  se  convierte  en  número  romano  ("MMM");  afirmar  que  
(3333).  se  convierte  en  número  romano  ("MMMCCCXXXIII");

Bien.  Nuestro  código  funciona  incluso  con  estos  casos.  Sin  embargo,  hay  algunos  números  romanos  que  aún  no  se  han  
implementado.  Por  ejemplo,  el  5  que  se  tiene  que  convertir  a  “V”.

afirmar  que  (5).  se  convierte  en  número  romano  ("V");

Como  era  de  esperar,  esta  prueba  falla.  La  pregunta  interesante  es  la  siguiente:  ¿qué  debemos  hacer  ahora  que  la  prueba
se  pasa?  Tal  vez  pienses  en  un  tratamiento  especial  de  este  caso.  Pero,  ¿es  este  realmente  un  caso  especial,  o  podemos  
tratar  esta  conversión  de  la  misma  manera  que  las  conversiones  anteriores  y  ya  implementadas?
Probablemente  lo  más  simple  que  podría  funcionar  es  simplemente  agregar  un  nuevo  elemento  en  el  índice  correcto  para
nuestra  matriz?  Bueno,  tal  vez  valga  la  pena  probarlo...

const  ArabicToRomanMappings  arabicToRomanMappings  =  { {
{1000,  "M"},
{100,  "C"},  {
10,  "X" },  {
5,  "V" },  {
1,  "yo" } } };

Nuestra  suposición  era  cierta:  ¡Se  pasan  todas  las  pruebas!  Incluso  los  números  arábigos  como  6  y  37  deben  convertirse
correctamente  ahora  a  su  equivalente  romano.  Verificamos  eso  agregando  aserciones  para  estos  casos:

afirmar  que  (6).  se  convierte  en  número  romano  

("VI"); //...afirmeEso(37).esConvertidoEnNúmeroRomano("XXXVII");

Acercándose  a  la  línea  de  meta
Y  no  sorprende  que  podamos  usar  básicamente  el  mismo  enfoque  para  "50­is­L"  y  "500­is­D".
A  continuación,  debemos  ocuparnos  de  la  implementación  de  la  llamada  notación  de  resta,  por  ejemplo,  el  número  
arábigo  4  debe  convertirse  en  el  número  romano  "IV".  ¿Cómo  podríamos  implementar  estos  casos  especiales  con  elegancia?

Bueno,  después  de  una  breve  consideración,  se  vuelve  obvio  que  estos  casos  no  son  nada  realmente  especial.
En  última  instancia,  por  supuesto,  no  está  prohibido  agregar  una  regla  de  mapeo  a  nuestra  matriz  donde  la  cadena  contiene  
dos  caracteres  en  lugar  de  uno.  Por  ejemplo,  podemos  simplemente  agregar  una  nueva  entrada  "4­is­IV"  a  la  matriz  
arabicToRomanMappings.  Tal  vez  dirás:  "¿No  es  eso  un  truco?"  No,  no  lo  creo,  es  pragmático  y  fácil,  sin  complicar  
innecesariamente  las  cosas.
Por  lo  tanto,  primero  agregamos  una  nueva  prueba  que  fallará:

afirmar  que  (4).  se  convierte  en  número  romano  ("IV");

210
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Para  pasar  la  nueva  prueba,  agregamos  la  regla  de  mapeo  correspondiente  para  4  (vea  la  penúltima  entrada  
en  la  matriz):

const  ArabicToRomanMappings  arabicToRomanMappings  =  { {
{1000,  "M"},
{500,  "D"},
{100,  "C"},  {
50,  "L" },  {
10,  "X" },  {
5,  "V" },  {
4,  "IV" },  {
1,  "yo" } } };

Después  de  ejecutar  todas  las  pruebas  y  verificar  que  pasaron,  ¡podemos  estar  seguros  de  que  nuestra  solución  
también  funciona  para  4!  Por  lo  tanto,  podemos  repetir  ese  patrón  para  “9­is­IX”,  “40­is­XL”,  “90­is­XC”,  y  así  sucesivamente.  El  
esquema  es  siempre  el  mismo,  por  lo  que  no  muestro  el  código  fuente  resultante  aquí  (el  resultado  final  con  el  código  completo  
se  muestra  a  continuación),  pero  creo  que  no  es  difícil  de  comprender.

¡Hecho!

La  pregunta  interesante  es  esta:  ¿Cuándo  sabemos  que  hemos  terminado?  ¿Que  el  software  que  tenemos  que  
implementar  está  terminado?  ¿Que  podemos  dejar  de  ejecutar  el  ciclo  TDD?  ¿Realmente  tenemos  que  probar  todos  los  
números  del  1  al  3999  cada  uno  mediante  una  prueba  unitaria  para  saber  que  hemos  terminado?
La  respuesta  simple:  si  todos  los  requisitos  de  nuestro  fragmento  de  código  se  han  implementado  con  éxito,
y  no  encontramos  una  nueva  prueba  unitaria  que  conduzca  a  un  nuevo  código  de  producción,  ¡hemos  terminado!
Y  ese  es  exactamente  el  caso  en  este  momento  para  nuestro  kata  TDD.  Todavía  podríamos  agregar  muchas  más  
afirmaciones  al  método  de  prueba;  la  prueba  se  pasaría  cada  vez  sin  necesidad  de  cambiar  el  código  de  producción.  Esta  
es  la  forma  en  que  TDD  nos  "habla":  "¡Oye,  amigo,  ya  terminaste!"
El  resultado  se  parece  a  lo  siguiente:

Listado  8­25.  Esta  versión  se  registró  en  GitHub  (ver  la  URL  a  continuación)  con  el  mensaje  de  confirmación  "Listo".

#include  <gtest/gtest.h>  
#include  <cadena>  
#include  <matriz>

int  main(int  argc,  char**  argv)  {
pruebas::InitGoogleTest(&argc,  argv);  devolver  
EJECUTAR_TODAS_PRUEBAS(); }

struct  ArabicToRomanMapping  
{ unsigned  int  arabicNumber;  
std::string  númeroromano; };

const  std::size_t  numberOfMappings  { 13 };  usando  
ArabicToRomanMappings  =  std::array<ArabicToRomanMapping,  numberOfMappings>;

211
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

const  ArabicToRomanMappings  arabicToRomanMappings  =  { {
{1000,  "M"},
{900,  "CM"},
{500,  "D"},
{ 400,  "CD" },
{100,  "C"},  {
90,  "XC" },  {
50,  "L" },  {
40,  "XL" },  {
10,  "X" },  {
9,  "IX" },  {
5,  "V" },  {
4,  "IV" },  {
1,  "yo" } } };

std::string  convertArabicNumberToRomanNumeral(unsigned  int  arabicNumber)  {
std::string  númeroromano;  for  
(const  auto&  mapeo:  arabicToRomanMappings)  { while  (número  
arábigo  >=  mapeo.número  arábigo)  { númeroromano  +=  
mapeo.númeroromano;  numeroArabe  ­=  
mapeo.numeroArabe; }

}  return  númeroromano;
}

//  El  código  de  prueba  comienza  aquí...

clase  RomanNumeralAssert  
{ público:
RomanNumeralAssert()  =  eliminar;  
explícito  RomanNumeralAssert  (const  unsigned  int  arabicNumber):
NúmeroArabeParaConvertir(NúmeroArábigo)  { }
void  isConvertedToRomanNumeral(const  std::string&  ExpectedRomanNumeral)  const  {
ASSERT_EQ(número  romano  esperado,  convertir  número  arábigo  en  número  romano  (número  arábigo  en  conversión)); }

privado:  
const  unsigned  int  arabicNumberToConvert; };

RomanNumeralAssert  afirmar  que  (const  unsigned  int  arabicNumber)  {
return  RomanNumeralAssert  { número  arabe };
}

PRUEBA(ArabicToRomanNumeralsConverterTestCase,  conversionOfArabicNumbersToRomanNumerals_Works)  
{
afirmar  que  (1).  se  convierte  en  número  romano  ("I");  
afirmar  que  (2).  se  convierte  en  número  romano  ("II");  
afirmar  que  (3).  se  convierte  en  número  romano  ("III");

212
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

afirmar  que  (4).  se  convierte  en  número  romano  ("IV");  afirmar  
que  (5).  se  convierte  en  número  romano  ("V");  afirmar  que  
(6).  se  convierte  en  número  romano  ("VI");  afirmar  que  (9).  se  
convierte  en  número  romano  ("IX");  afirmar  que  (10).  se  
convierte  en  número  romano  ("X");  afirmar  que  (20).  se  
convierte  en  número  romano  ("XX");  afirmar  que  (30).  se  
convierte  en  número  romano  ("XXX");  afirmar  que  (33).  se  
convierte  en  número  romano  ("XXXIII");  afirmar  que  (37).  se  convierte  
en  número  romano  ("XXXVII");  afirmar  que  (50).  se  convierte  en  
número  romano  ("L");  afirmar  que  (99).  se  convierte  en  número  
romano  ("XCIX");  afirmar  que  (100).  se  convierte  en  número  
romano  ("C");  afirmar  que  (200).  se  convierte  en  número  
romano  ("CC");  afirmar  que  (300).  se  convierte  en  número  
romano  ("CCC");  afirmar  que  (499).  se  convierte  en  número  
romano  ("CDXCIX");  afirmar  que  (500).  se  convierte  en  número  
romano  ("D");  afirmar  que  (1000).  se  convierte  en  número  
romano  ("M");  afirmar  que  (2000).  se  convierte  en  número  
romano  ("MM");  assertThat(2017).isConvertedToRomanNumeral("MMXVII");  
afirmar  que  (3000).  se  convierte  en  número  romano  ("MMM");  afirmar  
que  (3333).  se  convierte  en  número  romano  ("MMMCCCXXXIII");  
assertThat(3999).isConvertedToRomanNumeral("MMMCMXCIX"); }

■  Información  El  código  fuente  de  Roman  Numerals  Kata  completo,  incluido  su  historial  de  versiones,  se  puede  
encontrar  en  GitHub  en:  https://github.com/clean­cpp/book­samples/.

¡Esperar!  Sin  embargo,  todavía  queda  un  paso  muy  importante  por  dar:  debemos  separar  el  código  de  producción  
del  código  de  prueba.  Usamos  el  archivo  ArabicToRomanNumeralsConverterTestCase.cpp  todo  el  tiempo  como  nuestro  
banco  de  trabajo,  pero  ahora  ha  llegado  el  momento  en  que  el  creador  de  software  tiene  que  quitar  su  trabajo  terminado  del  
tornillo  de  banco.  En  otras  palabras,  el  código  de  producción  ahora  debe  moverse  a  un  nuevo  archivo  diferente,  aún  por  
crear;  pero,  por  supuesto,  las  pruebas  unitarias  aún  deberían  poder  probar  el  código.
Durante  este  último  paso  de  refactorización,  se  pueden  tomar  algunas  decisiones  de  diseño.  Por  ejemplo,  ¿permanece  
con  una  función  de  conversión  independiente,  o  el  método  de  conversión  y  la  matriz  deben  envolverse  en  una  nueva  clase?  
Claramente  preferiría  lo  último  (incrustar  el  código  en  una  clase)  porque  tiene  un  diseño  orientado  a  objetos  y  es  más  fácil  
ocultar  los  detalles  de  implementación  con  la  ayuda  de  la  encapsulación.
No  importa  cómo  se  proporcione  el  código  de  producción  y  cómo  se  integre  en  su  entorno  de  uso  (esto  depende  del  
propósito),  nuestra  cobertura  de  prueba  de  unidad  sin  interrupciones  hace  que  sea  poco  probable  que  algo  salga  mal.

Las  ventajas  de  TDD
El  desarrollo  basado  en  pruebas  es  principalmente  una  herramienta  y  una  técnica  para  el  diseño  y  desarrollo  
incremental  de  un  componente  de  software.  Es  por  eso  que  el  acrónimo  TDD  también  se  conoce  como  "Diseño  basado  
en  pruebas".  Es  una  forma,  por  supuesto  no  la  única,  de  pensar  en  sus  requisitos  o  diseñar  antes  de  escribir  el  código  
de  producción.

213
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Las  ventajas  significativas  de  TDD  son  las  siguientes:

•  TDD,  si  se  hace  bien,  lo  obliga  a  dar  pequeños  pasos  al  escribir  software.  El
El  enfoque  garantiza  que  siempre  tenga  que  escribir  solo  unas  pocas  líneas  de  código  de  producción  
para  volver  a  alcanzar  el  estado  cómodo  en  el  que  todo  funciona.  Esto  también  significa  que,  como  
mucho,  se  encuentra  a  unas  pocas  líneas  de  código  de  una  situación  en  la  que  todo  ha  funcionado.  
Esta  es  la  principal  diferencia  con  el  enfoque  tradicional  de  producir  y  cambiar  una  gran  cantidad  de  
código  de  producción  de  antemano,  lo  que  va  de  la  mano  con  el  inconveniente  de  que  el  software  a  
veces  no  se  puede  compilar  y  ejecutar  sin  errores  durante  horas  o  días.

•  TDD  establece  un  ciclo  de  retroalimentación  muy  rápido.  Los  desarrolladores  siempre  deben  saber  si  todavía  
están  trabajando  en  un  sistema  correcto.  Por  lo  tanto,  es  importante  para  ellos  que  tengan  un  circuito  de  
retroalimentación  rápido  para  saber  en  una  fracción  de  segundo  que  todo  funciona  correctamente.  Las  
pruebas  complejas  de  sistema  e  integración,  especialmente  si  aún  se  realizan  manualmente,  no  son  
capaces  de  esto  y  son  demasiado  lentas  (recuerde  la  Pirámide  de  prueba  en  el  Capítulo  2).

•  La  creación  de  una  prueba  unitaria  primero  ayuda  a  un  desarrollador  a  considerar  realmente  lo  que  debe  
hacerse.  En  otras  palabras,  TDD  garantiza  que  el  código  no  se  piratee  simplemente  desde  el  cerebro  
hasta  el  teclado.  Eso  es  bueno,  porque  el  código  que  se  escribió  de  esta  manera  a  menudo  es  propenso  
a  errores,  difícil  de  leer  y,  a  veces,  incluso  superfluo.  Muchos  desarrolladores  suelen  ir  más  rápido  que  su  
verdadera  capacidad  para  ofrecer  un  buen  trabajo.  TDD  es  una  forma  de  ralentizar  a  los  desarrolladores  
en  un  sentido  positivo.  No  se  preocupen,  gerentes,  es  bueno  que  sus  desarrolladores  disminuyan  la  
velocidad,  porque  esto  pronto  se  verá  recompensado  con  un  aumento  notable  en  la  calidad  y  velocidad  
en  el  proceso  de  desarrollo  cuando  la  alta  cobertura  de  prueba  muestre  su  efecto  positivo.

•  Con  TDD  surge  una  especificación  sin  espacios  en  forma  de  código  ejecutable.
Las  especificaciones  escritas  en  lenguaje  natural  con  un  programa  de  procesamiento  de  texto  de  una  suite  
de  Office,  por  ejemplo,  no  son  ejecutables,  son  "artefactos  muertos".

•  El  desarrollador  trata  mucho  más  consciente  y  responsablemente  con  las  dependencias.
Si  se  requiere  otro  componente  de  software  o  incluso  un  sistema  externo  (por  ejemplo,  una  base  de  
datos),  esta  dependencia  puede  definirse  debido  a  una  abstracción  (interfaz)  y  reemplazarse  por  un  
doble  de  prueba  (también  conocido  como  objeto  simulado)  para  la  prueba.  Los  módulos  de  software  
resultantes  (p.  ej.,  clases)  son  más  pequeños,  tienen  un  acoplamiento  flexible  y  contienen  solo  el  
código  necesario  para  pasar  las  pruebas.

•  El  código  de  producción  emergente  con  TDD  tendrá  una  cobertura  de  prueba  unitaria  del  100  %  de  
forma  predeterminada.  Si  TDD  se  realizó  correctamente,  no  debería  haber  una  sola  línea  de  código  
de  producción  que  no  haya  sido  motivada  por  una  prueba  de  unidad  escrita  previamente.

El  desarrollo  basado  en  pruebas  puede  ser  un  impulsor  y  un  habilitador  para  un  diseño  de  software  bueno  y  sostenible.  Como  
ocurre  con  muchas  otras  herramientas  y  métodos,  la  práctica  de  TDD  no  puede  garantizar  un  buen  diseño.  No  es  una  panacea  para  
los  problemas  de  diseño.  Las  decisiones  de  diseño  todavía  las  toma  el  desarrollador  y  no  la  herramienta.  Como  mínimo,  TDD  es  un  
enfoque  útil  para  evitar  lo  que  podría  percibirse  como  un  mal  diseño.  Muchos  desarrolladores  que  usan  TDD  en  su  trabajo  diario  
pueden  confirmar  que  es  extremadamente  difícil  producir  o  tolerar  código  malo  y  desordenado  con  este  enfoque.

Y  no  hay  duda  sobre  cuándo  un  desarrollador  ha  terminado  de  implementar  todas  las  funcionalidades  requeridas:  si  todas  las  
pruebas  unitarias  están  en  verde,  significa  que  todos  los  requisitos  en  la  unidad  están  satisfechos  y  ¡el  trabajo  está  hecho!  Y  un  efecto  
secundario  agradable  es  que  está  hecho  en  alta  calidad.

214
Machine Translated by Google

Capítulo  8  ■  Desarrollo  basado  en  pruebas

Además,  el  flujo  de  trabajo  de  TDD  también  impulsa  el  diseño  de  la  unidad  a  desarrollar,  especialmente  su  interfaz.
Con  TDD  y  Test  First,  el  diseño  y  la  implementación  de  la  API  se  guían  por  sus  casos  de  prueba.  Cualquiera  que  haya  intentado  escribir  
pruebas  unitarias  para  código  heredado  sabe  lo  difícil  que  puede  ser.  Estos  sistemas  generalmente  se  construyen  "Código  primero".  
Muchas  dependencias  inconvenientes  y  un  mal  diseño  de  API  complican  las  pruebas  en  tales  sistemas.  Y  si  una  unidad  de  software  
es  difícil  de  probar,  también  es  difícil  de  (re)utilizar.  En  otras  palabras:  TDD  brinda  una  retroalimentación  temprana  sobre  la  usabilidad  de  
una  unidad  de  software,  es  decir,  qué  tan  simple  puede  integrarse  y  usarse  esa  pieza  de  software  en  su  entorno  de  ejecución  planificado.

Cuándo  no  debemos  usar  TDD
La  pregunta  final  es  esta:  ¿deberíamos  desarrollar  cada  pieza  de  código  de  un  sistema  utilizando  un  enfoque  de  prueba  primero?
Mi  respuesta  clara  es  ¡ No!
Sin  duda:  el  desarrollo  basado  en  pruebas  es  una  excelente  práctica  para  guiar  el  diseño  y  la  implementación  de  una  pieza  de  
software.  En  teoría,  incluso  sería  posible  desarrollar  casi  todas  las  partes  de  un  sistema  de  software  de  esta  manera.  Y  como  una  especie  
de  efecto  secundario  positivo,  el  código  emergente  se  prueba  al  100  %  de  forma  predeterminada.
Pero  algunas  partes  de  un  proyecto  son  tan  simples,  pequeñas  o  menos  complejas  que  no  justifican  este  enfoque.  Si  puede  escribir  
su  código  rápidamente,  porque  la  complejidad  y  los  riesgos  son  bajos,  entonces,  por  supuesto,  puede  hacerlo.  Ejemplos  de  tales  situaciones  
son  las  clases  de  datos  puras  sin  funcionalidad  (lo  cual  es,  por  cierto,  un  olor,  pero  por  otras  razones;  vea  la  sección  sobre  clases  
anémicas  en  el  Capítulo  6),  o  un  simple  código  de  unión  que  simplemente  se  acopla  con  dos  módulos.

Además,  la  creación  de  prototipos  puede  ser  una  tarea  muy  difícil  con  TDD.  Cuando  ingresa  a  un  nuevo  territorio,  o  debe  desarrollar  
software  en  un  entorno  muy  innovador  sin  experiencia  en  el  dominio,  a  veces  no  está  seguro  de  qué  camino  tomará  para  encontrar  una  
solución.  Escribir  pruebas  unitarias  primero  en  proyectos  con  requisitos  muy  volátiles  y  confusos  puede  ser  una  tarea  extremadamente  
desafiante.  A  veces,  puede  ser  mejor  escribir  una  primera  solución  rudimentaria  de  manera  fácil  y  rápida,  y  garantizar  su  calidad  en  un  
paso  posterior  con  la  ayuda  de  pruebas  unitarias  actualizadas.

Otro  gran  desafío,  para  el  que  TDD  no  ayudará,  es  conseguir  una  buena  arquitectura.  TDD  no  reemplaza  la  reflexión  
necesaria  sobre  las  estructuras  de  grano  grueso  (subsistemas,  componentes,  …)  de  su  sistema  de  software.  Si  se  enfrenta  a  
decisiones  fundamentales  sobre  marcos,  bibliotecas,  tecnologías  o  patrones  de  arquitectura,  TDD  no  le  ayudará.

Para  cualquier  otra  cosa,  recomiendo  encarecidamente  TDD.  Este  enfoque  puede  ahorrar  mucho  tiempo,  dolores  de  cabeza  y  falsos
comienza  cuando  debe  desarrollar  una  unidad  de  software,  como  una  clase,  en  C++.

Para  cualquier  cosa  que  sea  más  compleja  que  unas  pocas  líneas  de  código,  los  artesanos  del  software  pueden  
probar  el  código  tan  rápido  como  otros  desarrolladores  pueden  escribir  código  sin  pruebas,  si  no  más  rápido.

—Sandro  Mancuso

■  Sugerencia  Si  desea  profundizar  más  en  el  desarrollo  controlado  por  pruebas  con  C++,  le  recomiendo  el  excelente  libro  
Programación  moderna  en  C++  con  desarrollo  controlado  por  pruebas  [Langr13]  de  Jeff  Langr.  El  libro  de  Jeff  ofrece  
información  mucho  más  profunda  sobre  TDD  y  le  brinda  lecciones  prácticas  sobre  los  desafíos  y  las  recompensas  de  hacer  TDD  en  C++.

215
Machine Translated by Google

CAPÍTULO  9

Patrones  de  diseño  y  modismos

Los  buenos  artesanos  pueden  aprovechar  una  gran  cantidad  de  experiencia  y  conocimientos.  Una  vez  que  han  encontrado  una  buena  
solución  para  un  determinado  problema,  toman  esta  solución  en  su  repertorio  para  aplicarla  en  el  futuro  a  un  problema  
similar.  Idealmente,  transforman  su  solución  en  algo  que  se  conoce  como  forma  canónica  y  la  documentan,  tanto  para  ellos  
como  para  los  demás.

FORMA  CANÓNICA

El  término  Forma  Canónica  en  este  contexto  describe  una  representación  de  algo  que  se  reduce  a  la  forma  
más  simple  y  significativa  sin  perder  generalidad.  Relacionado  con  los  patrones  de  diseño,  la  forma  canónica  
de  un  patrón  describe  sus  elementos  más  básicos:  nombre,  contexto,  problema,  fuerzas,  solución,  ejemplos,  
inconvenientes,  etc.

Esto  también  es  cierto  para  los  desarrolladores  de  software.  Los  desarrolladores  experimentados  pueden  aprovechar  una  
gran  cantidad  de  soluciones  de  muestra  para  problemas  de  diseño  constantemente  recurrentes  en  el  software.  Comparten  su  
conocimiento  con  otros  y  lo  hacen  reutilizable  para  problemas  similares.  El  principio  detrás  de  esto:  ¡No  reinventar  la  rueda!
En  1995,  se  publicó  un  libro  muy  conocido  y  ampliamente  aclamado.  Sus  cuatro  autores,  a  saber,  Erich
Gamma,  Richard  Helm,  Ralph  Johnson  y  John  Vlissides,  también  conocidos  como  Gang  of  Four  (GoF),  introdujeron  el  
principio  de  los  patrones  de  diseño  en  el  desarrollo  de  software  y  presentaron  un  catálogo  de  23  patrones  de  diseño  orientados  
a  objetos.  Su  título  es  Design  Patterns:  Elements  of  Reusable  Object­Oriented  Software  [Gamma95]  y  puede  considerarse  
hasta  el  día  de  hoy  como  uno  de  los  trabajos  más  importantes  en  el  dominio  del  desarrollo  de  software.

Algunas  personas  creen  que  Gamma  et  al.  había  inventado  todos  los  patrones  de  diseño  que  se  describen  en  su  libro.  
Pero  eso  no  es  cierto.  Los  patrones  de  diseño  no  se  inventan,  pero  se  pueden  encontrar.  Los  autores  han  examinado  los  sistemas  
de  software  que  estaban  bien  hechos  en  cuanto  a  flexibilidad,  mantenibilidad  y  extensibilidad.
Encontraron  la  causa  de  estas  características  positivas  y  las  describieron  en  forma  canónica.
Después  de  que  apareció  el  libro  de  la  Banda  de  los  Cuatro,  se  pensó  que  habría  una  avalancha  de  patrones
libros  en  los  años  siguientes.  Pero  esto  no  sucedió.  De  hecho,  en  los  años  siguientes  hubo  algunos  otros  libros  importantes  
sobre  el  tema  patrón,  como  Pattern­Oriented  Software  Architecture  (también  conocido  bajo  el  acrónimo  “POSA”)  [Busch96]  o  Patterns  
of  Enterprise  Application  Architecture  [Fowler02]  sobre  patrones  arquitectónicos. ,  pero  la  gran  masa  esperada  se  quedó  fuera.

Principios  de  diseño  frente  a  patrones  de  diseño
En  los  capítulos  anteriores,  hemos  discutido  muchos  principios  de  diseño.  Pero,  ¿cómo  se  relacionan  estos  principios  con  los  patrones  
de  diseño?  ¿Qué  es  más  importante?

©  Stephan  Roth  2017   217
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0_9
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Bueno,  asumamos  hipotéticamente  que  quizás  algún  día  la  orientación  a  objetos  se  vuelva  totalmente  impopular  y  la  
Programación  Funcional  (vea  el  Capítulo  7)  sea  el  paradigma  de  programación  dominante.
¿Principios  como  KISS,  DRY,  YAGNI,  Principio  de  responsabilidad  única,  Principio  abierto­cerrado,  Ocultación  de  información,  etc.,  
se  están  volviendo  inválidos  y,  por  lo  tanto,  sin  valor?  ¡ La  respuesta  clara  es  no!
Un  principio  es  una  “verdad”  o  “ley”  fundamental  que  sirve  como  base  para  las  decisiones.  Por  lo  tanto,  un  principio  es  en  
la  mayoría  de  los  casos  independiente  de  un  determinado  paradigma  o  tecnología  de  programación.  El  principio  KISS  (ver  Capítulo  
3),  por  ejemplo,  es  un  principio  muy  universal.  No  importa  si  está  programando  en  un  estilo  orientado  a  objetos  o  funcional,  o  si  usa  
diferentes  lenguajes  como  C  ++,  C  #,  Java  o  Erlang,  ¡tratar  de  hacer  algo  lo  más  simple  posible  siempre  es  una  actitud  que  vale  la  
pena!
Por  el  contrario,  un  patrón  de  diseño  es  una  solución  para  un  problema  de  diseño  concreto  en  un  contexto  determinado.  
Especialmente  aquellos  que  se  describen  en  el  famoso  libro  de  patrones  de  diseño  de  Gang  of  Four  están  estrechamente  asociados  
con  la  orientación  a  objetos.  Por  lo  tanto,  los  principios  son  más  duraderos  y  más  importantes.  Puede  encontrar  un  patrón  de  diseño  
para  un  determinado  problema  de  programación  por  sí  mismo,  si  ha  interiorizado  los  principios.

Las  decisiones  y  los  patrones  dan  soluciones  a  las  personas;  los  principios  les  ayudan  a  diseñar  los  suyos  propios.

—Eoin  Woods  en  un  discurso  de  apertura  sobre  el
Conferencia  de  trabajo  conjunto  IEEE/IFIP  sobre  arquitectura  de  software  2009  (WICSA2009)

Algunos  patrones  y  cuándo  usarlos
Además  de  los  23  patrones  de  diseño  descritos  en  el  libro  de  Gang  of  Four,  hay,  por  supuesto,  más  patrones.  Algunos  patrones  
se  encuentran  a  menudo  en  los  proyectos  de  desarrollo,  mientras  que  otros  son  más  o  menos  raros  o  exóticos.  Las  siguientes  
secciones  discuten  algunos  de  los  patrones  de  diseño  más  importantes  en  mi  opinión.  Aquellos  que  resuelven  problemas  de  diseño  
que  ocurren  con  mucha  frecuencia  y  que  un  desarrollador  debería  al  menos  haber  escuchado  antes.
Por  cierto,  ya  hemos  utilizado  algunos  patrones  de  diseño  en  los  capítulos  anteriores,  algunos  incluso  relativamente
intenso,  pero  no  lo  hemos  mencionado  ni  notado.  Solo  una  pequeña  pista:  en  el  libro  de  Gang  of  Four  [Gamma95]  puedes  
encontrar  un  patrón  de  diseño  que  se  llama...  ¡ Iterator!
Antes  de  continuar  con  la  discusión  de  patrones  de  diseño  individuales,  se  debe  señalar  aquí  una  advertencia:

■  Advertencia  ¡No  exagere  con  el  uso  de  patrones  de  diseño!  Sin  duda,  los  patrones  de  diseño  son  geniales  y,  a  veces,  incluso  

fascinantes.  Pero  un  uso  exagerado  de  ellos,  especialmente  si  no  hay  buenas  razones  que  lo  justifiquen,  puede  tener  consecuencias  

catastróficas.  Su  diseño  de  software  sufrirá  un  exceso  de  ingeniería  inútil.  Recuerda  siempre  KISS  y  YAGNI  (ver  Capítulo  3).

Pero  ahora  echemos  un  vistazo  a  algunos  patrones.

Inyección  de  dependencia  (DI)

La  inyección  de  dependencia  es  un  elemento  clave  de  la  arquitectura  ágil.

—Ward  Cunningham,  parafraseado  del
Panel  de  discusión  sobre  "Desarrollo  ágil  y  tradicional"  en  la  Conferencia  de  
calidad  de  software  del  noroeste  del  Pacífico  (PNSQC)  2004

218
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

El  hecho  de  que  comience  la  sección  sobre  patrones  de  diseño  específicos  con  uno  que  no  se  menciona  en  el  famoso  libro  de  
Gang  of  Four  tiene  razones  de  peso,  por  supuesto.  Estoy  convencido  de  que  la  inyección  de  dependencia  es,  con  mucho,  el  
patrón  más  importante  que  puede  ayudar  a  los  desarrolladores  de  software  a  mejorar  significativamente  el  diseño  de  un  software.
Este  patrón  puede  considerarse  con  razón  como  un  cambio  de  juego.
Antes  de  profundizar  en  la  Inyección  de  dependencia,  primero  quiero  contar  con  otro  patrón  que  es
perjudicial  para  el  buen  diseño  de  software:  ¡el  Singleton!

El  antipatrón  Singleton
Estoy  bastante  seguro  de  que  conoce  el  patrón  de  diseño  llamado  Singleton.  Es,  a  primera  vista,  un  patrón  simple  y  
extendido,  no  solo  en  el  dominio  de  C++  (veremos  pronto  que  su  supuesta  simplicidad  puede  ser  engañosa).  Algunas  
bases  de  código  incluso  están  llenas  de  Singletons.  Este  patrón,  por  ejemplo,  se  usa  a  menudo  para  los  llamados  registradores  
(objetos  con  fines  de  registro),  para  conexiones  de  bases  de  datos,  para  la  administración  central  de  usuarios  o  para  representar  
cosas  del  mundo  físico  (por  ejemplo,  hardware,  como  USB  o  interfaces  de  impresora) .  Además,  las  Fábricas  y  las  denominadas  
Clases  de  Utilidad  a  menudo  se  implementan  como  Singletons.  Estos  últimos  son  un  olor  a  código  por  sí  mismos,  porque  son  un  
signo  de  cohesión  débil  (ver  Capítulo  3).
Los  periodistas  han  preguntado  regularmente  a  los  autores  de  Design  Patterns  cuándo  revisarían  su  libro  y  publicarían  
una  nueva  edición.  Y  su  respuesta  habitual  era  que  no  verían  ninguna  razón  para  ello,  porque  el  contenido  del  libro  sigue  
siendo  válido  en  gran  medida.  Sin  embargo,  en  una  entrevista  con  la  revista  en  línea  InformIT ,  se  permitieron  dar  una  
respuesta  más  detallada.  Aquí  hay  un  pequeño  extracto  de  toda  la  entrevista,  que  revela  una  interesante  opinión  de  Gamma  
sobre  Singletons  (Larry  O'Brien  fue  el  entrevistador,  y  Erich  Gamma  da  la  respuesta):

[…]

Larry:  ¿Cómo  refactorizarías  "Patrones  de  diseño"?

Erich:  Hicimos  este  ejercicio  en  2005.  Aquí  hay  algunas  notas  de  nuestra  sesión.  Hemos  descubierto  que  los  
principios  del  diseño  orientado  a  objetos  y  la  mayoría  de  los  patrones  no  han  cambiado  desde  entonces.  (…)

Al  discutir  qué  patrones  eliminar,  descubrimos  que  todavía  los  amamos  a  todos.  (En  realidad  no,  estoy  a  favor  de  
eliminar  Singleton.  Su  uso  es  casi  siempre  un  olor  a  diseño).

—Patrones  de  diseño  15  años  después:  una  entrevista  con  Erich  Gamma,
Richard  Helm  y  Ralph  Johnson,  2009  [InformIT09]

Entonces,  ¿por  qué  Erich  Gamma  dijo  que  el  Patrón  Singleton  es  casi  siempre  un  olor  a  diseño?  ¿Qué  tiene  de  malo?

Para  responder  a  esto,  primero  veamos  qué  objetivos  se  deben  lograr  mediante  Singletons.  ¿Qué  requisitos  
se  pueden  cumplir  con  este  patrón?  Aquí  está  la  declaración  de  la  misión  del  patrón  Singleton  del  libro  GoF:

Asegúrese  de  que  una  clase  solo  tenga  una  instancia  y  proporcione  un  punto  de  acceso  global  a  ella.

—Erich  Gamma  et.  al.,  Patrones  de  diseño  [Gamma95]

Esta  declaración  contiene  dos  aspectos  conspicuos.  Por  un  lado,  la  misión  de  este  patrón  es  controlar  y  gestionar  todo  el  ciclo  
de  vida  de  su  única  instancia.  De  acuerdo  con  el  principio  de  Separación  de  preocupaciones,  la  gestión  del  ciclo  de  vida  de  un  
objeto  debe  ser  independiente  y  separada  de  su  lógica  comercial  específica  de  dominio.  En  un  Singleton,  estas  dos  
preocupaciones  básicamente  no  están  separadas.

219
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Por  otro  lado,  se  proporciona  un  acceso  global  a  esta  instancia,  de  modo  que  cualquier  otro  objeto  en  el
aplicación  puede  usarlo.  Este  discurso  sobre  un  "punto  de  acceso  global"  en  el  contexto  de  la  orientación  a  objetos  
parece  sospechoso  y  debería  generar  señales  de  alerta.
Veamos  primero  un  estilo  de  implementación  general  de  un  Singleton  en  C++,  el  llamado  Singleton  de  Meyers,  llamado  
así  por  Scott  Meyers,  el  autor  del  libro  Eficaz  C++  [Meyers05]:

Listado  9­1.  Una  implementación  de  Singleton  de  Meyers  en  C++  moderno

#ifndef  SINGLETON_H_  
#define  SINGLETON_H_

clase  Singleton  final  { público:  
Singleton  
estático  y  getInstance()  {
Singleton  estático  theInstance  { };  devolver  la  
instancia; }

int  hacerAlgo()  { return  
42; }

// ...más  funciones  miembro  haciendo  cosas  más  o  menos  útiles  aquí...

privado:  
Singleton()  =  predeterminado;  
Singleton(const  Singleton&)  =  eliminar;  
Singleton(Singleton&&)  =  eliminar;  Operador  
Singleton&  =(const  Singleton&)  =  borrar;  Singleton&  
operator=(Singleton&&)  =  eliminar; // ...

#terminara  si

Una  de  las  principales  ventajas  de  este  estilo  de  implementación  de  Singleton  es  que,  desde  C++  11,  el  
proceso  de  construcción  de  la  única  instancia  que  usa  una  variable  estática  dentro  de  getInstance()  es  seguro  para  
subprocesos  por  defecto  (ver  §  6.7  en  [ ISO11]).  ¡Tenga  cuidado,  porque  eso  no  significa  automáticamente  que  todas  las  
demás  funciones  miembro  de  Singleton  también  sean  seguras  para  subprocesos!  Esto  último  debe  ser  garantizado  por  el  desarrollador.
En  el  código  fuente,  el  uso  de  una  instancia  de  singleton  global  de  este  tipo  suele  verse  así:

Listado  9­2.  Un  extracto  de  la  implementación  de  una  clase  arbitraria  que  usa  Singleton

001  #include  "CualquierUsuarioSingleton.h"  
002  #include  "Singleton.h"  003  
#include  <cadena>  004 ... // ...

024
025  void  AnySingletonUser::aMemberFunction()  { // ...  std::string  
... result  =  
040 Singleton::getInstance().doThis();

220
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

... // ...
050 }  

051 ... // ...  

089  090  void  CualquierUsuarioSingleton::otraFunciónMiembro()  {
... //...  
098 resultado  int  =  Singleton::getInstance().doThat(); //...
...
104  valor  doble  =  Singleton::getInstance().doSomethingMore();
... //...
110 }  
111 // ...

Creo  que  ahora  queda  claro  cuál  es  uno  de  los  principales  problemas  con  Singletons.  Debido  a  su  
visibilidad  y  accesibilidad  global,  simplemente  se  usan  en  cualquier  lugar  dentro  de  la  implementación  de  otras  
clases.  Eso  significa  que  en  el  diseño  del  software,  todas  las  dependencias  de  este  Singleton  están  ocultas  
dentro  del  código.  No  puede  ver  estas  dependencias  examinando  las  interfaces  de  sus  clases,  es  decir,  sus  
atributos  y  métodos.
Y  la  clase  AnySingletonUser  ejemplificada  anteriormente  es  solo  representativa  de  quizás  cientos  de  clases  
dentro  de  una  gran  base  de  código,  muchas  de  las  cuales  también  usan  Singleton  en  diferentes  lugares.  En  otras  
palabras:  un  Singleton  en  OO  es  como  una  variable  global  en  la  programación  procedimental.  Puede  usar  este  
objeto  global  en  todas  partes,  y  no  puede  ver  ese  uso  en  la  interfaz  de  la  clase  de  uso,  sino  solo  en  su  implementación.
Esto  tiene  un  impacto  negativo  significativo  en  la  situación  de  dependencia  en  un  proyecto,  como  se  muestra  en  
la  Figura  9­1.

Figura  9­1.  Amado  por  todos:  ¡el  Singleton!

221
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

■  Nota  Quizás  se  esté  preguntando  al  ver  la  Figura  9­1  que  hay  una  instancia  de  variable  miembro  privada  dentro  
de  la  clase  Singleton,  que  no  se  puede  encontrar  en  este  formulario  en  la  implementación  recomendada  por  
Meyers.  Bueno,  UML  es  un  lenguaje  de  programación  agnóstico,  es  decir,  como  lenguaje  de  modelado  multipropósito  
no  conoce  C++,  Java  u  otros  lenguajes  orientados  a  objetos.  De  hecho,  también  en  Singleton  de  Meyers  hay  una  
variable  que  contiene  la  única  instancia,  pero  no  hay  una  notación  gráfica  para  una  variable  con  duración  de  
almacenamiento  estático  en  UML,  porque  esta  característica  es  propiedad  de  C++.  Por  lo  tanto,  elegí  la  forma  de  
representar  esta  variable  como  un  miembro  estático  privado.  Esto  hace  que  la  representación  también  sea  
compatible  con  la  implementación  Singleton  que  ya  no  se  recomienda  y  que  se  describe  en  el  libro  GoF  [Gamma95].

Creo  que  es  fácil  imaginar  que  todas  estas  dependencias  tendrán  importantes  inconvenientes  con  respecto  a  la  reutilización,  
la  mantenibilidad  y  la  capacidad  de  prueba.  Todas  esas  clases  de  clientes  anónimos  de  Singleton  están  estrechamente  acopladas  
a  él  (recuerde  la  buena  propiedad  del  acoplamiento  débil  que  hemos  discutido  en  el  Capítulo  3).
Como  consecuencia,  perdemos  por  completo  la  posibilidad  de  aprovechar  el  polimorfismo  para  proporcionar  una  implementación  
alternativa.  Solo  piensa  en  las  pruebas  unitarias.  ¿Cómo  puede  tener  éxito  implementar  una  prueba  de  unidad  real,  si  se  usa  algo  
dentro  de  la  implementación  de  la  clase  que  se  va  a  probar  que  no  puede  ser  reemplazado  fácilmente  por  un  Doble  de  prueba  
(también  conocido  como  Objeto  simulado;  consulte  la  sección  sobre  Dobles  de  prueba  en  el  Capítulo  2) ?
Y  recuerde  todas  las  reglas  para  las  buenas  pruebas  unitarias  que  hemos  discutido  en  el  Capítulo  2,  especialmente  
la  independencia  de  las  pruebas  unitarias.  Un  objeto  global  como  un  Singleton  tiene  a  veces  un  estado  mutable.  ¿Cómo  se  puede  
asegurar  la  independencia  de  las  pruebas,  si  muchas  o  casi  todas  las  clases  en  un  código  base  dependen  de  un  solo  objeto  
que  tiene  un  ciclo  de  vida  que  termina  con  la  terminación  del  programa,  y  que  posiblemente  tenga  un  estado  compartido  entre  
ellos? ?!
Otra  desventaja  de  Singletons  es  que  si  tienen  que  cambiarse  debido  a  requisitos  nuevos  o  cambiantes,  este  
cambio  podría  desencadenar  una  cascada  de  cambios  en  todas  las  clases  dependientes.  Todas  las  dependencias  visibles  en  la  
Figura  9­1  y  que  apuntan  al  Singleton  son  posibles  rutas  de  propagación  de  cambios.
Finalmente,  también  es  muy  difícil  asegurar  en  un  sistema  distribuido,  que  es  un  caso  común  en  la  arquitectura  de  software  
hoy  en  día,  que  exista  exactamente  una  instancia  de  una  clase.  Solo  imagine  el  patrón  de  microservicios,  donde  un  sistema  de  
software  complejo  se  compone  de  muchos  procesos  pequeños,  independientes  y  distribuidos.  En  tal  entorno,  los  Singletons  no  solo  
son  difíciles  de  proteger  contra  instanciaciones  múltiples,  sino  que  también  son  problemáticos  debido  al  estrecho  acoplamiento  que  
fomentan.
Entonces,  tal  vez  te  preguntes  ahora:  "Está  bien,  lo  tengo,  los  Singleton  son  malos,  pero  ¿cuáles  son  las  alternativas?"  La  
respuesta  quizás  sorprendentemente  simple,  que  por  supuesto  requiere  algunas  explicaciones  adicionales,  es  esta:  ¡ simplemente  
cree  uno  e  inyéctelo  donde  sea  necesario!

Inyección  de  dependencia  al  rescate
En  la  entrevista  antes  mencionada  con  Erich  Gamma  et  al.  los  autores  también  hicieron  una  declaración  sobre  esos
patrones  de  diseño,  que  les  gustaría  incluir  en  una  nueva  revisión  de  su  libro.  Nominaron  solo  algunos  patrones  que  posiblemente  se  
convertirían  en  su  trabajo  legendario  y  uno  de  ellos  es  Inyección  de  dependencia.
Básicamente,  la  inyección  de  dependencia  (DI)  es  una  técnica  en  la  que  los  objetos  de  servicio  independientes  que  necesita  
un  objeto  de  cliente  dependiente  se  suministran  desde  el  exterior.  El  objeto  de  cliente  no  tiene  que  preocuparse  por  sus  objetos  de  
servicio  requeridos  por  sí  mismo,  o  solicitar  activamente  los  objetos  de  servicio,  por  ejemplo,  de  una  fábrica  (consulte  
el  patrón  de  fábrica  más  adelante  en  este  capítulo)  o  de  un  localizador  de  servicios.

222
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

La  intención  detrás  de  DI  podría  formularse  de  la  siguiente  manera:

Desacople  los  componentes  de  sus  servicios  requeridos  de  tal  manera  que  los  componentes  
no  tengan  que  saber  los  nombres  de  estos  servicios,  ni  cómo  deben  adquirirse.

Veamos  un  ejemplo  específico,  el  Logger  ya  mencionado  anteriormente,  por  ejemplo,  una  clase  de  servicio,  que  ofrece  
la  posibilidad  de  escribir  entradas  de  registro.  Dichos  registradores  a  menudo  se  han  implementado  como  Singletons.  Por  
lo  tanto,  cada  cliente  del  registrador  depende  de  ese  objeto  Singleton  global,  como  se  muestra  en  la  figura  9­2.

Figura  9­2.  Tres  clases  específicas  de  dominio  de  una  tienda  web  dependen  del  singleton  Logger

Así  es  como  podría  verse  la  clase  singleton  Logger  en  el  código  fuente  (solo  se  muestran  las  partes  relevantes):

Listado  9­3.  El  Logger  implementado  como  Singleton

#incluir  <vista_cadena>

class  Logger  final  
{ public:  
static  Logger&  getInstance()  { static  
Logger  theLogger  { };  devuelve  el  
Registrador; }

void  writeInfoEntry(std::string_view  entrada)  {
// ...
}

void  writeWarnEntry(std::string_view  entrada)  {
// ...
}

void  writeErrorEntry(std::string_view  entrada)  {
// ...

} };

223
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

std::string_view  [C++17]

Desde  C++17,  hay  una  nueva  clase  disponible  en  el  estándar  de  lenguaje  C++:  std ::  string_view  (definida  en  el  
encabezado  <string_view>).  Los  objetos  de  esta  clase  son  proxies  de  gran  rendimiento  (Proxy  es,  por  cierto,  también  
un  patrón  de  diseño)  de  una  cadena,  que  son  baratos  de  construir  (no  hay  asignación  de  memoria  para  datos  de  cadena  
sin  procesar)  y,  por  lo  tanto,  también  son  baratos  de  copiar.

Y  otra  buena  característica  es:  std::string_view  también  puede  servir  como  un  adaptador  para  cadenas  de  estilo  C  (char*),  
matrices  de  caracteres  e  incluso  para  implementaciones  de  cadenas  propietarias  de  diferentes  marcos  como
CString  (MFC)  o  QString  (Qt):

CString  aString("Soy  un  objeto  de  cadena  del  tipo  CString  de  MFC");  std::string_view  
viewOnCString  { (LPCTSTR)aString };

Por  lo  tanto,  es  la  clase  ideal  para  representar  cadenas  cuyos  datos  ya  pertenecen  a  otra  persona  y  si  se  requiere  
acceso  de  solo  lectura,  por  ejemplo,  durante  la  ejecución  de  una  función.  Por  ejemplo,  en  lugar  de  las  referencias  
constantes  generalizadas  a  std::string,  ahora  se  debe  usar  std::string_view  como  reemplazo  de  los  parámetros  de  
función  de  cadena  de  solo  lectura  en  un  programa  C++  moderno.

Ahora  solo  seleccionamos  con  fines  de  demostración  una  de  esas  muchas  clases  que  usan  el  registrador
Singleton  en  su  implementación  para  escribir  entradas  de  registro,  la  clase  CustomerRepository:

Listado  9­4.  Un  extracto  de  la  clase  CustomerRepository

#include  "Cliente.h"
#include  "Identificador.h"
#include  "Registrador.h"

class  CustomerRepository  

{ público: //...
Cliente  findCustomerById(const  Identifier&  customerId)  {
Logger::getInstance().writeInfoEntry("Comenzando  a  buscar  un  cliente  especificado  por  un
identificador  único  dado..."); // ...

} // ... };

Para  deshacerse  del  Singleton  y  poder  reemplazar  el  objeto  Logger  con  un  Test  Double  durante
pruebas  unitarias,  primero  debemos  aplicar  el  Principio  de  Inversión  de  Dependencia  (DIP;  consulte  el  Capítulo  6).  Esto  
significa  que  primero  tenemos  que  introducir  una  abstracción  (una  interfaz)  y  hacer  que  tanto  el  CustomerRepository  
como  el  Logger  concreto  dependan  de  esa  interfaz,  como  se  muestra  en  la  Figura  9­3.

224
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Figura  9­3.  Desacoplamiento  a  través  del  Principio  de  Inversión  de  Dependencia  aplicado

Así  es  como  se  ve  la  nueva  interfaz  LoggingFacility  en  el  código  fuente:

Listado  9­5.  La  interfaz  de  LoggingFacility

#include  <memoria>  
#include  <vista_cadena>

class  LoggingFacility  { público:  
virtual  
~LoggingFacility()  =  predeterminado;  virtual  void  
writeInfoEntry(std::string_view  entrada)  =  0;  virtual  void  writeWarnEntry(entrada  
std::string_view)  =  0;  virtual  void  writeErrorEntry(entrada  std::string_view)  =  
0; };

utilizando  Logger  =  std::shared_ptr<LoggingFacility>;

El  StandardOutputLogger  es  un  ejemplo  de  una  clase  Logger  específica  que  implementa  el
interfaz  LoggingFacility  y  escribe  el  registro  en  la  salida  estándar,  como  sugiere  su  nombre:

Listado  9­6.  Una  posible  implementación  de  LoggingFacility:  StandardOutputLogger

#incluye  "LoggingFacility.h"  #incluye  
<iostream>

clase  StandardOutputLogger:  public  LoggingFacility  { public:  virtual  void  

writeInfoEntry(std::string_view  entry)  override  { std::cout  <<  "[INFO]  "  <<  entrada  <<  
std::endl; }

virtual  void  writeWarnEntry(entrada  std::string_view)  override  { std::cout  <<  
"[ADVERTENCIA]  "  <<  entrada  <<  std::endl; }

225
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

virtual  void  writeErrorEntry(entrada  std::string_view)  override  { std::cout  <<  "[ERROR]  
"  <<  entrada  <<  std::endl; } };

A  continuación,  debemos  modificar  la  clase  CustomerRepository.  Primero,  creamos  una  nueva  variable  miembro  de
el  tipo  de  puntero  inteligente  alias  Logger.  Esta  instancia  de  puntero  se  pasa  a  la  clase  a  través  de  un  constructor  
de  inicialización.  En  otras  palabras,  permitimos  que  una  instancia  de  una  clase  que  implementa  la  interfaz  
LoggingFacility  se  inyecte  en  el  objeto  CustomerRepository  durante  la  construcción.  También  eliminamos  el  
constructor  predeterminado,  porque  no  queremos  permitir  que  se  cree  un  CustomerRepository  sin  un  registrador.
Además,  eliminamos  la  dependencia  directa  en  la  implementación  de  Singleton  y,  en  su  lugar,  usamos  el  puntero  
inteligente  Logger  para  escribir  entradas  de  registro.

Listado  9­7.  La  clase  modificada  Repositorio  de  clientes

#include  "Cliente.h"
#include  "Identificador.h"
#include  "LoggingFacility.h"

clase  CustomerRepository  { público:

CustomerRepository()  =  eliminar;  
explícito  CustomerRepository(const  Logger&  loggingService) :  logger  { loggingService }  { }
//...

Cliente  findCustomerById(const  Identifier&  customerId)  {
logger­>writeInfoEntry("Comenzando  a  buscar  un  cliente  especificado  por  un  identificador  único  dado..."); // ...

} // ...

privado: //...

registrador  registrador; };

Como  consecuencia  de  esta  refactorización,  ahora  hemos  logrado  que  la  clase  CustomerRepository  ya  no  
dependa  de  un  registrador  específico.  En  cambio,  CustomerRepository  simplemente  tiene  una  dependencia  de  una  
abstracción  (interfaz)  que  ahora  es  explícitamente  visible  en  la  clase  y  su  interfaz,  porque  está  representada  por  una  
variable  de  miembro  y  un  parámetro  de  constructor.  Eso  significa  que  la  clase  CustomerRepository  ahora  acepta  
objetos  de  servicio  con  fines  de  registro  que  se  pasan  desde  el  exterior,  como  este:

Listado  9­8.  El  objeto  Logger  se  inyecta  en  la  instancia  de  CustomerRepository

Registrador  registrador  =  std::make_shared<StandardOutputLogger>();
CustomerRepository  customerRepository  { registrador };

Este  cambio  de  diseño  tiene  efectos  significativamente  positivos.  Se  promueve  un  acoplamiento  flojo,  y  el  cliente
El  objeto  CustomerRepository  ahora  se  puede  configurar  con  varios  objetos  de  servicio  que  brindan  funcionalidad  
de  registro,  como  se  puede  ver  en  el  siguiente  diagrama  de  clases  UML  (Figura  9­4):

226
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Figura  9­4.  Class  CustomerRepository  se  puede  proporcionar  con  implementaciones  de  registro  específicas  a  través  de  su  constructor

Además,  la  capacidad  de  prueba  de  la  clase  CustomerRepository  se  ha  mejorado  significativamente.  Ya  no  hay  
dependencias  ocultas  para  Singletons.  Ahora  podemos  reemplazar  fácilmente  un  servicio  de  registro  real  por  un  objeto  simulado  
(consulte  el  Capítulo  2  sobre  Pruebas  Unitarias  y  Pruebas  Dobles).  Podemos  equipar  el  objeto  simulado  con  métodos  de  espionaje,  
por  ejemplo,  para  verificar  dentro  de  la  prueba  unitaria  qué  datos  dejarían  nuestro  objeto  CustomerRepository  a  través  de  la  
interfaz  LoggingFacility.

Listado  9­9.  Un  doble  de  prueba  (objeto  simulado)  para  pruebas  unitarias  de  clases  que  dependen  de  LoggingFacility

prueba  de  espacio  de  nombres  {

#include  "../src/LoggingFacility.h"  #include  <cadena>

class  LoggingFacilityMock :  public  LoggingFacility  { public:  virtual  void  

writeInfoEntry(std::string_view  entry)  override  {
entrada  de  registro  recientemente  escrita  =  entrada; }

virtual  void  writeWarnEntry(entrada  std::string_view)  override  {
entrada  de  registro  recientemente  escrita  =  entrada; }

virtual  void  writeErrorEntry(entrada  std::string_view)  override  {
entrada  de  registro  recientemente  escrita  =  entrada; }

std::string_view  getRecentlyWrittenLogEntry()  const  {
volver  recientementeWrittenLogEntry; }

privado:  
estándar::cadena  recientemente  escrita  en  el  
registro; };

usando  MockLogger  =  std::shared_ptr<LoggingFacilityMock>;

227
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Y  en  esta  prueba  de  unidad  ejemplar,  puede  ver  el  objeto  simulado  en  acción:

Listado  9­10.  Una  prueba  unitaria  de  ejemplo  usando  el  objeto  simulado

#include  "../src/CustomerRepository.h"  #include  
"LoggingFacilityMock.h"  #include  <gtest/gtest.h>

prueba  de  espacio  de  nombres  {

PRUEBA  (Caso  de  prueba  del  cliente,  Entrada  de  registro  escrita  como  se  esperaba)  {
Registrador  de  MockLogger  =  std::make_shared<LoggingFacilityMock>();
CustomerRepository  customerRepositoryToTest  { registrador };
Identificador  Idcliente  { 1234 };

customerRepositoryToTest.findCustomerById(customerId);

ASSERT_EQ("Comenzando  a  buscar  un  cliente  especificado  por  un  identificador  único  dado...",  logger­
>getRecentlyWrittenLogEntry());}

En  el  ejemplo  anterior,  presenté  la  inyección  de  dependencia  como  un  patrón  para  eliminar  los  molestos  Singleton,  pero,  por  
supuesto,  esta  es  solo  una  de  muchas  aplicaciones.  Básicamente,  un  buen  diseño  de  software  orientado  a  objetos  debe  garantizar  
que  los  módulos  o  componentes  involucrados  estén  acoplados  de  la  manera  más  flexible  posible,  y  la  inyección  de  dependencia  
es  la  clave  para  este  objetivo.  Al  aplicar  este  patrón  de  manera  consistente,  surgirá  un  diseño  de  software  que  tiene  una  
arquitectura  de  complemento  muy  flexible.  Y  como  una  especie  de  efecto  secundario  positivo,  esta  técnica  da  como  resultado  objetos  
altamente  comprobables.
La  responsabilidad  de  la  creación  y  vinculación  de  objetos  se  elimina  de  los  propios  objetos  y  se  centraliza  en  un  
componente  de  infraestructura,  el  llamado  Ensamblador  o  Inyector.  Este  componente  (vea  la  Figura  9­5)  generalmente  opera  
al  inicio  del  programa  y  procesa  algo  así  como  un  "plan  de  construcción" (por  ejemplo,  un  archivo  de  configuración)  para  todo  el  
sistema  de  software,  es  decir,  instancia  los  objetos  y  servicios  en  el  orden  correcto  y  inyecta  los  servicios  en  los  objetos  que  los  
necesitan.

Figura  9­5.  El  Ensamblador  es  responsable  de  la  creación  e  inyección  de  objetos.

228
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Por  favor,  preste  atención  a  la  agradable  situación  de  dependencia.  La  dirección  de  las  dependencias  de  creación.
(flechas  discontinuas  con  el  estereotipo  «Crear»)  conduce  desde  el  Ensamblador  a  los  otros  módulos  (clases).
En  otras  palabras,  ninguna  clase  en  este  diseño  "sabe"  que  existe  un  elemento  de  infraestructura  como  un  Ensamblador  (Eso  
no  es  completamente  correcto,  porque  al  menos  otro  elemento  en  el  sistema  de  software  sabe  sobre  la  existencia  de  
este  componente,  porque  el  proceso  de  ensamblaje  debe  ser  activado  por  alguien,  generalmente  al  inicio  del  programa).

En  algún  lugar  dentro  del  componente  Ensamblador,  posiblemente  se  podría  encontrar  algo  como  las  siguientes  líneas  de  
código:

Listado  9­11.  Partes  de  la  implementación  del  Ensamblador  podrían  verse  así

// ...
Registrador  loggingServiceToInject  =  std::make_shared<StandardOutputLogger>();  auto  
customerRepository  =  std::make_shared<CustomerRepository>(loggingServiceToInject); // ...

Esta  técnica  DI  se  denomina  inyección  de  constructor,  porque  el  objeto  de  servicio  que  se  va  a  inyectar  se  pasa  como  
argumento  a  un  constructor  de  inicialización  del  objeto  de  cliente.  La  ventaja  de  la  inyección  del  constructor  es  que  el  objeto  
del  cliente  se  inicializa  por  completo  durante  su  construcción  y  se  puede  usar  inmediatamente.
Pero,  ¿qué  hacemos  si  los  objetos  de  servicio  se  van  a  inyectar  en  objetos  de  cliente  mientras  se  ejecuta  el  programa,  por  
ejemplo,  si  un  objeto  de  cliente  solo  se  crea  ocasionalmente  durante  la  ejecución  del  programa,  o  si  el  registrador  específico  
debe  intercambiarse  en  tiempo  de  ejecución?  Luego,  el  objeto  del  cliente  debe  proporcionar  un  setter  para  el  objeto  del  servicio,  
como  en  el  siguiente  ejemplo:

Listado  9­12.  La  clase  Customer  proporciona  un  setter  para  inyectar  un  Logger

#include  "Dirección.h"  
#include  "LoggingFacility.h"

clase  Cliente  { público:

Cliente()  =  predeterminado;

void  setLoggingService(const  Logger&  loggingService)  { logger  =  
loggingService; }

//...

privado:
Dirección  Dirección;
registrador  registrador; };

Esta  técnica  DI  se  llama  inyección  setter.  Y,  por  supuesto,  también  es  posible  combinar  la  inyección  de  constructor  y  
la  inyección  de  setter.
La  Inyección  de  Dependencia  es  un  patrón  de  diseño  que  hace  que  un  diseño  de  software  esté  débilmente  acoplado  
y  eminentemente  configurable.  Permite  la  creación  de  diferentes  configuraciones  de  productos  para  diferentes  clientes  o  
propósitos  previstos  de  un  producto  de  software.  Aumenta  enormemente  la  capacidad  de  prueba  de  un  sistema  de  
software,  ya  que  permite  inyectar  objetos  simulados  muy  fácilmente.  Por  lo  tanto,  este  patrón  no  debe  ignorarse  al  diseñar  
cualquier  sistema  de  software  serio.  Si  desea  profundizar  más  en  este  patrón,  le  recomiendo  leer  el  artículo  del  blog  que  marca  
tendencia  “La  inversión  de  los  contenedores  de  control  y  el  patrón  de  inyección  de  dependencia”  escrito  por  Martin  Fowler  [Fowler04].

229
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

En  la  práctica,  a  menudo  se  utilizan  marcos  de  Inyección  de  Dependencia,  que  están  disponibles  comercialmente
y  soluciones  de  código  abierto.

Adaptador  Estoy  

seguro  de  que  el  Adaptador  (sinónimo:  Wrapper)  es  uno  de  los  patrones  de  diseño  más  utilizados.  La  razón  de  esto  es  que  la  
adaptación  de  interfaces  incompatibles  es  ciertamente  un  caso  que  a  menudo  es  necesario  en  el  desarrollo  de  software,  por  ejemplo,  si  
se  debe  integrar  un  módulo  desarrollado  por  otro  equipo,  o  cuando  se  utilizan  bibliotecas  de  terceros.

Aquí  está  la  declaración  de  la  misión  del  patrón  Adapter:

Convierta  la  interfaz  de  una  clase  en  otra  interfaz  que  esperan  los  clientes.  El  adaptador  permite  que  las  
clases  trabajen  juntas  que  de  otro  modo  no  podrían  debido  a  las  interfaces  incompatibles.

—Erich  Gamma  et.  al.,  Patrones  de  diseño  [Gamma95]

Desarrollemos  más  el  ejemplo  de  la  sección  anterior  sobre  Inyección  de  dependencia.  Supongamos  que  queremos  usar  BoostLog  v2  
(ver  http://www.boost.org)  para  fines  de  registro,  pero  queremos  que  el  uso  de  esta  biblioteca  de  terceros  sea  intercambiable  con  otros  
enfoques  y  tecnologías  de  registro.
La  solución  es  simple:  solo  tenemos  que  proporcionar  otra  implementación  de  la  interfaz  LoggingFacility,  que  adapta  la  
interfaz  de  BoostLog  a  la  interfaz  que  queremos,  como  se  muestra  en  la  Figura  9­6.

Figura  9­6.  Un  adaptador  para  una  solución  de  registro  de  Boost

En  el  código  fuente,  nuestra  implementación  adicional  de  la  interfaz  LoggingFacility  BoostTrivialLog
El  adaptador  se  ve  de  la  siguiente  manera:

Listado  9­13.  El  adaptador  para  Boost.Log  es  solo  otra  implementación  de  LoggingFacility

#incluye  "LoggingFacility.h"  #incluye  
<boost/log/trivial.hpp>

clase  BoostTrivialLogAdapter:  public  LoggingFacility  { public:  virtual  void  

writeInfoEntry(std::string_view  entry)  override  {
BOOST_LOG_TRIVIAL(información)  <<  entrada; }

230
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

virtual  void  writeWarnEntry(entrada  std::string_view)  override  {
BOOST_LOG_TRIVIAL(aviso)  <<  entrada; }

virtual  void  writeErrorEntry(entrada  std::string_view)  override  {
BOOST_LOG_TRIVIAL(error)  <<  entrada; } };

Las  ventajas  son  obvias:  a  través  del  patrón  Adapter,  ahora  hay  exactamente  una  clase  en  todo  mi  sistema  de  software  que  
depende  de  la  solución  de  registro  de  terceros.  Esto  también  significa  que  nuestro  código  no  está  contaminado  con  declaraciones  de  
registro  propietarias,  como  BOOST_LOG_TRIVIAL().  Y  debido  a  que  esta  clase  de  adaptador  es  solo  otra  implementación  de  la  interfaz  
LoggingFacility,  también  puedo  usar  Inyección  de  dependencia  (consulte  la  sección  anterior)  para  inyectar  instancias,  o  exactamente  la  
misma  instancia,  de  esta  clase  en  todos  los  objetos  de  cliente  que  quieran  usarla.

Los  adaptadores  pueden  facilitar  una  amplia  gama  de  posibilidades  de  adaptación  y  conversión  para  interfaces  incompatibles.  
Esto  va  desde  adaptaciones  simples,  como  nombres  de  operaciones  y  conversiones  de  tipos  de  datos,  hasta  admitir  un  conjunto  
completamente  diferente  de  operaciones.  En  nuestro  caso  anterior,  una  llamada  de  una  función  miembro  con  un  parámetro  de  cadena  
se  convierte  en  una  llamada  del  operador  de  inserción  para  secuencias.
Las  adaptaciones  de  interfaz  son,  por  supuesto,  más  fáciles  si  las  interfaces  a  adaptar  son  similares.  Si  las  interfaces  son
muy  diferente,  un  adaptador  también  puede  convertirse  en  una  pieza  de  código  muy  compleja.

Estrategia  Si  

recordamos  el  Principio  Abierto­Cerrado  (OCP)  descrito  en  el  Capítulo  6  como  guía  para  un  diseño  extensible  orientado  a  objetos,  el  
patrón  de  diseño  de  la  estrategia  puede  considerarse  como  el  "concierto  de  celebridad"  de  este  importante  principio.  Aquí  está  la  declaración  
de  la  misión  de  este  patrón:

Defina  una  familia  de  algoritmos,  encapsule  cada  uno  y  hágalos  intercambiables.
La  estrategia  permite  que  el  algoritmo  varíe  independientemente  de  los  clientes  que  lo  utilicen.

—Erich  Gamma  et.  al.,  Patrones  de  diseño  [Gamma95]

Hacer  las  cosas  de  diferentes  maneras  es  un  requisito  común  en  el  diseño  de  software.  Solo  piense  en  ordenar  algoritmos  para  
listas.  Hay  varios  algoritmos  de  clasificación  que  tienen  diferentes  características  con  respecto  a  la  complejidad  del  tiempo  (número  de  
operaciones  requeridas)  y  la  complejidad  del  espacio  (espacio  de  almacenamiento  adicional  requerido  además  de  la  lista  de  entrada).  
Algunos  ejemplos  son  Bubble­Sort,  Quick­Sort,  Merge­Sort,  Insert­Sort  y  Heap­Sort.

Por  ejemplo,  Bubble­Sort  es  el  menos  complejo  y  es  muy  eficiente  en  cuanto  al  consumo  de  memoria,  pero  también  
uno  de  los  algoritmos  de  clasificación  más  lentos.  Por  el  contrario,  Quick­Sort  es  un  algoritmo  de  clasificación  rápido  y  eficiente  que  es  
fácil  de  implementar  a  través  de  su  estructura  recursiva  y  no  requiere  memoria  adicional,  pero  es  muy  ineficiente  con  listas  preordenadas  
e  invertidas.  Con  la  ayuda  del  patrón  de  estrategia,  se  puede  implementar  un  simple  intercambio  del  algoritmo  de  clasificación,  por  
ejemplo,  dependiendo  de  las  propiedades  de  la  lista  que  se  va  a  clasificar.

Consideremos  otro  ejemplo.  Supongamos  que  queremos  tener  una  representación  textual  de  una  instancia
de  una  clase  Cliente  en  un  sistema  de  TI  comercial  arbitrario.  Un  requisito  de  las  partes  interesadas  establece  que  la  representación  
textual  se  formateará  en  varios  formatos  de  salida:  como  texto  sin  formato,  como  XML  (lenguaje  de  marcado  extensible)  y  como  
JSON  (notación  de  objetos  de  JavaScript).

231
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Bien,  antes  que  nada,  presentemos  una  abstracción  para  nuestras  diversas  estrategias  de  formato,  la  clase  abstracta
Formateador:

Listado  9­14.  El  formateador  abstracto  contiene  todo  lo  que  todas  las  clases  específicas  de  formateador  tienen  en  común

#include  <memoria>  
#include  <cadena>  
#include  <vista_cadena>  
#include  <sstream>

formateador  de  clase  
{ public:  
virtual  ~Formatter()  =  predeterminado;

Formateador  y  withCustomerId(std::string_view  customerId)  {
this­>IdCliente  =  IdCliente;  devolver  
*esto; }

Formatter&  withForename(std::string_view  nombre)  {
este­>nombre  =  nombre;  devolver  
*esto; }

Formatter&  withSurname(std::string_view  apellido)  { this­>apellido  
=  apellido;  devolver  *esto; }

Formatter&  withStreet(std::string_view  street)  { this­>street  =  
street;  devolver  *esto; }

Formatter&  withZipCode(std::string_view  zipCode)  { this­>zipCode  
=  zipCode;  devolver  *esto; }

Formatter&  withCity(std::string_view  city)  { this­>city  =  
city;  devolver  *esto; }

virtual  std::string  format()  const  =  0;

protegido:  
std::string  customerId  { "000000" };  std::string  
nombre  { "n/a" };  std::string  apellido  { "n/
a" };  std::string  calle  { "n/a" };

232
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

std::string  código  postal  { "n/a" };  
std::string  city  { "n/a" };n };

usando  FormatterPtr  =  std::unique_ptr<Formatter>;

Los  tres  formateadores  específicos  que  proporcionan  los  estilos  de  formato  solicitados  por  las  partes  interesadas  
son  los  siguientes:

Listado  9­15.  Los  tres  formateadores  específicos  anulan  la  función  miembro  de  formato  virtual  puro  ()  de  formateador
#include  "Formatador.h"

clase  PlainTextFormatter:  Formateador  público  { público:  
virtual  
std::string  format()  const  override  { std::stringstream  
formattedString  { };  formattedString  <<  "["  <<  
IDcliente  <<  "]:  "  <<  apellido  <<  ",  "  <<  código  postal  <<
"  "
<<  nombre  <<
"  "

<<  calle  <<  ",  "  <<  
ciudad  <<  ".";
devuelve  formattedString.str(); } };

clase  XmlFormatter:  Formateador  público  
{ público:  
virtual  std::string  format()  const  override  { std::stringstream  
formattedString  { };  formattedString  <<  "<id  del  
cliente=\""  <<  ID  del  
cliente  <<  "\">\n"  <<
"  <nombre>"  <<  nombre  <<  "</nombre>\n"  <<  "  <apellido>"  
<<  apellido  <<  "</apellido>\n"  <<  "  <calle>"  <<  calle  <<  
"</calle>\n"  <<  "  <código  postal>"  <<  código  postal  
<<  "</código  postal>\n"  <<  "  <ciudad>"  <<  ciudad  <<  "</
ciudad>\n"  <<  "</cliente>\n";  devuelve  
formattedString.str(); } };

clase  JsonFormatter:  Formateador  público  { público:  
virtual  
std::string  format()  const  override  { std::stringstream  
formattedString  { };  Cadena  con  formato  <<  "{\n"  
<<

"  \"IdCliente :  \""  <<  IdCliente  <<  FIN_DE_PROPIEDAD  <<
"  \"Nombre:  \""  <<  nombre  <<  FIN_DE_PROPIEDAD  <<
"  \"Apellido:  \""  <<  apellido  <<  FIN_DE_PROPIEDAD  <<
"  \"Calle:  \""  <<  calle  <<  FIN_DE_PROPIEDAD  <<

233
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

"  \"Código  postal:  \""  <<  zipCode  <<  FIN_DE_PROPIEDAD  <<  "  
\"Ciudad:  \""  <<  ciudad  <<  "\"\n"  <<  "}\n";  
return  
formattedString.str  ( ) ; }

privado:  
static  constexpr  const  char*  const  END_OF_PROPERTY  { "\",\n" }; };

Como  se  puede  ver  claramente  aquí,  la  OCP  está  particularmente  bien  apoyada.  Tan  pronto  como  se  requiera  
un  nuevo  formato  de  salida,  solo  se  debe  implementar  otra  especialización  de  la  clase  abstracta  Formatter.  No  se  
requieren  modificaciones  a  los  formateadores  ya  existentes.

Listado  9­16.  Así  es  como  se  usa  el  objeto  del  formateador  pasado  dentro  de  la  función  miembro  
getAsFormattedString()
#include  "Dirección.h"  
#include  "Id.  de  Cliente.h"  
#include  "Formatador.h"

class  Customer  

{ public: // ...  std::string  getAsFormattedString(const  FormatterPtr&  formatter)  const  {
volver  formateador­>  
withCustomerId(customerId.toString()).  
withForename(nombre  de  
pila).  con  Apellido(apellido).  
withStreet(dirección.getStreet()).  
withZipCode(dirección.getZipCodeAsString()).  
withCity(dirección.getCity()).  

formato(); } // ...

privado:
ID  de  cliente  ID  de  cliente;  
std::string  nombre;  
std::string  apellido;
Dirección  Dirección; };

La  función  miembro  Customer::getAsFormattedString()  tiene  un  parámetro  que  espera  un  puntero  único  a  
un  objeto  formateador.  Este  parámetro  se  puede  usar  para  controlar  el  formato  de  la  cadena  que  se  puede  
recuperar  a  través  de  esta  función  miembro  o,  en  otras  palabras:  la  función  miembro  Customer::getAsFormatted  
String()  se  puede  proporcionar  con  una  estrategia  de  formato.
Por  cierto:  tal  vez  hayas  notado  el  diseño  especial  de  la  interfaz  pública  del  Formateador  con  sus  numerosas  
funciones  encadenadas  con...().  Aquí  también  se  ha  utilizado  otro  patrón  de  diseño,  que  se  llama  Fluent  Interface.  En  
la  programación  orientada  a  objetos,  una  interfaz  fluida  es  un  estilo  para  diseñar  API  de  manera  que  la  legibilidad  del  
código  sea  similar  a  la  de  la  prosa  escrita  ordinaria.  En  el  capítulo  anterior  sobre  Test  Driven  Development  (Capítulo  
8),  ya  vimos  una  interfaz  de  este  tipo.  Ahí  hemos  introducido  una  costumbre

234
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

aserción  (consulte  la  sección  "Pruebas  más  sofisticadas  con  una  aserción  personalizada")  para  escribir  pruebas  más  elegantes  y  legibles.  En  
nuestro  caso  aquí,  el  truco  es  que  cada  función  miembro  with...()  es  autorreferencial,  es  decir,  el  nuevo  contexto  para  llamar  a  una  función  
miembro  en  el  Formateador  es  equivalente  al  contexto  anterior,  a  menos  que  el  formato  final( )  se  llama  la  función.

Como  de  costumbre,  aquí  también  hay  una  visualización  gráfica  de  la  estructura  de  clases  de  nuestro  ejemplo  de  código,  un  diagrama  
de  clases  UML  (Figura  9­7):

Figura  9­7.  Una  estrategia  de  formateo  abstracta  y  sus  tres  estrategias  de  formateo  concretas

Como  es  fácil  de  ver,  el  patrón  de  estrategia  en  este  ejemplo  asegura  que  la  persona  que  llama  a  la  función  miembro  Cust  
omer::getAsFormattedString()  pueda  configurar  el  formato  de  salida  como  quiera.  ¿Quieres  admitir  otro  formato  de  salida?  No  hay  problema:  
gracias  al  excelente  soporte  del  Principio  Abierto­Cerrado,  se  puede  agregar  fácilmente  otra  estrategia  de  formato  concreta.  Las  otras  estrategias  
de  formato,  así  como  la  clase  Cliente,  no  se  ven  afectadas  por  esta  extensión.

Dominio
Los  sistemas  de  software  generalmente  tienen  que  realizar  una  variedad  de  acciones  debido  a  la  recepción  de  instrucciones.  Los  usuarios  de  
software  de  procesamiento  de  texto,  por  ejemplo,  emiten  una  variedad  de  comandos  al  interactuar  con  la  interfaz  de  usuario  del  software.
Quieren  abrir  un  documento,  guardar  un  documento,  imprimir  un  documento,  copiar  un  fragmento  de  texto,  pegar  un  fragmento  de  texto  
copiado,  etc.  Este  patrón  general  también  es  observable  en  otros  dominios.  Por  ejemplo,  en  el  mundo  financiero,  podría  haber  órdenes  de  un  
cliente  a  su  corredor  de  valores  para  comprar  acciones,  vender  acciones,  etc.  Y  en  un  dominio  más  técnico  como  la  fabricación,  los  comandos  
se  utilizan  para  controlar  instalaciones  y  máquinas  industriales.
Al  implementar  sistemas  de  software  controlados  por  comandos,  es  importante  asegurarse  de  que  la  solicitud  de  una  acción  esté  
separada  del  objeto  que  realmente  realiza  la  acción.  El  principio  rector  detrás  de  esto  es  el  acoplamiento  flexible  (consulte  el  Capítulo  3)  y  la  
separación  de  preocupaciones.
Una  buena  analogía  es  un  restaurante.  En  un  restaurante,  el  mesero  acepta  el  pedido  del  cliente,  pero  no  es  responsable  de  
cocinar  la  comida.  Esa  es  una  tarea  de  la  cocina  del  restaurante.  De  hecho,  es  incluso  transparente  para  el  cliente  cómo  se  prepara  la  
comida.  Tal  vez  el  restaurante  prepare  la  comida  él  mismo,  pero  la  comida  también  puede  ser  entregada  desde  otro  lugar.

235
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

En  el  desarrollo  de  software  orientado  a  objetos  existe  un  patrón  de  comportamiento  llamado  Comando  (sinónimo:  
Acción)  que  fomenta  este  tipo  de  desacoplamiento.  Su  declaración  de  misión  es  la  siguiente:

Encapsule  una  solicitud  como  un  objeto,  lo  que  le  permitirá  parametrizar  clientes  con  diferentes  solicitudes,  
poner  en  cola  o  registrar  solicitudes  y  admitir  operaciones  que  no  se  pueden  deshacer.

—Erich  Gamma  et.  al.,  Patrones  de  diseño  [Gamma95]

Un  buen  ejemplo  del  patrón  Comando  es  una  arquitectura  Cliente/Servidor,  donde  un  cliente,  el  llamado  Invocador ,  envía  
comandos  que  deben  ejecutarse  en  un  servidor,  al  que  se  hace  referencia  como  Receptor .
Comencemos  con  el  Comando  abstracto,  que  es  una  interfaz  simple  y  pequeña  que  se  ve  de  la  siguiente  manera:

Listado  9­17.  La  interfaz  de  comando

#include  <memoria>

clase  Comando  
{ público:  
virtual  ~Comando()  =  predeterminado;  
ejecución  de  vacío  virtual  ()  =  0; };

usando  CommandPtr  =  std::shared_ptr<Comando>;

También  introdujimos  un  alias  de  tipo  (CommandPtr)  para  un  puntero  inteligente  a  los  comandos.
Esta  interfaz  de  comando  abstracta  ahora  puede  implementarse  mediante  varios  comandos  concretos.  Déjanos  primero
eche  un  vistazo  a  un  comando  muy  simple,  la  salida  de  la  cadena  "¡Hola  mundo!":

Listado  9­18.  Una  primera  y  muy  sencilla  implementación  de  un  Comando  concreto

#incluir  <iostream>

class  HelloWorldOutputCommand :  public  Command  { public:  virtual  
void  
execute()  override  {
std::cout  <<  "¡Hola  mundo!"  <<  "\n";

} };

A  continuación,  necesitamos  el  elemento  que  acepta  y  ejecuta  los  comandos.  Este  elemento  se  llama  Receptor  en
la  descripción  general  de  este  patrón  de  diseño.  En  nuestro  caso  es  una  clase  llamada  Servidor  que  cumple  este  rol:

Listado  9­19.  El  receptor  de  comandos

#include  "Comando.h"

class  Server  
{ public:  
void  acceptCommand(const  CommandPtr&  command)  { command­
>execute(); } };

236
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Actualmente,  esta  clase  contiene  solo  una  función  miembro  pública  simple  que  puede  aceptar  y  ejecutar  comandos.

Finalmente,  necesitamos  el  llamado  Invoker,  que  es  la  clase  Cliente  en  nuestra  arquitectura  Cliente/Servidor:

Listado  9­20.  El  Cliente  envía  comandos  al  Servidor

clase  Cliente  
{ public:  
void  run()  
{ Servidor  theServer  { };  
CommandPtr  helloWorldOutputCommand  =  std::make_shared<HelloWorldOutputCommand>();  
elServidor.acceptCommand(helloWorldOutputCommand); } };

Dentro  de  la  función  main()  encontramos  el  siguiente  código  simple:

Listado  9­21.  La  función  principal()

#include  "Cliente.h"

int  main()  
{ Cliente  cliente  { };  
cliente.ejecutar();  
devolver  0;
}

Si  este  programa  ahora  se  está  compilando  y  ejecutando,  la  salida  "¡Hola  mundo!"  aparecerá  en  la  salida  estándar.
Bueno,  a  primera  vista,  esto  puede  parecer  poco  emocionante,  pero  lo  que  hemos  logrado  a  través  del  patrón  de  comando  es  
que  el  origen  y  el  envío  del  comando  están  desvinculados  de  su  ejecución.  Ahora  podemos  manejar  objetos  de  comando  así  
como  otros  objetos.
Dado  que  este  patrón  de  diseño  admite  muy  bien  el  Principio  Abierto­Cerrado  (OCP;  consulte  el  Capítulo  6),  también  es
muy  fácil  de  agregar  nuevos  comandos  con  modificaciones  menores  insignificantes  del  código  existente.  Por  ejemplo,  si  
queremos  forzar  al  servidor  a  esperar  un  cierto  tiempo,  podemos  simplemente  agregar  el  siguiente  comando  nuevo:

Listado  9­22.  Otro  comando  concreto  que  le  indica  al  servidor  que  espere

#include  "Command.h"  
#include  <crono>  
#include  <hilo>

clase  WaitCommand:  comando  público  { público:  
explícito  
WaitCommand  (const  sin  firmar  int  duración  en  milisegundos)  no  excepto :
duraciónEnMillisegundos{duraciónEnMillisegundos}  { };

virtual  void  execute()  override  
{ std::chrono::milliseconds  dur(durationInMilliseconds);  
std::this_thread::sleep_for(dur); }

privado:  
duración  int  sin  firmar  en  milisegundos  { 1000 }; };

237
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Ahora  podemos  usar  el  nuevo  WaitCommand  así:

Listado  9­23.  Nuestro  nuevo  WaitCommand  en  uso

clase  Cliente  
{ public:  
void  run()  {
Servidor  elServidor  { };  const  
unsigned  int  SERVER_DELAY_TIMESPAN  { 3000 };

CommandPtr  waitCommand  =  std::make_shared<WaitCommand>(SERVER_DELAY_TIMESPAN);  
elServidor.acceptCommand(waitCommand);

CommandPtr  helloWorldOutputCommand  =  std::make_shared<HelloWorldOutputCommand>();  
elServidor.acceptCommand(helloWorldOutputCommand); } };

Para  obtener  una  visión  general  de  la  estructura  que  se  ha  originado  hasta  ahora,  la  Figura  9­8  muestra  una
diagrama  de  clase  UML  correspondiente:

Figura  9­8.  El  servidor  solo  conoce  la  interfaz  de  comando,  pero  no  ningún  comando  concreto.

Como  se  puede  ver  en  este  ejemplo,  podemos  parametrizar  comandos  con  valores.  Dado  que  la  firma  de  la  función  
miembro  de  ejecución  virtual  pura  ()  está  especificada  como  sin  parámetros  por  la  interfaz  de  comando,  la  
parametrización  se  realiza  con  la  ayuda  de  un  constructor  de  inicialización.  Además,  no  tuvimos  que  cambiar  nada  en  el  
servidor  de  clase,  ya  que  pudo  tratar  y  ejecutar  el  nuevo  comando  de  inmediato.

El  patrón  Command  ofrece  múltiples  posibilidades  de  aplicaciones.  Por  ejemplo,  los  comandos  pueden
estar  en  cola  Esto  también  admite  una  ejecución  asíncrona  de  los  comandos:  el  invocador  envía  el  comando  y  luego  
puede  hacer  otras  cosas  de  inmediato,  pero  el  receptor  ejecuta  el  comando  en  un  momento  posterior.

Sin  embargo,  ¡falta  algo!  En  la  declaración  de  misión  citada  anteriormente  del  patrón  de  Comando,  puede  leer  algo  sobre  
"...apoyar  operaciones  que  no  se  pueden  deshacer".  Bueno,  la  siguiente  sección  está  dedicada  a  ese  tema.

238
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Procesador  de  comandos
En  nuestro  pequeño  ejemplo  de  una  arquitectura  Cliente/Servidor  de  la  sección  anterior,  hice  un  poco  de  trampa.  En  realidad,  un  
servidor  no  ejecutaría  los  comandos  de  esa  manera  como  lo  demostré  anteriormente.  El  comando
los  objetos  que  están  llegando  al  servidor  se  distribuirían  a  las  partes  internas  del  servidor  que  son  responsables  de  la  
ejecución  del  comando.  Esto  se  puede  hacer,  por  ejemplo,  con  la  ayuda  de  otro  patrón  que  se  llama  Cadena  de  responsabilidad  
(este  patrón  no  se  describe  en  este  libro).
Consideremos  otro  ejemplo  un  poco  más  complejo.  Supongamos  que  tenemos  un  programa  de  dibujo.
Los  usuarios  de  este  programa  pueden  dibujar  muchas  formas  diferentes,  por  ejemplo,  círculos  y  rectángulos.  Para  este  propósito,  los  
menús  correspondientes  están  disponibles  en  la  interfaz  de  usuario  del  programa  a  través  de  los  cuales  se  pueden  invocar  estas  
operaciones  de  dibujo.  Estoy  bastante  seguro  de  que  lo  ha  adivinado:  los  desarrolladores  de  software  bien  calificados  de  este  
programa  implementaron  el  patrón  de  Comando  para  realizar  estas  operaciones  de  dibujo.  Sin  embargo,  un  requisito  de  las  partes  
interesadas  establece  que  un  usuario  del  programa  también  puede  deshacer  las  operaciones  de  dibujo.
Para  cumplir  con  este  requisito,  necesitamos,  en  primer  lugar,  comandos  que  se  puedan  deshacer.

Listado  9­24.  La  interfaz  UndoableCommand  se  crea  combinando  Command  y  Revertable

#include  <memoria>

clase  Comando  
{ público:  
virtual  ~Comando()  =  predeterminado;  
ejecución  de  vacío  virtual  ()  =  0; };

class  Reversible  { public:  
virtual  
~Revertible()  =  predeterminado;  deshacer  vacío  
virtual  ()  =  0; };

class  UndoableCommand :  public  Command,  public  Reversible  { };

usando  CommandPtr  =  std::shared_ptr<UndoableCommand>;

De  acuerdo  con  el  Principio  de  Segregación  de  la  Interfaz  (ISP;  consulte  el  Capítulo  6),  hemos  agregado  otra  interfaz  
Reversible  que  admite  la  función  Deshacer.  Esta  nueva  interfaz  se  puede  combinar  con  la  interfaz  de  Comando  existente  mediante  la  
herencia  a  un  UndoableCommand.
Como  ejemplo  de  muchos  comandos  de  dibujo  diferentes  que  se  pueden  deshacer,  solo  muestro  el  comando  concreto  para  el  
círculo  aquí:

Listado  9­25.  Un  comando  que  se  puede  deshacer  para  dibujar  círculos

#incluye  "Comando.h"  
#incluye  "Procesador  de  dibujo.h"  #incluye  
"Punto.h"

clase  DrawCircleCommand :  public  UndoableCommand  { public:

DrawCircleCommand(DrawingProcessor&  receptor,  const  Point&  centerPoint,  const  doble  radio)  noexcept :  
receptor  { receptor },  centerPoint  { centerPoint },  
radio  { radio }  { }

239
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

virtual  void  execute()  override  
{ receptor.drawCircle(centerPoint,  radius); }

virtual  void  undo()  override  
{ receptor.eraseCircle(centerPoint,  radius); }

privado:
Procesador  de  dibujo  y  receptor;  
const  Punto  centroPunto;  const  
radio  doble; };

Es  fácil  imaginar  que  los  comandos  para  dibujar  un  rectángulo  y  otras  formas  se  parecen  mucho.
El  receptor  de  ejecución  del  comando  es  una  clase  llamada  DrawingProcessor,  que  es  el  elemento  que  realiza  las  
operaciones  de  dibujo.  Se  pasa  una  referencia  a  este  objeto  junto  con  otros  argumentos  durante  la  construcción  del  
comando  (ver  constructor  de  inicialización).  En  este  lugar  solo  muestro  un  pequeño  extracto  de  la  clase  probablemente  
compleja  DrawingProcessor,  porque  no  juega  un  papel  importante  para  la  comprensión  del  patrón:

Listado  9­26.  El  DrawingProcessor  es  el  elemento  que  realizará  las  operaciones  de  dibujo.

class  DrawingProcessor  { public:  
void  
drawCircle(const  Point&  centerPoint,  const  doble  radio)  {
//  Instrucciones  para  dibujar  un  círculo  en  la  pantalla...

void  eraseCircle(const  Point&  centerPoint,  const  doble  radio)  {
//  Instrucciones  para  borrar  un  círculo  de  la  pantalla...

// ...

Ahora  llegamos  a  la  pieza  central  de  este  patrón,  el  CommandProcessor:

Listado  9­27.  La  clase  CommandProcessor  administra  una  pila  de  objetos  de  comando  que  se  pueden  deshacer

#incluir  <pila>

class  CommandProcessor  
{ public:  
void  ejecutar(const  CommandPtr&  comando)  { comando­
>ejecutar();  
comandoHistorial.push(comando); }

240
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

void  undoLastCommand()  { if  
(commandHistory.empty())  { return; }  

commandHistory.top()­>undo();  
comandoHistorial.pop(); }

privado:  
std::stack<std::shared_ptr<Revertible>>  commandHistory; };

La  clase  CommandProcessor  (que,  por  cierto,  no  es  segura  para  subprocesos  cuando  se  utiliza  la  
implementación  anterior)  contiene  un  std::stack<T>  (definido  en  el  encabezado  <stack>),  que  es  un  tipo  de  datos  abstracto  que  
funciona  como  LIFO  (Last  ­Entrada  primero  en  salir).  Después  de  que  la  función  miembro  CommandProcessor::execute()  haya  
desencadenado  la  ejecución  de  un  comando,  el  objeto  del  comando  se  almacena  en  la  pila  commandHistory.  Al  llamar  a  la  función  
miembro  CommandProcessor::undoLastCommand(),  el  último  comando  almacenado  en  la  pila  se  deshace  y  luego  se  elimina  
de  la  parte  superior  de  la  pila.
Además,  la  operación  de  deshacer  ahora  se  puede  modelar  como  un  objeto  de  comando.  En  este  caso,  el  receptor  de  comandos  
es,  por  supuesto,  el  mismo  CommandProcessor:

Listado  9­28.  El  UndoCommand  solicita  al  CommandProcessor  que  realice  una  acción  de  deshacer

#include  "Comando.h"
#incluir  "CommandProcessor.h"

class  UndoCommand :  public  UndoableCommand  { public:  

undoCommand  explícito  (CommandProcessor&  receptor)  noexcept :  receptor  { receptor }  
{ }

virtual  void  execute()  override  
{ receptor.undoLastCommand(); }

deshacer  vacío  virtual  ()  anular  {
//  Se  dejó  en  blanco  intencionalmente,  porque  no  se  debe  deshacer  una  operación  de  deshacer. }

privado:
Procesador  de  comandos  y  receptor; };

241
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

¿Perdió  la  visión  general?  Bien,  una  vez  más  es  hora  de  un  “panorama  general”  en  la  forma  de  un  diagrama  de  clases  
UML  (Figura  9­9).

Figura  9­9.  El  CommandProcessor  (a  la  derecha)  ejecuta  los  comandos  que  recibe  y  gestiona  un  historial  de  comandos

Cuando  se  utiliza  el  patrón  de  comando  en  la  práctica,  a  menudo  se  enfrenta  a  la  necesidad  de  poder  componer  un  
comando  más  complejo  a  partir  de  varios  comandos  simples  o  grabar  y  reproducir  comandos  (secuencias  de  comandos).  Para  
poder  implementar  dichos  requisitos  de  una  manera  elegante,  el  siguiente  patrón  de  diseño  es  adecuado.

Compuesto
Una  estructura  de  datos  muy  utilizada  en  Ciencias  de  la  Computación  es  la  de  un  árbol.  Los  árboles  se  pueden  encontrar  en  todas  
partes.  Por  ejemplo,  la  organización  jerárquica  de  un  sistema  de  archivos  en  un  medio  de  datos  (por  ejemplo,  un  disco  duro)  se  
ajusta  a  la  de  un  árbol.  El  navegador  de  proyectos  de  un  entorno  de  desarrollo  integrado  (IDE)  suele  tener  una  estructura  de  
árbol.  En  el  diseño  de  compiladores,  el  árbol  de  sintaxis  abstracta  (AST)  es,  como  sugiere  su  nombre,  una  representación  en  
árbol  de  la  estructura  sintáctica  abstracta  del  código  fuente  que  suele  ser  el  resultado  de  la  fase  de  análisis  de  sintaxis  de  un  
compilador.
El  modelo  orientado  a  objetos  para  una  estructura  de  datos  en  forma  de  árbol  se  denomina  patrón  compuesto .  Este  patrón  tiene  
la  siguiente  intención:

Componga  objetos  en  estructuras  de  árbol  para  representar  jerarquías  de  parte­todo.  Composite  permite  a  
los  clientes  tratar  objetos  individuales  y  composiciones  de  objetos  de  manera  uniforme.

—Erich  Gamma  et.  al.,  Patrones  de  diseño  [Gamma95]

242
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Nuestro  ejemplo  anterior  de  las  secciones  Comando  y  Procesador  de  comandos  debe  ampliarse  con  la  posibilidad  
de  que  podamos  crear  comandos  compuestos  y  que  los  comandos  se  puedan  grabar  y  reproducir.  Entonces  
agregamos  una  nueva  clase  al  diseño  anterior,  un  ComandoCompuesto:

Listado  9­29.  Un  nuevo  UndoableCommand  concreto  que  gestiona  una  lista  de  comandos
#include  "Comando.h"  
#include  <vector>

class  CompositeCommand :  public  UndoableCommand  
{ public:  
void  addCommand(CommandPtr&  command)  
{ commands.push_back(command); }

virtual  void  ejecutar  ()  anular  {
for  (const  auto&  command :  commands)  {
comando­>ejecutar(); } }

virtual  void  undo()  override  { for  (const  
auto&  command :  commands)  { command­
>undo(); } }

privado:  
comandos  std::vector<CommandPtr>; };

El  comando  compuesto  tiene  una  función  miembro  addCommand(),  que  le  permite  agregar  comandos  a  una  
instancia  de  CompositeCommand.  Dado  que  la  clase  CompositeCommand  también  implementa  la  interfaz  
UndoableCommand,  sus  instancias  pueden  tratarse  como  comandos  ordinarios.  En  otras  palabras,  también  es  
posible  ensamblar  comandos  compuestos  con  otros  comandos  compuestos  jerárquicamente.  A  través  de  la  
estructura  recursiva  del  patrón  Composite,  puede  generar  árboles  de  comando.

243
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

El  siguiente  diagrama  de  clases  UML  (Figura  9­10)  muestra  el  diseño  extendido.

Figura  9­10.  Con  CompositeCommand  agregado  (a  la  izquierda),  los  comandos  ahora  se  pueden  programar

La  clase  CompositeCommand  recién  agregada  ahora  se  puede  usar,  por  ejemplo,  como  una  grabadora  de  macros  para
para  grabar  y  reproducir  secuencias  de  comandos:

Listado  9­30.  Nuestro  nuevo  CompositeCommand  en  acción  como  grabador  de  macros

int  principal()  {
Procesador  de  comandos  Procesador  de  comandos  { };
Procesador  de  dibujo  Procesador  de  dibujo  { };

auto  macroRecorder  =  std::make_shared<CompositeCommand>();

Punto  circuloCentroPunto  { 20,  20 };  CommandPtr  
drawCircleCommand  =  std::make_shared<DrawCircleCommand>(drawingProcessor,  circleCenterPoint,  10);  

commandProcessor.execute(drawCircleCommand);  
macroRecorder­>addCommand(dibujarCircleCommand);

Punto  rectánguloCentroPunto  { 30,  10 };  CommandPtr  
drawRectangleCommand  =  std::make_shared<DrawRectangleCommand>(drawingProcessor,  rectánguloCenterPoint,  5,  8);  

commandProcessor.execute(drawRectangleCommand);  
macroRecorder­>addCommand(dibujarRectangleCommand);

244
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

commandProcessor.execute(macroRecorder);

CommandPtr  undoCommand  =  std::make_shared<UndoCommand>(commandProcessor);  
commandProcessor.execute(deshacerComando);

devolver  0;
}

Con  la  ayuda  del  patrón  Composite,  ahora  es  muy  fácil  ensamblar  secuencias  de  comandos  complejas  a  
partir  de  comandos  simples  (estos  últimos  se  denominan  "hojas"  en  la  forma  canónica).  Dado  que  CompositeCommand  
también  implementa  la  interfaz  UndoableCommand,  se  pueden  usar  exactamente  como  los  comandos  simples.  Esto  simplifica  
enormemente  el  uso  a  través  del  código  del  cliente.
En  una  inspección  más  cercana  hay  una  pequeña  desventaja.  Es  posible  que  haya  notado  que  el  acceso  a  la  función  
miembro  CompositeCommand::addCommand()  solo  es  posible  si  usa  una  instancia  (macroRecorder)  del  tipo  concreto  
CompositeCommand  (consulte  el  código  fuente  anterior).  Esta  función  miembro  no  está  disponible  a  través  de  la  interfaz  
UndoableCommand.  En  otras  palabras,  ¡aquí  no  se  da  el  trato  igualitario  prometido  (recuerde  la  intención  del  patrón)  de  
compuestos  y  hojas!
Si  observa  el  patrón  compuesto  general  en  [Gamma95],  verá  que  las  funciones  administrativas  para  
administrar  elementos  secundarios  se  declaran  en  la  abstracción.  En  nuestro  caso,  sin  embargo,  esto  significaría  que  
tendríamos  que  declarar  un  addCommand()  en  la  interfaz  UndoableCommand  (lo  que  sería  una  violación  del  ISP,  por  cierto).  
La  consecuencia  fatal  sería  que  los  elementos  hoja  tendrían  que  anular  addCommand()  y  proporcionar  una  implementación  
significativa  para  esta  función  miembro.
¡Esto  no  es  posible!  ¿Qué  pasará,  por  favor,  que  no  viole  el  Principio  del  Mínimo  Asombro  (ver  Capítulo  3),  si  añadimos  
un  comando  a  una  instancia  de  DrawCircleCommand?
Si  hiciéramos  eso,  sería  una  violación  del  Principio  de  Sustitución  de  Liskov  (LSP;  consulte  el  Capítulo  6).
Por  lo  tanto,  es  mejor  hacer  una  compensación  en  nuestro  caso  y  prescindir  del  tratamiento  igualitario  de  composites  y  
hojas.

Observador
Un  patrón  de  arquitectura  bien  conocido  para  la  estructuración  de  sistemas  de  software  es  Model­View­Controller  (MVC).
Con  la  ayuda  de  este  patrón  de  arquitectura,  que  se  describe  en  detalle  en  el  libro  Pattern­Oriented  Software  Architecture  
[Busch96],  normalmente  se  estructura  la  parte  de  presentación  (interfaz  de  usuario)  de  una  aplicación.
El  principio  detrás  de  esto  es  la  Separación  de  preocupaciones  (SoC).  Entre  otras  cosas,  los  datos  a  mostrar,  que  se  mantienen  
en  el  llamado  modelo,  se  separan  de  las  múltiples  representaciones  visuales  (las  llamadas  vistas)  de  estos  datos.

En  MVC,  el  acoplamiento  entre  las  vistas  y  el  modelo  debe  ser  lo  más  flexible  posible.  Este  acoplamiento  suelto  
generalmente  se  realiza  con  el  patrón  Observer .  El  observador  es  un  patrón  de  comportamiento  que  se  describe  en  [Gamma95]  
y  tiene  la  siguiente  intención:

Defina  una  dependencia  de  uno  a  muchos  entre  objetos  para  que  cuando  un  objeto  cambie  de  estado,  
todos  sus  dependientes  sean  notificados  y  actualizados  automáticamente.

—Erich  Gamma  et.  al.,  Patrones  de  diseño  [Gamma95]

Como  de  costumbre,  el  patrón  se  puede  explicar  mejor  con  un  ejemplo.  Consideremos  una  aplicación  de  hoja  de  cálculo,  
que  es  un  componente  natural  de  muchas  suites  de  software  de  oficina.  En  una  aplicación  de  este  tipo,  los  datos  se  
pueden  mostrar  en  una  hoja  de  trabajo,  en  un  gráfico  circular  y  en  muchas  otras  formas  de  presentación;  las  llamadas  vistas.
Se  pueden  crear  diferentes  vistas  de  los  datos  y  también  cerrarlas  de  nuevo.

245
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

En  primer  lugar,  necesitamos  un  elemento  abstracto  para  las  vistas  que  se  llama  Observer.

Listado  9­31.  El  observador  abstracto

#include  <memoria>

class  Observer  
{ público:  
virtual  ~Observer()  =  predeterminado;  
getId  int  virtual  ()  =  0;  
actualización  de  vacío  virtual  ()  =  
0; };

usando  ObserverPtr  =  std::shared_ptr<Observador>;

Los  Observadores  observan  a  un  llamado  Sujeto.  Para  ello,  podrán  ser  registrados  en  el  Sujeto,  y  también  
podrán  ser  dados  de  baja.

Listado  9­32.  Los  observadores  se  pueden  agregar  y  eliminar  de  un  llamado  Sujeto
#include  "Observer.h"  
#include  <algoritmo>  
#include  <vector>

class  IsEqualTo  final  { público:  
explícito  
IsEqualTo(const  ObserverPtr&  Observer) :
observador  { observador }  { }  
operador  bool()(const  ObserverPtr&  observadorParaComparar)  {
return  observadorParaComparar­>getId()  ==  observador­>getId(); }

privado:
observadorPtr  observador; };

class  Asunto  
{ public:  
void  addObserver(ObserverPtr&  observerToAdd)  { auto  iter  
=  std::find_if(begin(observadores),  end(observadores),  
IsEqualTo(observerToAdd));  if  
(iter  ==  end(observadores))  
{ observadores.push_back(observerToAdd); }

void  removeObserver(ObserverPtr&  ObserverToRemove)  {
observadores.erase(std::remove_if(begin(observadores),  end(observadores),  
IsEqualTo(observerToRemove)),  end(observadores));
}

246
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

protegido:  
void  notificar  a  Todos  los  Observadores()  
const  { for  (const  auto&  observador :  observadores)  
{ observador­

>actualizar(); } }

privado:  
std::vector<ObserverPtr>  observadores; };

Además  de  la  clase  Sujeto,  también  se  define  un  Functor  llamado  IsEqualTo  (ver  Capítulo  7  about  Functors),  
que  se  usa  para  comparaciones  al  agregar  y  eliminar  observadores.  El  Functor  compara  los  ID  del  Observer.  También  
sería  concebible  que  compare  las  direcciones  de  memoria  de  las  instancias  de  Observer.  Entonces  incluso  sería  
posible  que  varios  observadores  del  mismo  tipo  se  registraran  en  el  Sujeto.
El  núcleo  es  la  función  de  miembro  notificar  a  Todos  los  Observadores().  Está  protegido  ya  que  está  destinado  a  
ser  llamado  por  los  Sujetos  concretos  que  se  heredan  de  éste.  Esta  función  itera  sobre  todos  los  observadores  
registrados  y  llama  a  su  función  miembro  update().
Veamos  un  tema  concreto,  el  modelo  de  hoja  de  cálculo.

Listado  9­33.  El  modelo  de  hoja  de  cálculo  es  un  sujeto  concreto

#incluir  "Asunto.h"  #incluir  
<iostream>
#incluir  <vista_cadena>

class  SpreadsheetModel :  public  Subject  { public:  
void  
changeCellValue(std::string_view  column,  const  int  row,  const  double  value)  { std::cout  <<  "Celda  ["  <<  
"
columna  <<  ",  "  <<  fila  <<  " ]  = //  Cambiar  el  valor  de  una  celda  de  hoja   <<  valor  <<  std::endl;
de  cálculo  y  luego...  notificar  a  Todos  los  Observadores(); } };

Esto,  por  supuesto,  es  solo  un  mínimo  absoluto  de  un  modelo  de  hoja  de  cálculo.  Solo  sirve  para  explicar  
el  principio  funcional  del  patrón.  Lo  único  que  puede  hacer  aquí  es  llamar  a  una  función  miembro  que  llama  a  la  función  
notificar  a  Todos  los  Observadores()  heredada.
Los  tres  observadores  concretos  de  nuestro  ejemplo  que  implementan  la  función  miembro  update()  del
La  interfaz  del  observador  son  las  tres  vistas  TableView,  BarChartView  y  PieChartView.

Listado  9­34.  Tres  vistas  concretas  implementan  la  interfaz  abstracta  de  Observer

#incluye  "Observador.h"  
#incluye  "Modelo  de  hoja  de  cálculo.h"

class  TableView:  observador  público  { público:  

TableView  explícito  (Modelo  de  hoja  de  cálculo  y  el  modelo):  
modelo  { el  modelo}  {}  virtual  
int  getId  ()  invalidar  { return  1;

247
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

anular  la  actualización  virtual  del  vacío  ()  {
std::cout  <<  "Actualización  de  TableView".  <<  estándar::endl; }

privado:
Modelo  de  hoja  de  cálculo  y  
modelo; };

class  BarChartView:  public  Observer  { public:  
explicit  
BarChartView  (SpreadsheetModel&  theModel) :  model  { theModel }  
{ }  virtual  int  getId()  override  
{ return  2;

virtual  void  update()  override  { std::cout  
<<  "Actualización  de  BarChartView".  <<  estándar::endl; }

privado:
Modelo  de  hoja  de  cálculo  y  
modelo; };

clase  PieChartView:  public  Observer  { público:  
explícito  
PieChartView  (SpreadsheetModel&  theModel) :  model  { theModel }  
{ }  virtual  int  getId()  override  
{ return  3;

virtual  void  update()  override  { std::cout  
<<  "Actualización  de  PieChartView".  <<  estándar::endl; }

privado:
Modelo  de  hoja  de  cálculo  y  
modelo; };

Creo  que  es  hora  de  volver  a  mostrar  una  visión  general  en  forma  de  diagrama  de  clases.  La  figura  9­11  
muestra  la  estructura  (clases  y  dependencias)  que  ha  surgido.

248
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Figura  9­11.  Cuando  se  cambia  el  modelo  de  hoja  de  cálculo,  notifica  a  todos  sus  observadores

En  la  función  main()  ahora  usamos  el  modelo  de  hoja  de  cálculo  y  las  tres  vistas  de  la  siguiente  manera:

Listado  9­35.  Nuestro  modelo  de  hoja  de  cálculo  y  las  tres  vistas  ensambladas  y  en  acción

#include  "SpreadsheetModel.h"  #include  
"SpreadsheetViews.h"

int  principal()  {
Modelo  de  hoja  de  cálculo  modelo  de  hoja  de  cálculo  { };

ObserverPtr  observador1  =  std::make_shared<TableView>(spreadsheetModel);  modelo  de  hoja  de  
cálculo.addObserver(observador1);

ObserverPtr  observador2  =  std::make_shared<BarChartView>(spreadsheetModel);  modelo  de  hoja  de  
cálculo.addObserver(observador2);

modelo  de  hoja  de  cálculo.cambiarValorCelda("A",  1,  42);

modelo  de  hoja  de  cálculo.removeObserver(observador1);

modelo  de  hoja  de  cálculo.cambiarValorCelda("B",  2,  23.1);

ObserverPtr  observador3  =  std::make_shared<PieChartView>(spreadsheetModel);  modelo  de  hoja  de  
cálculo.addObserver(observador3);

modelo  de  hoja  de  cálculo.cambiarValorCelda("C",  3,  3.1415926);

devolver  0; }

249
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Después  de  compilar  y  ejecutar  el  programa,  vemos  lo  siguiente  en  la  salida  estándar:

Celda  [A,  1]  =  42  
Actualización  de  TableView.
Actualización  de  BarChartView.
Celda  [B,  2]  =  23.1  
Actualización  de  BarChartView.
Celda  [C,  3]  =  3.14153  
Actualización  de  BarChartView.
Actualización  de  PieChartView.

Además  de  la  característica  positiva  del  acoplamiento  débil  (el  Sujeto  concreto  no  sabe  nada  acerca  de  los  Observadores),  este  
patrón  también  apoya  muy  bien  el  Principio  Abierto­Cerrado.  Se  pueden  agregar  muy  fácilmente  nuevos  observadores  concretos  (en  nuestro  
caso,  nuevas  vistas)  ya  que  no  es  necesario  ajustar  ni  cambiar  nada  en  las  clases  existentes.

Fábricas
De  acuerdo  con  el  principio  de  Separación  de  preocupaciones  (SoC),  la  creación  o  adquisición  de  objetos  debe  estar  separada  de  
las  tareas  específicas  del  dominio  que  tiene  un  objeto.  El  patrón  de  inyección  de  dependencia  discutido  anteriormente  sigue  este  principio  
de  una  manera  directa,  porque  todo  el  proceso  de  creación  de  objetos  está  centralizado  en  un  elemento  de  infraestructura  y  los  
objetos  no  tienen  que  preocuparse  por  eso.
Pero,  ¿qué  haremos  si  se  requiere  que  un  objeto  se  cree  dinámicamente  en  algún  punto  en
tiempo  de  ejecución?  Bueno,  esta  tarea  puede  ser  asumida  por  una  fábrica  de  objetos.
El  patrón  de  diseño  de  fábrica  es  básicamente  relativamente  simple  y  aparece  en  las  bases  de  código  de  muchas  maneras  diferentes.
formas  y  variedades.  Además  del  principio  SoC,  también  se  admite  en  gran  medida  la  ocultación  de  información  (consulte  el  Capítulo  3),  
porque  el  proceso  de  creación  de  una  instancia  debe  ocultarse  a  sus  usuarios.
Como  ya  se  ha  dicho,  las  fábricas  se  pueden  encontrar  en  innumerables  formas  y  variantes.  Discutimos  solo  una  variante  simple.

Fábrica  sencilla
La  implementación  probablemente  más  simple  de  una  fábrica  se  ve  así  (tomamos  el  ejemplo  de  registro  del
sección  DI  anterior):

Listado  9­36.  Probablemente  la  fábrica  de  objetos  más  simple  imaginable

#incluye  "LoggingFacility.h"  #incluye  
"StandardOutputLogger.h"

class  LoggerFactory  { público:  
registrador  
estático  crear  ()  {
return  std::make_shared<StandardOutputLogger>(); } };

250
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

El  uso  de  esta  fábrica  muy  simple  se  ve  de  la  siguiente  manera:

Listado  9­37.  Uso  de  LoggerFactory  para  crear  una  instancia  de  Logger

#include  "LoggerFactory.h"

int  main()  
{ Registrador  registrador  =  
LoggerFactory::create(); // ...registrar  
algo...  return  
0; }

Tal  vez  te  preguntes  ahora  si  vale  la  pena  gastar  una  clase  extra  para  una  tarea  tan  insignificante.  Bueno,  tal  vez  no.  Es  
más  sensato,  si  la  fábrica  pudiera  crear  varios  registradores  y  decidiera  de  qué  tipo  será.  Esto  se  puede  hacer,  por  ejemplo,  
leyendo  y  evaluando  un  archivo  de  configuración,  o  se  lee  una  determinada  clave  de  la  base  de  datos  del  Registro  de  
Windows.  También  es  imaginable  que  el  tipo  de  objeto  generado  dependa  de  la  hora  del  día.  Las  posibilidades  son  infinitas.  
Es  importante  que  esto  sea  completamente  transparente  para  la  clase  de  cliente.  Entonces,  aquí  hay  un  LoggerFactory  un  
poco  más  sofisticado  que  lee  un  archivo  de  configuración  (por  ejemplo,  desde  el  disco  duro)  y  decide  sobre  la  configuración  
actual,  qué  registrador  específico  se  crea:

Listado  9­38.  Una  fábrica  más  sofisticada  que  lee  y  evalúa  un  archivo  de  configuración

#include  "LoggingFacility.h"  #include  
"StandardOutputLogger.h"  #include  
"FilesystemLogger.h"

#include  <fstream>  
#include  <cadena>  
#include  <vista_cadena>

class  LoggerFactory  { privado:  
enum  
class  OutputTarget:  int  {
SALIDA  ESTÁNDAR,

ARCHIVO };

public:  
explícito  LoggerFactory(std::string_view  nombre  del  archivo  de  configuración) :  nombre  
del  archivo  de  configuración  { nombre  del  archivo  de  configuración }  { }

Registrador  crear  ()  const  {
const  std::string  ConfigurationFileContent  =  readConfigurationFile();
OutputTarget  outputTarget  =  evaluarConfiguración(configuraciónArchivoContenido);  return  
createLogger(objetivo  de  salida); }

privado:  
std::string  readConfigurationFile()  const  {
std::ifstream  filestream(configurationFileName);  return  
std::string(std::istreambuf_iterator<char>(filestream),  std::istreambuf_iterator<char>()); }

251
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

OutputTarget  evaluarConfiguración(std::string_view  configuraciónArchivoContenido)  const  {
//  Evaluar  el  contenido  del  archivo  de  configuración...  return  
OutputTarget::STDOUT; }

Logger  createLogger(OutputTarget  outputTarget)  const  { switch  
(outputTarget)  { case  
OutputTarget::FILE:  return  
std::make_shared<FilesystemLogger>();  case  
OutputTarget::STDOUT:  

predeterminado:  return  std::make_shared<StandardOutputLogger>(); }

const  std::string  nombre  del  archivo  de  configuración; };

El  diagrama  de  clases  UML  de  la  figura  9­12  muestra  la  estructura  que  conocemos  básicamente  de  la  sección  
sobre  inyección  de  dependencias  (figura  9­5),  pero  ahora  con  nuestra  LoggerFactory  simple  en  lugar  de  un  ensamblador.

Figura  9­12.  El  Cliente  utiliza  una  LoggerFactory  para  obtener  Loggers  concretos

Una  comparación  de  este  diagrama  con  la  figura  9­5  muestra  una  diferencia  significativa:  mientras  que  
la  clase  CustomerRepository  no  depende  del  Ensamblador,  el  Cliente  "conoce"  la  clase  de  fábrica  cuando  usa  el  patrón  
Factory.  Presumiblemente,  esta  dependencia  no  es  un  problema  grave,  pero  deja  claro  una  vez  más  que  un  
acoplamiento  flojo  se  lleva  al  máximo  con  Dependency  Injection.

252
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Fachada
El  patrón  de  fachada  es  un  patrón  estructural  que  se  usa  a  menudo  a  nivel  arquitectónico  y  tiene  la  siguiente  intención:

Proporcionar  una  interfaz  unificada  a  un  conjunto  de  interfaces  en  un  subsistema.  Facade  define  una  interfaz  
de  nivel  superior  que  hace  que  el  subsistema  sea  más  fácil  de  usar.

—Erich  Gamma  et.  al.,  Patrones  de  diseño  [Gamma95]

La  estructuración  de  un  gran  sistema  de  software  de  acuerdo  con  los  principios  de  Separación  de  preocupaciones,  Principio  de  
responsabilidad  única  (consulte  el  Capítulo  6)  y  Ocultación  de  información  (consulte  el  Capítulo  3)  generalmente  tiene  como  
resultado  que  se  originen  algún  tipo  de  componentes  o  módulos  más  grandes.  En  general,  estos  componentes  o  módulos  a  veces  
se  pueden  denominar  "subsistemas".  Incluso  en  una  arquitectura  en  capas,  las  capas  individuales  se  pueden  considerar  como  subsistemas.
Para  promover  la  encapsulación,  la  estructura  interna  de  un  componente  o  subsistema  debe  estar  oculta  para  sus  
clientes  (consulte  Ocultación  de  información  en  el  Capítulo  3).  La  comunicación  entre  subsistemas  y,  por  lo  tanto,  la  cantidad  
de  dependencias  entre  ellos,  debe  minimizarse.  Sería  fatal  que  los  clientes  de  un  subsistema  deban  conocer  detalles  sobre  su  
estructura  interna  y  la  interacción  de  sus  partes.
Una  fachada  regula  el  acceso  a  un  subsistema  complejo  al  proporcionar  una  interfaz  simple  y  bien  definida  para  los  clientes.  
Cualquier  acceso  al  subsistema  deberá  realizarse  únicamente  por  encima  de  la  Fachada.
El  siguiente  diagrama  UML  (Figura  9­13)  muestra  un  subsistema  llamado  Facturación  para  preparar  facturas.
Su  estructura  interna  consta  de  varias  partes  interconectadas.  Los  clientes  del  subsistema  no  pueden  acceder  a  estas  partes  
directamente.  Deben  utilizar  Facade  BillingService,  que  está  representado  por  un  Puerto  UML  (estereotipo  «fachada»)  en  el  borde  del  
subsistema.

Figura  9­13.  El  subsistema  de  facturación  proporciona  un  servicio  de  facturación  de  fachada  como  un  punto  de  acceso  para  los  clientes

En  C++,  y  también  en  otros  lenguajes,  una  fachada  no  es  nada  especial.  A  menudo  es  solo  una  clase  simple  que
está  recibiendo  llamadas  en  su  interfaz  pública  y  las  reenvía  a  la  estructura  interna  del  subsistema.
A  veces  es  solo  un  simple  reenvío  de  una  llamada  a  uno  de  los  elementos  estructurales  internos  del  subsistema,  pero  ocasionalmente  
un  Fachada  también  realiza  conversiones  de  datos,  entonces  también  es  un  Adaptador  (ver  la  sección  sobre  Adaptador).

253
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

En  nuestro  ejemplo,  la  clase  Facade  BillingService  implementa  dos  interfaces,  representadas  por  UML
notación  de  bola.  De  acuerdo  con  el  Principio  de  Segregación  de  Interfaz  (ISP;  ver  Capítulo  6),  la  configuración  del  subsistema  
Facturación  (interfaz  Configuración)  está  separada  de  la  generación  de  facturas  (interfaz  InvoiceCreation).  Por  lo  tanto,  Facade  
debe  anular  las  operaciones  que  se  declaran  en  ambas  interfaces.

Money  Class  Si  la  alta  

precisión  es  importante,  debe  evitar  los  valores  de  coma  flotante.  Las  variables  de  punto  flotante  de  tipo  float,  double  o  long  double  ya  
fallan  en  adiciones  simples,  como  lo  demuestra  este  pequeño  ejemplo:

Listado  9­39.  Al  sumar  10  números  de  coma  flotante  de  esta  manera,  es  posible  que  el  resultado  no  sea  lo  suficientemente  preciso

#incluye  <afirmación.h>  
#incluye  <iostream>

int  principal()  {

doble  suma  =  0.0;  
sumando  doble  =  0,3;

for  (int  i  =  0;  i  <  10;  i++)  { suma  =  suma  +  
sumando; };

afirmar  (suma  ==  3.0);  
devolver  0; }

Si  compila  y  ejecuta  este  pequeño  programa,  esto  es  lo  que  verá  como  resultado  de  la  consola:

Aserción  fallida:  suma  ==  3.0,  archivo ..\main.cpp,  línea  13

Creo  que  la  causa  de  esta  desviación  es  generalmente  conocida.  Los  números  de  coma  flotante  se  almacenan  
internamente  en  formato  binario.  Debido  a  esto  es  imposible  almacenar  un  valor  de  0.3  (y  otros)  precisamente  en  una  variable  de  tipo  
float,  double  o  long  double,  porque  no  tiene  una  representación  exacta  de  longitud  finita  en  binario.
En  decimal,  tenemos  un  problema  similar.  No  podemos  representar  el  valor  1/3  (un  tercio)  usando  solo  notación  decimal.  
0.33333333  no  es  completamente  exacto.
Hay  varias  soluciones  para  este  problema.  Para  las  monedas,  puede  ser  un  enfoque  adecuado  almacenar  el  valor  del  
dinero  en  un  número  entero  con  la  precisión  requerida,  por  ejemplo,  $12,45  se  almacenará  como  1245.  Si  los  requisitos  no  son  
muy  altos,  un  número  entero  puede  ser  una  solución  factible.  Tenga  en  cuenta  que  el  estándar  C++  no  especifica  el  tamaño  de  los  
tipos  integrales  en  bytes;  por  lo  tanto,  debe  tener  cuidado  con  cantidades  muy  grandes  ya  que  puede  ocurrir  un  desbordamiento  de  
enteros.  En  caso  de  duda,  se  debe  utilizar  un  número  entero  de  64  bits,  ya  que  puede  contener  grandes  cantidades  de  dinero.

254
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

DETERMINACIÓN  DEL  RANGO  DE  UN  TIPO  ARITMÉTICO

Los  rangos  específicos  de  la  implementación  real  para  los  tipos  aritméticos  (ya  sea  enteros  o  de  punto  flotante)  se  
pueden  encontrar  como  plantillas  de  clase  en  el  encabezado  <límites>.  Por  ejemplo,  así  es  como  encontrará  el  
rango  máximo  para  int:

#include  <límites>  
constexpr  auto  INT_LOWER_BOUND  =  std::numeric_limits<int>::min();  constexpr  auto  
INT_UPPER_BOUND  =  std::numeric_limits<int>::max();

Otro  enfoque  popular  es  proporcionar  una  clase  especial  para  este  propósito,  la  llamada  Clase  de  Dinero:

Proporcione  una  clase  para  representar  cantidades  exactas  de  dinero.  Una  Clase  de  Dinero  maneja  diferentes  
monedas  y  cambios  entre  ellas.

—Martin  Fowler,  Patrones  de  arquitectura  de  aplicaciones  empresariales  [Fowler02]

Figura  9­14.  Una  clase  de  dinero

El  patrón  Money  Class  es  básicamente  una  clase  que  encapsula  una  cantidad  financiera  y  su  moneda,  pero  tratar  
con  dinero  es  solo  un  ejemplo  de  esta  categoría  de  clases.  Hay  muchas  otras  propiedades,  o  dimensiones,  que  deben  
ser  representadas  con  precisión,  por  ejemplo,  medidas  precisas  en  física  (Tiempo,  Voltaje,  Corriente,  Distancia,  Masa,  
Frecuencia,  Cantidad  de  sustancias…).

255
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

1991:  MISIL  PATRIOT  DESCONTINUADO

MIM­104  Patriot  es  un  sistema  de  misiles  tierra­aire  (SAM)  que  fue  diseñado  y  fabricado  por  Raytheon  Company  de  
los  Estados  Unidos.  Su  aplicación  típica  es  contrarrestar  misiles  balísticos  tácticos  de  gran  altitud,  misiles  de  crucero  y  
aeronaves  avanzadas.  Durante  la  primera  Guerra  del  Golfo  Pérsico  (1990  ­  1991),  también  conocida  como  operación  
"Tormenta  del  Desierto",  Patriot  se  utilizó  para  derribar  misiles  balísticos  de  corto  alcance  SCUD  iraquíes  o  Al  Hussein  
entrantes.

El  25  de  febrero  de  1991,  una  batería  en  Dhahran,  una  ciudad  ubicada  en  la  provincia  oriental  de  Arabia  Saudita,  no  pudo  
interceptar  un  SCUD.  El  misil  impactó  en  un  cuartel  del  Ejército  y  provocó  28  muertos  y  98  heridos.

Un  informe  de  investigación  [GAOIMTEC92]  reveló  que  la  causa  de  esta  falla  fue  un  cálculo  inexacto  del  tiempo  
transcurrido  desde  que  se  encendió  el  sistema  debido  a  errores  aritméticos  de  la  computadora.  Para  que  los  misiles  de  
Patriot  puedan  detectar  y  alcanzar  el  objetivo  después  del  lanzamiento,  deben  aproximarse  espacialmente  al  objetivo,  
también  conocido  como  "puerta  de  alcance".  Para  predecir  dónde  aparecerá  el  objetivo  a  continuación  (el  llamado  
ángulo  de  deflexión),  se  deben  realizar  algunos  cálculos  con  el  tiempo  del  sistema  y  la  velocidad  de  vuelo  del  
objetivo.  El  tiempo  transcurrido  desde  el  inicio  del  sistema  se  midió  en  décimas  de  segundo  y  se  expresó  como  un  número  
entero.  La  velocidad  del  objetivo  se  midió  en  millas  por  segundo  y  se  expresó  como  un  valor  decimal.  Para  calcular  la  
"puerta  de  rango",  el  valor  del  temporizador  del  sistema  debe  multiplicarse  por  1/10  para  obtener  el  tiempo  en  segundos.
Este  cálculo  se  realizó  utilizando  registros  que  tienen  solo  24  bits  de  longitud.

El  problema  era  que  el  valor  de  1/10  en  decimal  no  se  puede  representar  con  precisión  en  un  registro  de  24  bits.  
El  valor  se  cortó  en  24  bits  después  del  punto  de  raíz.  La  consecuencia  fue  que  la  conversión  de  tiempo  de  un  
número  entero  a  un  número  real  da  como  resultado  una  pequeña  pérdida  de  precisión  que  provoca  un  cálculo  de  tiempo  
menos  preciso.  Este  error  de  precisión  probablemente  no  habría  sido  un  problema  si  el  sistema  solo  hubiera  estado  en  
funcionamiento  durante  unas  pocas  horas,  de  acuerdo  con  su  concepto  de  funcionamiento  como  sistema  móvil.
Pero  en  este  caso,  el  sistema  ha  estado  funcionando  durante  más  de  100  horas.  El  número  que  representaba  el  tiempo  
de  actividad  del  sistema  era  bastante  grande.  Esto  significó  que  el  pequeño  error  de  conversión  de  1/10  en  su  
representación  decimal  de  24  bits  resultó  en  un  gran  error  de  desviación  de  casi  medio  segundo.  Un  misil  SCUD  
iraquí  viaja  aprox.  800  metros  en  este  lapso  de  tiempo,  lo  suficientemente  lejos  como  para  estar  fuera  de  la  "puerta  de  
alcance"  de  un  misil  Patriot  que  se  aproxima.

Si  bien  el  manejo  preciso  de  cantidades  de  dinero  es  un  caso  muy  común  en  muchos  sistemas  de  TI  
empresariales,  tendrá  que  luchar  en  vano  para  encontrar  una  clase  de  dinero  en  la  mayoría  de  las  bibliotecas  de  
clases  base  de  C++  convencionales.  ¡Pero  no  reinventes  la  rueda!  Hay  multitud  de  implementaciones  diferentes  de  
C++  Money  Class,  solo  pregúntele  al  motor  de  búsqueda  de  su  confianza  y  obtendrá  miles  de  resultados.  Como  sucede  
a  menudo,  una  implementación  no  satisface  todos  los  requisitos.  La  clave  es  comprender  el  dominio  de  su  
problema.  Al  elegir  (o  diseñar)  una  Money  Class,  puede  considerar  varias  limitaciones  y  requisitos.  Aquí  hay  algunas  
preguntas  que  quizás  deba  aclarar  primero:

•¿Cuál  es  el  rango  completo  de  valores  a  manejar  (mínimo,  máximo)?

•¿Qué  reglas  de  redondeo  se  aplican?  Existen  leyes  o  prácticas  nacionales  para  los  redondeos  en
algunos  paises.

•¿Existen  requisitos  legales  para  la  precisión?

256
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

•  ¿Qué  normas  se  deben  tener  en  cuenta  (p.  ej.,  la  norma  internacional  ISO  4217  para  códigos  de  
moneda)?

•¿Cómo  se  mostrarán  los  valores  al  usuario?

•¿Con  qué  frecuencia  tendrá  lugar  la  conversión?

Desde  mi  perspectiva,  es  absolutamente  esencial  tener  una  cobertura  de  prueba  unitaria  del  100  %  (consulte  el  Capítulo  
2  sobre  pruebas  unitarias)  para  una  clase  de  dinero  para  verificar  si  la  clase  está  funcionando  como  se  esperaba  en  todas  las  
circunstancias.  Por  supuesto,  Money  Class  tiene  un  pequeño  inconveniente  en  comparación  con  la  representación  numérica  
pura  con  un  número  entero:  pierde  una  pizca  de  rendimiento.  Esto  podría  ser  un  problema  en  algunos  sistemas.  Pero  estoy  
convencido  de  que  en  la  mayoría  de  los  casos  predominarán  las  ventajas  (siempre  ten  en  cuenta  que  la  optimización  prematura  es  mala).

Objeto  de  caso  especial  (objeto  nulo)
En  la  sección  “Don't  Pass  or  Return  0  (NULL,  nullptr)”  en  el  Capítulo  4  aprendimos  que  devolver  un  nullptr  desde  una  función  o  
método  es  malo  y  debe  evitarse.  Allí  también  discutimos  varias  estrategias  para  evitar  punteros  regulares  (en  bruto)  en  un  programa  
C++  moderno.  En  la  sección  “Una  excepción  es  una  excepción,  ¡literalmente!”  en  el  Capítulo  5  aprendimos  que  las  excepciones  solo  
deben  usarse  para  casos  excepcionales  reales  y  no  con  el  propósito  de  controlar  el  flujo  normal  del  programa.

La  pregunta  abierta  e  interesante  ahora  es  esta:  ¿Cómo  tratamos  esos  casos  especiales,  que  no  son  excepciones  reales  
(por  ejemplo,  una  asignación  de  memoria  fallida),  sin  usar  un  nullptr  no  semántico  u  otros  valores  extraños?
Retomemos  de  nuevo  nuestro  ejemplo  de  código,  que  hemos  visto  varias  veces  antes:  la  consulta  de  un  Cliente  por  su  nombre.

Listado  9­40.  Un  método  de  búsqueda  de  clientes  por  nombre

Customer  CustomerService::findCustomerByName(const  std::string&  name)  { //  Código  que  busca  al  
cliente  por  nombre... // ...pero  ¿qué  hacemos  si  no  existe  un  
cliente  con  el  nombre  dado? }

Bueno,  una  posibilidad  sería  devolver  listas  siempre  en  lugar  de  una  sola  instancia.  Si  la  lista  devuelta  es
vacío,  el  objeto  comercial  consultado  no  existe:

Listado  9­41.  Una  alternativa  a  nullptr:  devolver  una  lista  vacía  si  falla  la  búsqueda  de  un  cliente

#include  "Cliente.h"  #include  
<vector>

utilizando  CustomerList  =  std::vector<Cliente>;

CustomerList  CustomerService::findCustomerByName(const  std::string&  name)  {
//  Código  que  busca  el  cliente  por  nombre... // ...y  si  no  existe  un  
cliente  con  el  nombre  dado:  return  CustomerList(); }

La  lista  devuelta  ahora  se  puede  consultar  en  la  siguiente  secuencia  del  programa  si  está  vacía.  Pero  que
la  semántica  tiene  una  lista  vacía?  ¿Fue  un  error  responsable  del  vacío  de  la  lista?  Bueno,  la  función  miembro  
std::vector<T>::empty()  no  puede  responder  esta  pregunta.  Estar  vacío  es  un  estado  de  una  lista,  pero  este  estado  no  tiene  una  
semántica  específica  de  dominio.

257
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Amigos,  sin  duda,  esta  solución  es  mucho  mejor  que  devolver  un  nullptr,  pero  tal  vez  no  sea  lo  suficientemente  bueno
en  algunos  casos.  Lo  que  sería  mucho  más  cómodo  es  un  valor  de  retorno  que  se  pueda  consultar  sobre  su  causa  de  
origen,  y  sobre  qué  se  puede  hacer  con  él.  ¡ La  respuesta  es  el  patrón  de  Caso  Especial !

Una  subclase  que  proporciona  un  comportamiento  especial  para  casos  particulares.

—Martin  Fowler,  Patrones  de  arquitectura  de  aplicaciones  empresariales  [Fowler02]

La  idea  detrás  del  patrón  de  casos  especiales  es  que  aprovechamos  el  polimorfismo  y  proporcionamos  clases  que  representan  
los  casos  especiales,  en  lugar  de  devolver  nullptr  o  algún  otro  valor  impar.  Estas  clases  de  casos  especiales  tienen  la  misma  
interfaz  que  la  clase  "normal"  que  esperan  las  personas  que  llaman.  El  diagrama  de  clases  de  la  figura  9­15  representa  tal  
especialización.

Figura  9­15.  Las  clases  que  representan  un  caso  especial  se  derivan  de  la  clase  Cliente

En  el  código  fuente  de  C++,  una  implementación  de  la  clase  Customer  y  la  clase  NotFoundCustomer
representar  el  caso  especial  se  parece  a  esto  (solo  se  muestran  las  partes  relevantes):

Listado  9­42.  Un  extracto  del  archivo  Customer.h  con  las  clases  Customer  y  NotFoundCustomer

#ifndef  CLIENTE_H_  
#define  CLIENTE_H_

#include  "Address.h"  
#include  "CustomerId.h"  #include  
<memoria>  #include  
<cadena>

class  Customer  

{ public: // ...más  funciones  miembro  aquí...  virtual  
~Customer()  =  default;

virtual  bool  isPersistable()  const  noexcept  {
return  (IDcliente.isValid()  && !  nombre.empty()  && !  apellido.empty()  &&
dirección  de  facturación­>es  válida()  &&  dirección  de  envío­>es  válida());
}

258
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

privado:
ID  de  cliente  ID  de  cliente;  
std::string  nombre;  std::string  
apellido;  
std::shared_ptr<Dirección>  dirección  de  facturación;  
std::shared_ptr<Dirección>  dirección  de  envío; };

class  NotFoundCustomer  final :  public  Customer  { public:  virtual  
bool  
isPersistable()  const  noexcept  override  { return  false;

} };

usando  CustomerPtr  =  std::unique_ptr<Cliente>;

#endif /*  CLIENTE_H_  */

Los  objetos  que  representan  el  caso  especial  ahora  se  pueden  usar  en  gran  medida  como  si  fueran  instancias  
válidas  (normales)  de  la  clase  Cliente.  Las  comprobaciones  nulas  permanentes,  incluso  cuando  el  objeto  se  pasa  entre  
diferentes  partes  del  programa,  son  superfluas,  ya  que  siempre  hay  un  objeto  válido.  Se  pueden  hacer  muchas  cosas  con  
el  objeto  NotFoundCustomer,  como  si  fuera  una  instancia  de  Customer,  por  ejemplo,  presentarlo  en  una  interfaz  de  usuario.
El  objeto  puede  incluso  revelar  si  es  persistente.  Para  el  Cliente  “real”,  esto  se  hace  analizando  sus  campos  de  datos.  Sin  
embargo,  en  el  caso  de  NotFoundCustomer,  esta  comprobación  siempre  tiene  un  resultado  negativo.
Y  en  comparación  con  las  comprobaciones  nulas  sin  sentido,  una  declaración  como  la  siguiente  hace  significativamente
mas  sentido:

if  (cliente.esPersistente())  {
// ...escriba  el  cliente  en  una  base  de  datos  aquí...
}

estándar::opcional<T>  [C++17]

Desde  C++17,  existe  otra  alternativa  interesante  que  podría  usarse  para  un  posible  resultado  o  valor  faltante:  std::opcional<T>  
(definido  en  el  encabezado  <opcional>).  Las  instancias  de  esta  plantilla  de  clase  representan  un  "valor  contenido  opcional",  es  
decir,  un  valor  que  puede  o  no  estar  presente.

La  clase  Cliente  se  puede  usar  como  un  valor  opcional  usando  std::opcional<T>  introduciendo  un  alias  de  tipo  de  la  
siguiente  manera:

#incluye  "Cliente.h"  #incluye  
<opcional>  usando  
OpcionalCliente  =  std::opcional<Cliente>;

Nuestra  función  de  búsqueda  CustomerService::findCustomerByName()  ahora  se  puede  implementar  de  la  siguiente  manera:

clase  CustomerRepository  { público:

OptionalCustomer  findCustomerByName(const  std::string&  name)  {

259
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

if  ( /*  la  búsqueda  fue  exitosa  */ )  { return  Cliente(); }  más  
{ volver  {}; } } };

En  el  sitio  de  llamada  de  la  función,  ahora  tiene  dos  formas  de  manejar  el  valor  de  retorno,  como  se  ilustra  en  
el  siguiente  ejemplo:

int  main()  
{ Repositorio  CustomerRepository  { };  auto  
opcionalCliente  =  repository.findCustomerByName("John  Doe");

//  Opción  1:  Detectar  una  excepción,  si  'clienteopcional'  está  vacío  intente  { cliente  automático  
=  
Clienteopcional.valor();
}  catch  (std::bad_opcional_acceso&  ex)  { std::cerr  <<  
ex.what()  <<  std::endl; }

//  Opción  2:  Proporcione  un  sustituto  para  un  objeto  que  posiblemente  falte  auto  cliente  =  
opcionalCliente.valor_or(NoEncontradoCliente());

devolver  0;
}

En  la  segunda  opción,  por  ejemplo,  es  posible  proporcionar  un  cliente  estándar  (predeterminado)  o,  como  en  
este  caso,  una  instancia  de  un  objeto  de  caso  especial,  si  el  cliente  opcional  está  vacío.  Recomiendo  elegir  la  
primera  opción  cuando  la  ausencia  de  un  objeto  es  inesperada  y  es  una  pista  de  que  se  ha  producido  un  error  
grave.  Para  los  demás  casos,  en  los  que  la  falta  de  un  objeto  no  es  nada  inusual,  recomiendo  la  opción  2.

¿Qué  es  un  idioma?
Un  idioma  de  programación  es  un  tipo  especial  de  patrón  para  resolver  un  problema  en  un  lenguaje  de  programación  o  tecnología  
específica.  Es  decir,  a  diferencia  de  los  patrones  de  diseño  más  generales,  las  expresiones  idiomáticas  tienen  una  aplicabilidad  limitada.
A  menudo,  su  aplicabilidad  se  limita  exactamente  a  un  lenguaje  de  programación  específico  o  una  determinada  tecnología,  por  ejemplo,  un  
marco.
Los  modismos  se  utilizan  normalmente  durante  el  diseño  y  la  implementación  detallados,  si  los  problemas  de  programación  deben  
resolverse  con  un  bajo  nivel  de  abstracción.  Un  modismo  bien  conocido  en  el  dominio  de  C  y  C++  es  el  llamado  Include  Guard,  a  veces  
también  llamado  Macro  Guard  o  Header  Guard,  que  se  usa  para  evitar  la  doble  inclusión  del  mismo  archivo  de  encabezado:

#ifndef  NOMBRE  DE  ARCHIVO_H_  

#define  NOMBRE  DE  ARCHIVO_H_

// ...contenido  del  archivo  de  cabecera...

#terminara  si

260
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Una  desventaja  de  esta  expresión  es  que  se  debe  garantizar  un  esquema  de  nomenclatura  coherente  para  los  nombres  de  archivos  y,  
por  lo  tanto,  también  para  los  nombres  de  macros  de  include­guard.  Por  lo  tanto,  la  mayoría  de  los  compiladores  de  C  y  C++  admiten  una  directiva  
#pragma  once  no  estándar  en  la  actualidad.  Esta  directiva,  insertada  en  la  parte  superior  de  un  archivo  de  encabezado,  garantizará  que  el  
archivo  de  encabezado  se  incluya  solo  una  vez.
Por  cierto,  ya  conocemos  algunos  modismos.  En  el  Capítulo  4  discutimos  el  recurso
Adquisición  es  inicialización  (RAII),  y  en  el  Capítulo  7  hemos  visto  el  modismo  Erase­Remove.

Algunos  modismos  útiles  de  C++
No  es  una  broma,  pero  en  realidad  puede  encontrar  una  colección  exhaustiva  de  casi  100(!)  expresiones  idiomáticas  de  C++  en  Internet  (WikiBooks:  
More  C++  Idioms;  URL:  https://en.wikibooks.org/wiki/More_C++_Idioms ).  El  problema  es  que  no  todos  estos  modismos  conducen  a  un  programa  
C++  moderno  y  limpio.  A  veces  son  muy  complejos  y  apenas  comprensibles  (p.  ej.,  jerarquía  algebraica),  incluso  para  desarrolladores  de  C++  
bastante  hábiles.
Además,  algunos  modismos  se  han  vuelto  obsoletos  en  gran  medida  con  la  publicación  de  C++  11  y  los  estándares  posteriores.  Por  lo  
tanto,  presento  aquí  solo  una  pequeña  selección,  que  considero  interesante  y  aún  útil.

El  poder  de  la  inmutabilidad
A  veces  es  una  gran  ventaja  tener  clases  para  objetos  que  no  pueden  cambiar  su  estado  una  vez  que  han  sido  creados,  también  conocidas  
como  clases  inmutables  (lo  que  realmente  quiere  decir  con  esto  son  de  hecho  objetos  inmutables,  porque  propiamente  hablando  una  clase  
solo  puede  ser  alterada  por  un  desarrollador).  Por  ejemplo,  los  objetos  inmutables  se  pueden  usar  como  valores  clave  en  una  estructura  de  datos  
hash,  ya  que  el  valor  clave  nunca  debe  cambiar  después  de  la  creación.
Otro  ejemplo  conocido  de  un  inmutable  es  la  clase  String  en  varios  otros  lenguajes  como  C#  o  Java.
Los  beneficios  de  las  clases  inmutables,  respectivamente,  y  los  objetos  son  los  siguientes:

•Los  objetos  inmutables  son  seguros  para  subprocesos  de  forma  predeterminada,  por  lo  que  no  tendrá  ningún
problemas  de  sincronización  si  varios  subprocesos  o  procesos  acceden  a  esos  objetos  de  forma  no  
determinista.  Por  lo  tanto,  la  inmutabilidad  facilita  la  creación  de  un  diseño  de  software  paralelizable  ya  que  no  hay  
conflictos  entre  los  objetos.

•  La  inmutabilidad  hace  que  sea  más  fácil  escribir,  usar  y  razonar  sobre  el  código,  porque  una  clase  invariable,  es  
decir,  un  conjunto  de  restricciones  que  siempre  deben  cumplirse,  se  establece  una  vez  en  la  creación  del  
objeto  y  se  garantiza  que  permanecerá  sin  cambios  durante  la  vida  del  objeto.  toda  la  vida.

Para  crear  una  clase  inmutable  en  C++,  se  deben  tomar  las  siguientes  medidas:

•Todas  las  variables  miembro  de  la  clase  deben  ser  inmutables,  es  decir,  todas  deben  ser  constantes  (consulte  la  sección  
sobre  Corrección  de  constantes  en  el  Capítulo  4).  Esto  significa  que  solo  se  pueden  inicializar  una  vez  en  un  
constructor,  utilizando  la  lista  de  inicializadores  de  miembros  del  constructor.

•Los  métodos  de  manipulación  no  cambian  el  objeto  en  el  que  se  llaman,  pero  devuelven  una  nueva  instancia  de  la  clase  
con  un  estado  alterado.  El  objeto  original  no  se  modifica.
Para  enfatizar  esto,  no  debería  haber  setter,  porque  una  función  miembro  cuyo  nombre  comienza  con  set…()  es  
engañosa.  No  hay  nada  que  establecer  en  un  objeto  inmutable.

•La  clase  debe  marcarse  como  final.  Esta  no  es  una  regla  estricta,  pero  si  se  puede  heredar  una  nueva  clase  de  una  clase  
supuestamente  inmutable,  podría  ser  posible  eludir  su  inmutabilidad.

261
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Aquí  hay  un  ejemplo  de  una  clase  inmutable  en  C++:

Listado  9­43.  El  empleado  está  diseñado  como  una  clase  inmutable.

#include  "Identificador.h"  
#include  "Dinero.h"  
#include  <string>  
#include  <string_view>

clase  Empleado  final  { público:

Empleado(std::string_view  nombre,  
std::string_view  apellido,  const  
Identificador  y  número  de  personal,  
const  Dinero  y  salario)  noexcept :  nombre  
{ nombre },  apellido  { apellido },  
número  de  personal  
{ número  de  personal },  salario  
{ salario }  { }

Identifier  getStaffNumber()  const  noexcept  { return  
staffNumber; }

Dinero  getSalario()  const  noexcept  { return  
salario; }

Empleado  changeSalary(const  Money&  newSalary)  const  noexcept  { return  
Employee(nombre,  apellido,  staffNumber,  newSalary); }

privado:  
const  std::string  nombre;  const  
std::string  apellido;  const  
Identificador  staffNumber;  salario;  const  
Dinero };

El  fallo  de  sustitución  no  es  un  error  (SFINAE)
De  hecho,  la  falla  de  sustitución  no  es  un  error  (abreviado:  SFINAE)  no  es  un  modismo  real  sino  una  característica  
del  compilador  de  C++.  Ya  ha  sido  parte  del  estándar  C++98,  pero  con  C++11  se  han  agregado  varias  características  
nuevas.  Sin  embargo,  todavía  se  lo  conoce  como  modismo,  también  porque  se  usa  en  un  estilo  muy  idiomático,  
especialmente  en  bibliotecas  de  plantillas,  como  la  biblioteca  estándar  de  C++  o  Boost.

262
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

El  pasaje  de  texto  definitorio  en  el  estándar  se  puede  encontrar  en  la  sección  14.8.2  sobre  la  deducción  de  argumentos  de  
plantilla.  Allí  podemos  leer  en  §8  la  siguiente  afirmación:

Si  una  sustitución  da  como  resultado  un  tipo  o  una  expresión  no  válidos,  la  deducción  de  tipo  falla.  Un  tipo  o  
expresión  inválida  es  aquella  que  estaría  mal  formada  si  se  escribiera  utilizando  los  argumentos  sustituidos.
Solo  los  tipos  y  expresiones  no  válidos  en  el  contexto  inmediato  del  tipo  de  función  y  sus  tipos  de  parámetros  
de  plantilla  pueden  generar  un  error  de  deducción.

—Estándar  para  el  lenguaje  de  programación  C++  [ISO11]

Los  mensajes  de  error  en  caso  de  una  instanciación  defectuosa  de  las  plantillas  de  C++,  por  ejemplo,  con  argumentos  de  
plantilla  incorrectos,  pueden  ser  muy  detallados  y  crípticos.  SFINAE  es  una  técnica  de  programación  que  garantiza  que  una  sustitución  
fallida  de  los  argumentos  de  la  plantilla  no  genere  un  molesto  error  de  compilación.  En  pocas  palabras,  significa  que  si  falla  la  
sustitución  de  un  argumento  de  plantilla,  el  compilador  continúa  con  la  búsqueda  de  una  plantilla  adecuada  en  lugar  de  abortar  con  
un  error.
Aquí  hay  un  ejemplo  muy  simple  con  dos  plantillas  de  funciones  sobrecargadas:

Listado  9­44.  SFINAE  por  ejemplo  de  dos  plantillas  de  funciones  sobrecargadas

#incluir  <iostream>

template  <nombre  de  tipo  T>  
void  print(nombre  de  tipo  T::tipo)  {
std::cout  <<  "Llamando  a  print(typename  T::type)"  <<  std::endl; }

template  <typename  T>  void  
print(T)  { std::cout  <<  
"Llamando  a  print(T)"  <<  std::endl; }

estructura  Aestructura  {
usando  tipo  =  int; };

int  main()  
{ imprimir<Astruct>(42);  
imprimir<int>(42);  
imprimir  (42);

devolver  0; }

La  salida  de  este  pequeño  ejemplo  en  stdout  será:

Llamando  a  print(nombre  de  tipo  T::tipo)
Llamando  a  imprimir  (T)
Llamando  a  imprimir  (T)

Como  puede  verse,  el  compilador  usa  la  primera  versión  de  print()  para  la  primera  llamada  de  función  y  la  segunda
versión  para  las  dos  convocatorias  posteriores.  Y  este  código  también  funciona  en  C++98.

263
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Bueno,  pero  SFINAE  anterior  a  C++11  tenía  varios  inconvenientes.  El  ejemplo  muy  simple  anterior  es  un  poco  
engañoso  con  respecto  al  esfuerzo  real  de  usar  esta  técnica  en  proyectos  reales.  La  aplicación  de  SFINAE  de  esta  manera  
en  las  bibliotecas  de  plantillas  ha  dado  lugar  a  un  código  muy  detallado  y  complicado  que  es  difícil  de  entender.  Además,  
está  mal  estandarizado  y,  a  veces,  es  específico  del  compilador.
Con  la  llegada  de  C++11,  se  introdujo  la  llamada  biblioteca  Type  Traits,  que  ya  conocimos  en  el  Capítulo  7.  
Especialmente  la  metafunción  std::enable_if()  (definida  en  el  encabezado  <type_traits>),  que  está  disponible  desde  C+
+11,  ahora  juega  un  papel  central  en  SFINAE.  Con  esta  función,  obtenemos  una  "capacidad  de  eliminación  de  funciones"  
condicional  de  la  resolución  de  sobrecarga  basada  en  rasgos  de  tipo.  En  otras  palabras,  podemos,  por  ejemplo,  elegir  
una  función  en  su  versión  sobrecargada  dependiendo  del  tipo  de  argumento  como  este:

Listado  9­45.  SFINAE  utilizando  la  plantilla  de  función  std::enable_if<>

#include  <iostream>  
#include  <tipo_rasgos>

template  <typename  T>  void  
print(T  var,  typename  std::enable_if<std::is_enum<T>::value,  T>::type*  =  0)  { std::cout  <<  "Llamada  sobrecargada  
print()  para  enumeraciones".  <<  estándar::endl; }

template  <typename  T>  void  
print(T  var,  typename  std::enable_if<std::is_integral<T>::value,  T>::type  =  0)  { std::cout  <<  "Llamando  a  print()  
sobrecargado  para  tipos  integrales".  <<  estándar::endl; }

template  <typename  T>  void  
print(T  var,  typename  std::enable_if<std::is_floating_point<T>::value,  T>::type  =  0)  { std::cout  <<  "Llamando  a  print()  
sobrecargado  para  tipos  de  coma  flotante".  <<  estándar::endl; }

template  <typename  T>  void  
print(const  T&  var,  typename  std::enable_if<std::is_class<T>::value,  T>::type*  =  0)  {
std::cout  <<  "Llamando  a  print()  sobrecargado  para  clases".  <<  estándar::endl; }

Las  plantillas  de  funciones  sobrecargadas  se  pueden  usar  simplemente  llamándolas  con  argumentos  de  diferentes  
tipos,  como  este:

Listado  9­46.  Gracias  a  SFINAE,  hay  una  función  print()  coincidente  para  argumentos  de  diferente  tipo

enum  Enumeración1  {
Literal1,
Literal2 };

enum  clase  Enumeración2:  int  {
Literal1,
Literal2 };

264
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

clase  Clazz  { };

int  principal()  {
Enumeración1  enumVar1  { };  
imprimir(enumVar1);

Enumeración2  enumVar2  { };  
imprimir(enumVar2);

imprimir  (42);

instancia  de  Clazz  { };  
imprimir  (instancia);

imprimir  (42.0f);

imprimir  (42.0);

devolver  0;
}

Después  de  compilar  y  ejecutar,  vemos  el  siguiente  resultado  en  la  salida  estándar:

Llamar  a  print()  sobrecargado  para  enumeraciones.
Llamar  a  print()  sobrecargado  para  enumeraciones.
Llamar  a  print()  sobrecargado  para  tipos  integrales.
Llamar  a  print()  sobrecargado  para  clases.
Llamar  a  print()  sobrecargado  para  tipos  de  punto  flotante.
Llamar  a  print()  sobrecargado  para  tipos  de  punto  flotante.

Debido  al  hecho  de  que  la  versión  C++11  de  std::enable_if  es  un  poco  detallada,  C++14  ha  agregado  una
alias  denominado  std::enable_if_t.

El  idioma  de  copiar  e  intercambiar
En  la  sección  "La  prevención  es  mejor  que  el  cuidado  posterior"  en  el  Capítulo  5,  hemos  aprendido  los  cuatro  niveles  de  garantía  
de  seguridad  de  excepción:  seguridad  sin  excepción,  seguridad  con  excepción  básica,  seguridad  con  excepción  fuerte  y  la  
garantía  de  no  tirar.  Lo  que  las  funciones  miembro  de  una  clase  siempre  deben  garantizar  es  la  seguridad  de  excepción  básica,  
porque  este  nivel  de  seguridad  de  excepción  suele  ser  fácil  de  implementar.
En  la  sección  “La  Regla  del  Cero”  en  el  Capítulo  5  hemos  aprendido  que  debemos  diseñar  clases  siempre  de  manera  que  
las  funciones  miembro  especiales  generadas  automáticamente  por  el  compilador  (constructor  de  copia,  operador  de  asignación  de  
copia,  etc.)  hagan  automáticamente  las  cosas  correctas.  O  dicho  de  otro  modo:  cuando  nos  vemos  obligados  a  
proporcionar  un  destructor  no  trivial,  estamos  ante  un  caso  excepcional  que  requiere  un  tratamiento  especial  durante  la  
destrucción  del  objeto.  Como  consecuencia,  se  deduce  que  las  funciones  miembro  especiales  generadas  por  el  compilador  no  
son  suficientes  para  hacer  frente  a  esta  situación,  y  tenemos  que  implementarlas  nosotros  mismos.
Sin  embargo,  ocasionalmente  es  inevitable  que  la  Regla  del  Cero  no  se  pueda  cumplir,  es  decir,  un  desarrollador  tiene  que  
implementar  las  funciones  de  miembros  especiales  por  sí  mismo.  En  este  caso,  puede  ser  una  tarea  desafiante  crear  una  
implementación  segura  de  excepción  de  un  operador  de  asignación  sobrecargado.  En  tal  caso,  el  modismo  Copiar  e  intercambiar  
es  una  forma  elegante  de  resolver  este  problema.

265
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Por  lo  tanto,  la  intención  de  este  modismo  es  la  siguiente:

Implemente  el  operador  de  asignación  de  copia  con  una  fuerte  seguridad  de  excepción.

La  forma  más  sencilla  de  explicar  el  problema  y  su  solución  es  un  pequeño  ejemplo.  Considere  la  siguiente  clase:

Listado  9­47.  Una  clase  que  administra  un  recurso  que  se  asigna  en  el  montón

#incluir  <cstddef>

clase  Clazz  final  { público:

Clazz(const  std::size_t  size) :  resourceToManage  { new  char[size] },  size  { size }  { }
~Clazz()  
{ eliminar  []  resourceToManage; }

privado:  
char*  resourceToManage;  
estándar::tamaño_t  
tamaño; };

Esta  clase  es,  por  supuesto,  solo  para  fines  de  demostración  y  no  debe  ser  parte  de  un  programa  real.
Supongamos  que  queremos  hacer  lo  siguiente  con  la  clase  Clazz:

int  principal()  {
instancia  Clazz1  { 1000 };
Clazz  instancia2  { instancia1 };  devolver  
0; }

Ya  lo  sabemos  por  el  capítulo  5  que  la  versión  generada  por  el  compilador  de  un  constructor  de  copia  hace  el
algo  incorrecto  aquí:  ¡solo  crea  una  copia  plana  del  puntero  de  carácter  resourceToManage!
Por  lo  tanto,  tenemos  que  proporcionar  nuestro  propio  constructor  de  copias,  así:

#incluye  <algoritmo>

clase  Clazz  final  

{ público: // ...
Clazz(const  Clazz&  other) :  Clazz  { other.size }  
{ std::copy(other.resourceToManage,  other.resourceToManage  +  other.size,  resourceToManage); } // ...

Hasta  ahora,  todo  bien.  Ahora  la  construcción  de  la  copia  funcionará  bien.  Pero  ahora  también  necesitaremos  una  tarea  de  copia
operador.  Si  no  está  familiarizado  con  el  idioma  de  copiar  e  intercambiar,  una  implementación  de  un  operador  de  
asignación  podría  verse  así:

266
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

#incluye  <algoritmo>

clase  Clazz  final  

{ público: // ...
Operador  Clazz&  =(const  Clazz&  otro)  {
if  (&otro  ==  esto)  { return  
*esto; }  eliminar  

[]  recurso  para  administrar;  
resourceToManage  =  new  char[otro.tamaño];  
std::copy(other.resourceToManage,  other.resourceToManage  +  other.size,  resourceToManage);  
tamaño  =  otro.tamaño;  
devolver  *esto;

} // ... };

Básicamente,  este  operador  de  asignación  funcionará,  pero  tiene  varios  inconvenientes.  Por  ejemplo,  el  
código  constructor  y  destructor  está  duplicado  en  él,  lo  cual  es  una  violación  del  principio  DRY  (ver  Capítulo  3).
Además,  hay  una  verificación  de  autoasignación  al  principio.  Pero  la  mayor  desventaja  es  que  no  podemos  garantizar  
la  seguridad  excepcional.  Por  ejemplo,  si  la  declaración  nueva  provoca  una  excepción,  el  objeto  puede  quedar  en  un  estado  
extraño  que  viola  las  invariantes  de  clase  elemental.
¡Ahora  entra  en  juego  el  idioma  de  copiar  e  intercambiar,  también  conocido  como  "Crear­Temporal­e­Intercambiar"!
Para  una  mejor  comprensión,  presento  ahora  toda  la  clase  Clazz:

Listado  9­48.  Una  implementación  mucho  mejor  de  un  operador  de  asignación  usando  el  idioma  de  copiar  e  intercambiar

#incluir  <algoritmo>  #incluir  
<cstddef>

clase  Clazz  final  { público:

Clazz(const  std::size_t  size) :  resourceToManage  { new  char[size] },  size  { size }  { }

~Clazz()  
{ eliminar  []  resourceToManage; }

Clazz(const  Clazz&  other) :  Clazz  { other.size }  
{ std::copy(other.resourceToManage,  other.resourceToManage  +  other.size,  resourceToManage);

Operador  Clazz&  =( Otro  Clazz)  
{ swap(otro);  
devolver  *esto; }

267
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

privado:  
void  swap(Clazz  y  otros)  noexcept  { using  
std::swap;  
swap(recursoParaGestionar,  otro.recursoParaGestionar);  
swap(tamaño,  otro.tamaño); }

char*  resourceToManage;  
estándar::tamaño_t  
tamaño; };

¿Cuál  es  el  truco  aquí?  Veamos  el  operador  de  asignación  completamente  diferente.  Esto  ya  no  tiene  una  referencia  
constante  (const  Clazz  y  otro)  como  parámetro,  sino  un  parámetro  de  valor  ordinario  (Clazz  otro).  Esto  significa  que  cuando  se  
llama  a  este  operador  de  asignación,  primero  se  llama  al  constructor  de  copias  de  Clazz.  El  constructor  de  copia,  a  su  vez,  
llama  al  constructor  predeterminado  que  asigna  memoria  para  el  recurso.  Y  eso  es  exactamente  lo  que  queremos:  ¡necesitamos  
una  copia  temporal  de  otro!
Ahora  llegamos  al  corazón  del  modismo:  la  llamada  de  la  función  miembro  privada  Clazz::swap().Dentro
esta  función,  el  contenido  de  la  instancia  temporal  other,  es  decir,  sus  variables  miembro,  se  intercambia  (“swapped”)  
con  el  contenido  de  las  mismas  variables  miembro  de  nuestro  propio  contexto  de  clase  (this).  Esto  se  hace  usando  la  función  
std::swap()  que  no  lanza  (definida  en  el  encabezado  <utility>).  Después  de  las  operaciones  de  intercambio,  el  objeto  temporal  
otro  ahora  posee  los  recursos  que  antes  eran  propiedad  de  este  objeto,  y  viceversa.
viceversa

Además,  la  función  de  miembro  Clazz::swap()  ahora  hace  que  sea  muy  fácil  implementar  un  movimiento
constructor:

clase  Clazz  

{ público: // ...
Clazz(Clazz&&  otro)  noexcept  

{ swap(otro); } // ... };

Por  supuesto,  el  objetivo  principal  en  un  buen  diseño  de  clase  debe  ser  que  no  sea  necesario  implementar  
constructores  de  copia  y  operadores  de  asignación  explícitos  (Regla  del  Cero).  Pero  cuando  se  vea  obligado  a  hacerlo,  
debe  recordar  el  idioma  de  copiar  e  intercambiar.

Puntero  a  la  implementación  (PIMPL)
La  última  sección  de  este  capítulo  está  dedicada  a  un  modismo  con  el  divertido  acrónimo  PIMPL.  PIMPL  significa  
Puntero  a  la  Implementación;  y  el  idioma  también  se  conoce  como  Handle  Body,  Compilation  Firewall  o  técnica  del  gato  
de  Cheshire  (el  gato  de  Cheshire  es  un  personaje  ficticio,  un  gato  sonriente,  de  la  novela  Alicia  en  el  país  de  las  
maravillas  de  Lewis  Carroll).  Y  tiene,  por  cierto,  algunas  similitudes  con  el  patrón  Bridge  descrito  en  [Gamma95].

La  intención  del  PIMPL  podría  formularse  de  la  siguiente  manera:

Elimine  las  dependencias  de  compilación  en  los  detalles  de  implementación  de  la  clase  interna  reubicándolos  
en  una  clase  de  implementación  oculta  y,  por  lo  tanto,  mejore  los  tiempos  de  compilación.

268
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

Echemos  un  vistazo  a  un  extracto  de  nuestra  clase  Cliente,  una  clase  que  hemos  visto  en  muchos  ejemplos  antes:

Listado  9­49.  Un  extracto  del  contenido  del  archivo  de  cabecera  Customer.h

#ifndef  CLIENTE_H_  
#define  CLIENTE_H_

#include  "Dirección.h"  
#include  "Identificador.h"  #include  
<cadena>

clase  Cliente  { público:  
Cliente();  
virtual  ~Cliente()  
=  predeterminado;  std::string  getFullName()  
const;  void  setShippingAddress( dirección  y  
dirección  const); // ...

privado:
Identificador  Idcliente;  std::string  
nombre;  std::string  apellido;

dirección  dirección  de  envío; };

#endif /*  CLIENTE_H_  */

Supongamos  que  se  trata  de  una  entidad  comercial  central  en  nuestro  sistema  de  software  comercial  y  que  muchas  otras  
clases  la  utilizan  (#include  "Customer.h").  Cuando  este  archivo  de  encabezado  cambia,  cualquier  archivo  que  use  ese  archivo  
deberá  volver  a  compilarse,  incluso  si  solo  se  agrega  una  variable  miembro  privada,  se  cambia  el  nombre,  etc.
Para  reducir  estas  recopilaciones  al  mínimo  absoluto,  entra  en  juego  el  lenguaje  PIMPL.
Primero  reconstruimos  la  interfaz  de  clase  de  la  clase  Cliente  de  la  siguiente  manera:

Listado  9­50.  El  archivo  de  encabezado  alterado  Customer.h

#ifndef  CLIENTE_H_  
#define  CLIENTE_H_

#include  <memoria>  
#include  <cadena>

dirección  de  clase ;

clase  Cliente  { público:  
Cliente();  
~Cliente  virtual  
();  std::string  getFullName()  
const;  void  setShippingAddress( dirección  y  
dirección  const); // ...

269
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

privado:  
clase  Impl;  
std::unique_ptr<Impl>  impl; };

#endif /*  CLIENTE_H_  */

Llama  la  atención  que  todas  las  variables  miembro  privadas  anteriores,  así  como  sus  directivas  de  inclusión  
asociadas,  ahora  han  desaparecido.  En  su  lugar,  está  presente  una  declaración  de  avance  para  una  clase  denominada  
Impl,  así  como  un  std::unique_ptr<T>  para  esta  clase  declarada  de  avance.
Y  ahora  echemos  un  vistazo  al  archivo  de  implementación  correspondiente:

Listado  9­51.  El  contenido  del  archivo  Customer.cpp

#include  "Cliente.h"

#include  "Dirección.h"
#include  "Identificador.h"

clase  Cliente::Impl  final  { public:  
std::string  
getFullName()  const;  void  
setShippingAddress( dirección  y  dirección  const);

privado:
Identificador  Idcliente;  
std::string  nombre;  
std::string  apellido;
dirección  dirección  de  envío; };

std::string  Cliente::Impl::getFullName()  const  {
"  "
volver  nombre  + +  apellido;
}

void  Cliente::Impl::setShippingAddress( dirección  y  dirección  const)  {
dirección  de  envío  =  dirección; }

//  La  implementación  de  la  clase  Cliente  comienza  aquí...

Cliente::Cliente() :  impl  { std::make_unique<Cliente::Impl>() }  { }

Cliente::~Cliente()  =  predeterminado;

std::string  Cliente::getFullName()  const  { return  impl­
>getFullName(); }

void  Customer::setShippingAddress(const  Dirección  y  dirección)  {
impl­>setShippingAddress(dirección); }

270
Machine Translated by Google

Capítulo  9  ■  Patrones  de  diseño  y  modismos

En  la  parte  superior  del  archivo  de  implementación  (hasta  el  comentario  del  código  fuente),  podemos  ver  
la  clase  Customer::Impl.  En  esta  clase,  ahora  se  ha  reubicado  todo,  lo  anterior  lo  ha  hecho  directamente  la  
clase  Cliente.  Aquí  también  encontramos  todas  las  variables  miembro.
En  la  sección  inferior  (comenzando  con  el  comentario),  ahora  encontramos  la  implementación  de  la  
clase  Cliente.  El  constructor  crea  una  instancia  de  Customer::Impl  y  la  mantiene  en  el  puntero  inteligente  impl.  Por  
lo  demás,  cualquier  llamada  de  la  API  de  clase  Cliente  se  delega  al  objeto  de  implementación  interno.
Si  ahora  hay  que  cambiar  algo  en  la  implementación  interna  en  Customer::Impl,  el  compilador
solo  debe  compilar  Customer.h/Customer.cpp,  y  luego  el  enlazador  puede  comenzar  su  trabajo  de  inmediato.  
Tal  cambio  no  tiene  ningún  efecto  en  el  exterior  y  se  evita  una  compilación  de  casi  todo  el  proyecto  que  
consume  mucho  tiempo.

271
Machine Translated by Google

APÉNDICE  A

Pequeña  guía  UML

El  OMG  Unified  Modeling  Language™  (OMG  UML)  es  un  lenguaje  gráfico  estandarizado  para  crear  modelos  de  software  y  
otros  sistemas.  Su  objetivo  principal  es  permitir  que  los  desarrolladores,  arquitectos  de  software  y  otras  partes  interesadas  diseñen,  
especifiquen,  visualicen,  construyan  y  documenten  artefactos  de  un  sistema  de  software.
Los  modelos  UML  respaldan  la  discusión  entre  diferentes  partes  interesadas,  sirven  como  ayuda  para  aclarar  los  requisitos  y  otras  
cuestiones  relacionadas  con  el  sistema  de  interés,  y  pueden  capturar  decisiones  de  diseño.
Este  apéndice  proporciona  una  breve  descripción  general  de  ese  subconjunto  de  notaciones  UML  que  se  utilizan  en  este  libro.  Cada
El  elemento  UML  se  ilustra  (sintaxis)  y  se  explica  brevemente  (semántica).  La  definición  abreviada  de  un  elemento  se  basa  en  la  
especificación  UML  actual  [OMG15]  que  se  puede  descargar  de  forma  gratuita  desde  el  sitio  web  de  OMG.
Se  debe  realizar  una  introducción  en  profundidad  al  lenguaje  de  modelado  unificado  con  la  ayuda  de  la  literatura  adecuada  o  tomando  
un  curso  en  un  proveedor  de  capacitación.

diagramas  de  clase
Entre  otras  aplicaciones  variadas,  los  diagramas  de  clases  se  utilizan  generalmente  para  representar  estructuras  de  un  diseño  de  software  
orientado  a  objetos.

Clase
El  elemento  central  en  los  diagramas  de  clases  es  la  clase.

CLASE

Una  clase  describe  un  conjunto  de  objetos  que  comparten  las  mismas  especificaciones  de  características,  restricciones  
y  semántica.

Una  instancia  de  una  clase  se  conoce  comúnmente  como  un  objeto.  Por  lo  tanto,  las  clases  pueden  ser  consideradas  como
planos  de  objetos.  El  símbolo  UML  para  una  clase  es  un  rectángulo,  como  se  muestra  en  la  Figura  A­1.

©  Stephan  Roth  2017   273
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0
Machine Translated by Google

APÉNDICE  a  ■  Guía  UML  pequeña

Figura  A­1.  Una  clase  llamada  Cliente

Una  clase  tiene  un  nombre  (en  este  caso,  "Cliente"),  que  se  muestra  centrado  en  el  primer  compartimento  del  
símbolo  rectangular.  Si  una  clase  es  abstracta,  es  decir,  no  se  puede  instanciar,  su  nombre  generalmente  se  muestra  en  
letras  cursivas.  Las  clases  pueden  tener  atributos  (datos,  estructura)  y  operaciones  (comportamiento),  que  se  muestran  en  los  
compartimentos  segundo,  respectivo  y  tercero.  El  tipo  de  un  atributo  se  indica  separado  por  dos  puntos  después  del  nombre  
del  atributo.  Lo  mismo  se  aplica  al  tipo  del  valor  de  retorno  de  una  operación.  Las  operaciones  pueden  tener  parámetros  que  
se  especifican  entre  paréntesis  (corchetes).  Los  atributos  u  operaciones  estáticos  están  subrayados.

Las  clases  tienen  un  mecanismo  para  regular  el  acceso  a  atributos  y  operaciones.  En  UML  se  llaman
visibilidades  El  tipo  de  visibilidad  se  coloca  delante  del  nombre  del  atributo  o  de  la  operación  y  puede  ser  uno  de  los  
caracteres  que  se  describen  en  la  Tabla  A­1.

Tabla  A­1.  Visibilidades

Tipo  de  visibilidad  del  personaje

+ public:  Este  atributo  u  operación  es  visible  para  todos  los  elementos  que  pueden  acceder  a  la  clase.
# protegido:  este  atributo  u  operación  no  solo  es  visible  dentro  de  la  clase  en  sí,  sino  que  también  es  visible  para  
los  elementos  que  se  derivan  de  la  clase  que  lo  posee  (consulte  Relación  de  generalización).

~ paquete:  este  atributo  u  operación  es  visible  para  los  elementos  que  están  en  el  mismo  paquete  que  su  clase  
propietaria.  Este  tipo  de  visibilidad  no  tiene  una  representación  adecuada  en  C++  y  no  se  usa  en  este  libro.

­
privado:  este  atributo  u  operación  solo  es  visible  dentro  de  la  clase,  en  ningún  otro  lugar.

Una  definición  de  clase  C++  correspondiente  a  la  clase  UML  que  se  muestra  en  la  Figura  A­1  anterior  puede  verse  así:

Listado  A­1.  La  clase  Cliente  en  C++

#include  <string>  
#include  "DateTime.h"  
#include  "CustomerIdentifier.h"

clase  Cliente  { público:

Cliente();  
~Cliente  virtual  ();

274
Machine Translated by Google

APÉNDICE  a  ■  Guía  UML  pequeña

std::string  getFullName()  const;
DateTime  getCumpleaños()  const;  
std::string  getPrintableIdentifier()  const;

privado:  
std::string  nombre;  
std::string  apellido;
Identificación  del  identificador  de  
cliente; };

La  representación  gráfica  de  las  instancias  rara  vez  es  necesaria,  por  lo  que  los  llamados  diagramas  de  objetos  
de  UML  solo  juegan  un  papel  menor.  El  símbolo  UML  para  representar  una  instancia  creada  (es  decir,  un  objeto)  de  
una  clase,  la  llamada  Especificación  de  instancia,  es  muy  similar  al  de  una  clase.  La  principal  diferencia  es  que  la  
leyenda  del  primer  compartimento  está  subrayada.  Muestra  el  nombre  de  la  instancia  especificada,  separado  por  dos  
puntos  de  su  tipo,  por  ejemplo,  la  clase  (consulte  la  Figura  A­2).  También  puede  faltar  el  nombre  (instancia  anónima).

Figura  A­2.  La  Especificación  de  instancia  a  la  derecha  representa  una  existencia  posible  o  real  de  una  instancia  de  
clase  Cliente

Interfaz
Una  interfaz  define  un  tipo  de  contrato:  una  clase  que  realiza  la  interfaz  debe  cumplir  ese  contrato.

INTERFAZ

Una  interfaz  es  una  declaración  de  un  conjunto  de  obligaciones  públicas  coherentes.

Las  interfaces  siempre  son  abstractas,  es  decir,  no  se  pueden  instanciar  de  forma  predeterminada.  El  símbolo  
UML  para  una  interfaz  es  muy  similar  a  una  clase,  con  la  palabra  clave  «interfaz» (entre  comillas  francesas  que  se  
denominan  «guillemets»)  que  precede  al  nombre,  como  se  muestra  en  la  Figura  A­3.

275
Machine Translated by Google

APÉNDICE  a  ■  Guía  UML  pequeña

Figura  A­3.  Class  Customer  implementa  operaciones  que  se  declaran  en  la  interfaz  Person

La  flecha  discontinua  con  la  punta  de  flecha  cerrada  pero  sin  rellenar  es  la  relación  de  realización  de  la  interfaz .
Esta  relación  expresa  que  la  clase  se  ajusta  al  contrato  especificado  por  la  interfaz,  es  decir,  la  clase  implementa  aquellas  
operaciones  que  son  declaradas  por  la  interfaz.  Por  supuesto,  está  permitido  que  una  clase  implemente  múltiples  interfaces.

A  diferencia  de  otros  lenguajes  orientados  a  objetos,  como  Java  o  C#,  no  existe  una  palabra  clave  de  interfaz  en  C++.
Por  lo  tanto,  las  interfaces  generalmente  se  emulan  con  la  ayuda  de  clases  abstractas  que  consisten  únicamente  en  funciones  de  
miembros  virtuales  puros,  como  se  muestra  en  los  siguientes  ejemplos  de  código.

Listado  A­2.  La  interfaz  de  persona  en  C++

#include  <cadena>  
#include  "FechaHora.h"

class  Person  { public:  
virtual  
~Person()  { }  virtual  std::string  
getFullName()  const  =  0;  virtual  DateTime  getbirthday()  const  =  
0; };

276
Machine Translated by Google

APÉNDICE  a  ■  Guía  UML  pequeña

Listado  A­3.  La  clase  Customer  realizando  la  interfaz  Person
#include  "Persona.h"  
#include  "IdentificadorCliente.h"

clase  Cliente :  persona  pública  { público:

Cliente();  
~Cliente  virtual  ();

virtual  std::string  getFullName()  const  override;  virtual  
DateTime  getbirthday()  const  override;  std::string  
getPrintableIdentifier()  const;

privado:  
std::string  nombre;  
std::string  apellido;
Identificación  del  identificador  de  
cliente; };

Para  mostrar  que  una  clase  o  componente  (consulte  la  sección  Componentes  a  continuación)  proporciona  o  requiere  
interfaces,  puede  utilizar  la  denominada  notación  de  bola  y  cavidad.  Una  interfaz  proporcionada  se  representa  con  una  bola  
(también  conocida  como  "piruleta"),  una  interfaz  requerida  se  representa  con  un  zócalo.  Estrictamente  hablando,  esta  es  una  notación  
alternativa,  como  lo  aclara  la  Figura  A­4.

Figura  A­4.  La  notación  de  rótula  para  las  interfaces  proporcionadas  y  requeridas

La  flecha  entre  la  clase  Cliente  y  la  interfaz  Cuenta  es  una  asociación  navegable,  que  se  explica
en  la  siguiente  sección  sobre  asociaciones  UML.

277
Machine Translated by Google

APÉNDICE  a  ■  Guía  UML  pequeña

Asociación
Las  clases  suelen  tener  relaciones  estáticas  con  otras  clases.  La  asociación  UML  especifica  este  tipo  de  relación.

ASOCIACIÓN

Una  relación  de  asociación  permite  que  una  instancia  de  un  clasificador  (por  ejemplo,  una  clase  o  un  componente)  
acceda  a  otra.

En  su  forma  más  simple,  la  sintaxis  UML  para  una  asociación  es  una  línea  sólida  entre  dos  clases,  como  se  muestra  en  
la  Figura  A­5.

Figura  A­5.  Una  relación  de  asociación  simple  entre  dos  clases.

Esta  simple  asociación  muchas  veces  no  es  suficiente  para  especificar  adecuadamente  la  relación  entre  ambas  clases.
Por  ejemplo,  la  dirección  de  navegación  a  través  de  una  asociación  tan  simple,  es  decir,  quién  puede  acceder  a  quién,  no  
está  definida  de  forma  predeterminada.  Sin  embargo,  la  navegabilidad  en  este  caso  suele  interpretarse  como  bidireccional  por  
convención,  es  decir,  el  Cliente  tiene  un  atributo  para  acceder  a  ShoppingCart  y  viceversa.  Por  lo  tanto,  se  puede  proporcionar  
más  información  a  una  asociación.  La  Figura  A­6  ilustra  algunas  de  las  posibilidades.

278
Machine Translated by Google

APÉNDICE  a  ■  Guía  UML  pequeña

Figura  A­6.  Algunos  ejemplos  de  asociaciones  entre  clases

1.  Este  ejemplo  muestra  una  asociación  con  un  extremo  navegable  (representado  por  un
punta  de  flecha)  y  el  otro  de  navegabilidad  no  especificada.  La  semántica  es:  la  clase  A  puede  
navegar  a  la  clase  B.  En  la  otra  dirección  no  se  especifica,  es  decir,  la  clase  B  podría  navegar  a  la  
clase  A.

■  Nota  Se  recomienda  enfáticamente  definir  la  interpretación  de  la  navegabilidad  de  tal  extremo  de  asociación  
no  especificado  en  su  proyecto.  Mi  recomendación  es  considerarlos  como  no  navegables.  Esta  
interpretación  también  se  utiliza  en  este  libro.

2.  Esta  asociación  navegable  tiene  un  nombre  (“has”).  El  triángulo  sólido  indica  la  dirección  de  lectura.  
Aparte  de  eso,  la  semántica  de  esta  asociación  es  completamente  idéntica  al  ejemplo  1.

3.  En  este  ejemplo,  ambos  extremos  de  la  asociación  tienen  etiquetas  (nombres)  y  multiplicidades.
Las  etiquetas  se  utilizan  normalmente  para  especificar  los  roles  de  las  clases  en  una  asociación.

Una  multiplicidad  especifica  la  cantidad  permitida  de  instancias  de  las  clases  que  están  
involucradas  en  una  asociación.  Es  un  intervalo  inclusivo  de  enteros  no  negativos  que  
comienza  con  un  límite  inferior  y  termina  con  un  límite  superior  (posiblemente  infinito).
En  este  caso,  cualquier  A  tiene  de  cero  a  cualquier  número  de  B,  mientras  que  cualquier  B  tiene  exactamente  una  A.

La  Tabla  A­2  muestra  algunos  ejemplos  de  multiplicidades  válidas.

4.  Esta  es  una  asociación  especial  llamada  agregación.  Representa  un  todo­parte
relación,  es  decir,  una  clase  (la  parte)  está  subordinada  jerárquicamente  a  la  otra  clase  (el  
todo).  El  diamante  poco  profundo  es  solo  un  marcador  en  este  tipo  de  asociación  e  identifica  el  
todo.  De  lo  contrario,  todo  lo  que  se  aplica  a  las  asociaciones  también  se  aplica  a  una  
agregación.

279
Machine Translated by Google

APÉNDICE  a  ■  Guía  UML  pequeña

5.  Esta  es  una  agregación  compuesta,  que  es  una  forma  fuerte  de  agregación.
Expresa  que  el  todo  es  dueño  de  las  partes,  y  por  lo  tanto  también  responsable  de  las  
partes.  Si  se  elimina  una  instancia  del  todo,  todas  sus  instancias  parciales  normalmente  
se  eliminan  con  ella.

■  Nota  Tenga  en  cuenta  que  una  parte  puede  (donde  esté  permitido)  eliminarse  de  un  compuesto  antes  de  que  se  elimine  el  todo  

y,  por  lo  tanto,  no  se  eliminará  como  parte  del  todo.  Esto  puede  ser  posible  mediante  una  multiplicidad  de  0..1  en  el  extremo  de  la  

asociación  que  está  conectado  al  todo,  es  decir,  el  extremo  con  el  rombo  lleno.  Las  únicas  multiplicidades  permitidas  en  este  extremo  

son  1  o  0..1;  todas  las  demás  multiplicidades  están  prohibidas.

Tabla  A­2.  Ejemplos  de  multiplicidad

Multiplicidad Significado

1 Exactamente  uno  Si  no  se  muestra  una  multiplicidad  en  un  extremo  de  la  asociación,  este  es  el  valor  predeterminado.

1..10 Un  intervalo  inclusivo  entre  1  y  10.

0..* Un  intervalo  inclusivo  entre  0  y  cualquier  número  (de  cero  a  muchos).  El  carácter  de  estrella  (*)  se  
utiliza  para  representar  el  límite  superior  ilimitado  (o  infinito).
* Forma  abreviada  de  0..*.

1..* Un  intervalo  inclusivo  entre  1  y  cualquier  número  (uno  a  muchos).

En  los  lenguajes  de  programación,  las  asociaciones  y  el  mecanismo  de  navegación  de  una  clase  a  otra  se  
pueden  implementar  de  varias  formas.  En  C++,  las  asociaciones  suelen  implementarse  mediante  miembros  que  tienen  
la  otra  clase  como  su  tipo,  por  ejemplo,  como  una  referencia  o  un  puntero,  como  se  muestra  en  el  siguiente  ejemplo.

Listado  A­4.  Ejemplo  de  implementación  de  una  asociación  navegable  entre  las  clases  A  y  B

clase  B; //  Declaración  de  reenvío

clase  A  
{ privada:  
B*  
b; // ...

clase  B  { //  
¡No  hay  puntero  ni  ninguna  otra  referencia  a  la  clase  A  aquí! };

Generalización
Un  concepto  central  en  el  desarrollo  de  software  orientado  a  objetos  es  la  llamada  herencia.  Lo  que  se  quiere  decir  
con  esto  es  la  generalización  de  la  respectiva  especialización  de  clases.

280
Machine Translated by Google

APÉNDICE  a  ■  Guía  UML  pequeña

GENERALIZACIÓN

Una  generalización  es  una  relación  taxonómica  entre  una  clase  general  y  una  clase  más  específica.

La  relación  de  generalización  se  utiliza  para  representar  el  concepto  de  herencia:  la  clase  específica  
(subclase)  hereda  atributos  y  operaciones  de  la  clase  más  general  (clase  base).  La  sintaxis  UML  de  la  relación  de  
generalización  es  una  flecha  continua  con  una  punta  de  flecha  cerrada  pero  sin  relleno,  como  se  muestra  en  la  Figura  A­7.

Figura  A­7.  Una  clase  base  abstracta  Forma  y  tres  clases  concretas  que  son  especializaciones  de  ella.

En  la  dirección  de  la  flecha,  esta  relación  se  lee  de  la  siguiente  manera:  “<Subclase>  es  un  tipo  de  
<Clase  base>”,  por  ejemplo,  “Rectángulo  es  un  tipo  de  Forma”.

Dependencia  Además  

de  las  asociaciones  ya  mencionadas,  las  clases  (y  componentes)  pueden  tener  más  relaciones  con  otras  clases  (y  
componentes).  Por  ejemplo,  si  una  clase  se  usa  como  un  tipo  para  un  parámetro  de  una  función  miembro,  esto  no  
es  una  asociación,  pero  es  un  tipo  de  dependencia  de  esa  clase  usada.

DEPENDENCIA

Una  dependencia  es  una  relación  que  significa  que  un  solo  elemento  o  un  conjunto  de  elementos  requiere  de  otros  
elementos  para  su  especificación  o  implementación.

Como  se  muestra  en  la  Figura  A­8,  una  dependencia  se  muestra  como  una  flecha  discontinua  entre  dos  
elementos,  por  ejemplo,  entre  dos  clases  o  componentes.  Implica  que  el  elemento  en  la  punta  de  flecha  es  requerido  
por  el  elemento  en  la  cola  de  la  flecha,  por  ejemplo,  para  propósitos  de  implementación.  En  otras  palabras:  el  
elemento  dependiente  está  incompleto  sin  el  elemento  independiente.

281
Machine Translated by Google

APÉNDICE  a  ■  Guía  UML  pequeña

Figura  A­8.  Dependencias  misceláneas

Además  de  su  forma  simple  (ver  el  primer  ejemplo  en  la  Figura  A­8),  se  pueden  distinguir  dos  tipos  especiales  de  dependencia:

1.  La  dependencia  de  uso  («uso»)  es  una  relación  en  la  que  un  elemento  requiere  de  otro  elemento  (o  
conjunto  de  elementos)  para  su  plena  implementación  u  operación.

2.  La  dependencia  de  creación  ("Crear")  es  un  tipo  especial  de  dependencia  de  uso
indicando  que  el  elemento  en  la  cola  de  la  flecha  crea  instancias  del  tipo  en  la  punta  de  flecha.

Componentes
El  componente  del  elemento  UML  representa  una  parte  modular  de  un  sistema  que  generalmente  se  encuentra  en  un  nivel  de  
abstracción  más  alto  que  una  sola  clase.  Un  componente  sirve  como  una  especie  de  “cápsula”  o  “envoltura”  para  un  conjunto  
de  clases  que  en  conjunto  cumplen  con  cierta  funcionalidad.  La  sintaxis  UML  para  un  componente  se  muestra  en  la  Figura  A­9.

Figura  A­9.  La  notación  UML  para  un  componente

Debido  al  hecho  de  que  un  componente  encapsula  su  contenido,  define  su  comportamiento  en  términos  de  los  llamados
interfaces  proporcionadas  y  requeridas.  Solo  estas  interfaces  están  disponibles  para  el  entorno  para  el  uso  de  un  componente.  
Esto  significa  que  un  componente  puede  ser  reemplazado  por  otro  si  y  solo  si  sus  interfaces  proporcionadas  y  requeridas  son  
idénticas.  La  sintaxis  concreta  para  las  interfaces  (notación  de  bola  y  cavidad)  es  exactamente  la  misma  que  se  muestra  en  la  
Figura  A­4  y  se  describe  en  la  sección  sobre  Interfaces.

282
Machine Translated by Google

APÉNDICE  a  ■  Guía  UML  pequeña

estereotipos
Entre  otras  formas,  el  vocabulario  de  UML  se  puede  ampliar  con  la  ayuda  de  los  llamados  estereotipos.  Este  mecanismo  liviano  permite  
la  introducción  de  extensiones  específicas  de  plataforma  o  dominio  de  elementos  UML  estándar.  Por  ejemplo,  mediante  la  aplicación  del  
estereotipo  «Fábrica»  en  el  elemento  Clase  estándar  de  UML,  los  diseñadores  pueden  expresar  que  esas  clases  específicas  son  fábricas  
de  objetos.
El  nombre  de  un  estereotipo  aplicado  se  muestra  entre  un  par  de  guillemets  (comillas  francesas)  encima  o  antes  del  nombre  del  elemento  
del  modelo.  Algunos  estereotipos  también  introducen  un  nuevo  símbolo  gráfico,  un  icono.
La  Tabla  A­3  contiene  una  lista  de  los  estereotipos  utilizados  en  este  libro.

Tabla  A­3.  Estereotipos  utilizados  en  este  libro

Estereotipo Significado

"Fábrica" Una  clase  que  crea  objetos  sin  exponer  la  lógica  de  creación  de  instancias  al  cliente.

"Fachada" Una  clase  que  proporciona  una  interfaz  unificada  a  un  conjunto  de  interfaces  en  un  componente  o  subsistema  
complejo.

«SUT» El  sistema  bajo  prueba.  Las  clases  o  componentes  con  este  estereotipo  son  las  entidades  a  probar,  por  ejemplo,  con  
la  ayuda  de  Unit  Tests.

«TestContext»  Un  contexto  de  prueba  es  una  entidad  de  software,  por  ejemplo,  una  clase  que  actúa  como  un  mecanismo  de  agrupación  
para  un  conjunto  de  casos  de  prueba  (ver  estereotipo  «TestCase»).

«TestCase»  Un  caso  de  prueba  es  una  operación  que  interactúa  con  el  «SUT»  para  verificar  su  corrección.  Los  casos  de  prueba  se  agrupan  
en  un  «TestContext».

283
Machine Translated by Google

Bibliografía

[Beck01]  Kent  Beck,  Mike  Beedle,  Arie  van  Bennekum,  et  al.  Manifiesto  para  el  desarrollo  ágil  de  software.
2001.  http://agilemanifesto.org,  consultado  el  24  de  septiembre  de  2016.
[Beck02]  Kent  Beck.  Desarrollo  basado  en  pruebas:  con  el  ejemplo.  Addison­Wesley  Professional,  2002.
[Busch96]  Frank  Buschmann,  Regine  Meunier,  Hans  Rohnert  y  Peter  Sommerlad.  Arquitectura  de  software  orientada  a  patrones  
Volumen  1:  un  sistema  de  patrones.  Wiley,  1996.
[Cohn09]  Mike  Cohn.  Tener  éxito  con  Agile:  desarrollo  de  software  usando  Scrum  (1.ª  edición).
Addison­Wesley,  2009.
[Evans04]  Eric  J.  Evans.  Diseño  basado  en  dominios:  abordar  la  complejidad  en  el  corazón  del  software
(1ª  Edición).  Addison­Wesley,  2004.
[Fernandes12]  R.  Martinho  Fernandes:  Regla  del  Cero.  https://rmf.io/cxx11/regla­de­cero,  recuperado
6­4­2017.
[Fowler02]  Martín  Fowler.  Patrones  de  Arquitectura  de  Aplicaciones  Empresariales.  Addison­Wesley,  2002.
[Fowler03]  Martín  Fowler.  Modelo  de  dominio  anémico.  Noviembre  de  2003.  URL:  https://martinfowler.com/
bliki/AnemicDomainModel.html,  consultado  el  5­1­2017.
[Fowler04]  Martín  Fowler.  Inversión  de  Contenedores  de  Control  y  el  patrón  de  Inyección  de  Dependencia.  Enero
2004.  URL:  https://martinfowler.com/articles/injection.html,  consultado  el  19­7­2017.
[Gamma95]  Erich  Gamma,  Richard  Helm,  Ralph  Johnson  y  John  Vlissides.  Patrones  de  diseño:  Elementos  de
Software  reutilizable  orientado  a  objetos.  Addison­Wesley,  1995.
[GAOIMTEC92]  Oficina  General  de  Contabilidad  de  los  Estados  Unidos.  GAO/IMTEC­92­26:  Defensa  antimisiles  Patriot:
Problema  de  software  llevó  a  falla  del  sistema  en  Dhahran,  Arabia  Saudita,  1992.  http://
www.fas.org/spp/starwars/gao/im92026.htm,  consultado  el  26­12­2013.
[Hunt99]  Andrew  Hunt,  David  Thomas.  El  programador  pragmático:  de  oficial  a  maestro.
Addison­Wesley,  1999.
[ISO11]  Organización  Internacional  de  Normalización  (ISO),  JTC1/SC22/WG21  (Los  estándares  de  C++
Comité).  ISO/IEC  14882:2011,  Estándar  para  el  lenguaje  de  programación  C++.
[Jeffries98]  Ron  Jeffries.  ¡NO  lo  vas  a  necesitar!  http://ronjeffries.com/xprog/articles/practices/
pracnotneed/,  consultado  el  24  de  septiembre  de  2016.
[JPL99]  Laboratorio  de  Propulsión  a  Chorro  de  la  NASA  (JPL).  El  equipo  de  Mars  Climate  Orbiter  encuentra  la  causa  probable  de  la  pérdida.
Septiembre  de  1999.  URL:  http://mars.jpl.nasa.gov/msp98/news/mco990930.html,  consultado  el  7­7­2013.
[Knuth74]  Donald  E.  Knuth.  Programación  estructurada  con  sentencias  Ir  a,  ACM  Journal  Computing  Surveys,  vol.  6,  No.  4,  
diciembre  de  1974.  http://cs.sjsu.edu/~mak/CS185C/KnuthStructuredProgrammingGoTo.pdf ,  consultado  el  
5­3­2014.
[Koenig01]  Andrew  Koenig  y  Barbara  E.  Moo.  C++  simplificado:  la  regla  de  tres.  Junio  de  2001.  http://www.drdobbs.com/c­
made­easier­the­rule­of­three/184401400,  consultado  el  16­5­2017.
[Langr13]  Jeff  Langr.  Programación  moderna  en  C++  con  desarrollo  basado  en  pruebas:  Codifique  mejor,  duerma  mejor.
Estantería  pragmática,  2013.
[Liskov94]  Barbara  H.  Liskov  y  Jeanette  M.  Wing:  una  noción  conductual  de  la  subtipificación.  Transacciones  ACM
sobre  lenguajes  y  sistemas  de  programación  (TOPLAS)  16  (6):  1811–1841.  Noviembre  de  1994.  http://dl.acm.org/
citation.cfm?doid=197320.197383,  consultado  el  30­12­2014.

©  Stephan  Roth  2017   285
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0
Machine Translated by Google

■  Bibliografía

[Martin96]  Robert  C.  Martín.  El  principio  de  sustitución  de  Liskov.  ObjectMentor,  marzo  de  1996.  http://
www.objectmentor.com/resources/articles/lsp.pdf,  consultado  el  30­12­2014.
[Martin03]  Robert  C.  Martín.  Desarrollo  ágil  de  software:  principios,  patrones  y  prácticas.  Prentice  Hall,  2003.
[Martin09]  Robert  C.  Martín.  Código  limpio:  un  manual  de  artesanía  ágil  de  software.  Prentice  Hall,  2009.
[Martín11]  Robert  C.  Martín.  The  Clean  Coder:  un  código  de  conducta  para  programadores  profesionales.
Prentice  Hall,  2011.
[Meyers05]  Scott  Meyers.  C++  eficaz:  55  formas  específicas  de  mejorar  sus  programas  y  diseños
(Tercera  edicion).  Addison­Wesley,  2005.
[OMG15]  Grupo  de  gestión  de  objetos.  OMG  Unified  Modeling  Language™  (OMG  UML),  versión  2.5.
Número  de  documento  OMG:  formal/2015­03­01.  http://www.omg.org/spec/UML/2.5,  consultado  el  11­5­2016.
[Parnas07]  Grupo  de  interés  especial  de  ACM  en  ingeniería  de  software:  perfil  de  miembro  de  ACM  de  David  Lorge  Parnas.
http://www.sigsoft.org/SEN/parnas.html,  consultado  el  24  de  septiembre  de  2016.
[Ram03]  Stefan  Ram.  Página  de  inicio:  Dr.  Alan  Kay  sobre  el  significado  de  "Programación  orientada  a  objetos".
http://www.purl.org/stefan_ram/pub/doc_kay_oop_en),  consultado  el  11­3­2013.
[Sommerlad13]  Peter  Sommerlad.  Reunión  C++  2013:  C++  más  simple  con  C++11/14.  Noviembre  de  2013.
http://wiki.hsr.ch/PeterSommerlad/files/MeetingCPP2013_SimpleC++.pdf,  consultado  el  1­2­2014.
[Thought08]  ThoughtWorks,  Inc.  (múltiples  autores).  La  antología  de  ThoughtWorks®:  ensayos  sobre  software
Tecnología  e  Innovación.  Estantería  pragmática,  2008.
[Wipo1886]  Organización  Mundial  de  la  Propiedad  Intelectual  (OMPI):  Convenio  de  Berna  para  la  Protección  de  las  Obras  Literarias  
y  Artísticas.  http://www.wipo.int/treaties/en/ip/berne/index.html,  consultado  el  3­9­2014.

286
Machine Translated by Google

Índice

A argumento  de  bandera,  
64  nombres  de  funciones,  
Árbol  de  sintaxis  abstracta  (AST),  242 60  indicaciones,  
Principio  de  dependencia  acíclica,  93,  150–153 59  indicadores  de  demasiadas  responsabilidades,  59  
Manifiesto  Ágil,  27 nombres  reveladores  de  intenciones,  
clases  anémicas,  163 61  optimización  de  aceleración  local  y  global,  60
Funciones  anónimas,  181 NULL/nullptr,  68  
número  de  argumentos,  62  

B punteros  regulares,  70  
parámetro  de  resultado,  
Teoría  de  la  ventana  rota,  2 66  código  fuente,  56–57
GetInfo(),  42  

C descomposición  jerárquica,  45
notación  húngara,  47–48  operador  
Limpiar  C++
de  inserción,  77  macros,  83  
niveles  de  abstracción,  45   nombre  
código  fuente  OpenOffice  3.4.1  de  Apache,  42   significativo  y  expresivo,  48  printf(),  76  
comentarios  
redundancia,  
bloque  de  comentarios,  50–52   46  código  
función  deshabilitada,  50   autodocumentado,  44  código  
generador  de  documentación,  54–56   fuente,  43
fórmula/algoritmo  matemático,  53  código  fuente,   TDD  (consulte  Desarrollo  basado  en  pruebas  (TDD)
49,  54–56  control  de  versión   Propiedad  de  código  colectivo,  40
sustituta,  52  texto  descripción,  49   Arquitectura  de  agente  de  solicitud  de  objetos  comunes
historias  de  usuarios,  49   (CORBA),  134
abreviaturas   Agregación  compuesta,  280
crípticas,  46  cadenas  de  C++,   patrón  compuesto,  245
74  arreglos  de   Interruptores  electrónicos  operados  por  computadora  (4ESS),  11
estilo  C,  80  cast  de   Inyección  de  constructor,  229
estilo  C,  81  DDD,  
44–45  
definición,  3   D
patrones  de  diseño  (consulte  Patrones  de   Objeto  de  acceso  a  datos,  20
diseño)  programación  funcional  (consulte   Inyección  de  dependencia  (DI),  25,  219  objeto  
Programación  funcional  ( FP)   de  cliente,  222
lenguaje)  funciones
clase  CustomerRepository,  224,  227–229  
Código  fuente  OpenOffice  3.4.1  de  Apache,   desacoplamiento,  
56–57  código   225  clases  específicas  de  dominio,  223–
comentado,  58  corrección   224  entradas  de  
constante,  70–72  complejidad   registro,  226  diseño  de  software,  229
ciclomática,  58 Registrador  de  salida  estándar,  225–226

©  Stephan  Roth  2017   287
S.  Roth,  C++  limpio,  DOI  10.1007/978­1­4842­2793­0
Machine Translated by Google

■  ÍNDICE

Principio  de  inversión  de  dependencia  (DIP)  clase   Lenguaje  de  programación  funcional  (FP)
Cuenta,  155   Código  limpio,  189–190  
componentes,  156   composición,  168  
Clase  de  cliente,  158   definición,  168  
módulos  de  alto  nivel,  157   fácil  de  probar,  169  
Propietario  de  interfaz,  154   funciones  de  orden  superior,  168,  188  
módulos  de  bajo  nivel,  157   datos  inmutables,  168  
Diagrama  de  clase  UML,  155   funciones  impuras,  171  
Patrones  de  diseño,  25   parámetros  de  entrada  y  salida,  169
adaptador,  230–231   Cálculo  lambda,  168
forma  canónica,  217   Lisp,  167  
comando,  235–238   funciones  miembro,  169  C++  
Procesador  de  comandos,  239–242   moderno
Compuesto,  242–245   Binary  Functor,  178  
inyección  de  dependencia,  219   carpetas  y  función
objeto  de  cliente,  222   envoltorios,  181
clase  CustomerRepository,  224,  226,  227   Objetos  similares  a  funciones,  173
desacoplamiento,   Generador,  174  
225  clases  específicas  de  dominio,   expresiones  lambda  genéricas,  183  
223  entradas  de   expresiones  lambda,  181
registro,  226  diseño  de   Predicados,  178  
software,  229  StandardOutputLogger,   plantillas,  173  
225–226  frente  a  principios  de   función  unaria,  176  sin  
diseño,  217  Fachada,   efectos  secundarios,  
253–254  Fábrica,   168  paralelización,  169  
250–252  Interfaz  fluida,   Lisp  pasado,  167  
234  modismos,   función  pura,  170  
260  RAII  (consulte  Adquisición  de  recursos   transparencia  referencial,  170
es  inicialización  (RAII))   Funtores,  173
copia  e  intercambio,  265–268  
inmutabilidad,  261–  262  
Incluir  guardia,  260   GRAMO

PIMPL,  268–271   Banda  de  los  cuatro  (GoF),  217
SFINAE,  262–265   Convenciones  generales  de  nomenclatura,  48
iterador,  218   Licencia  pública  general  (GPLv2),  54  función  
Clase  de  dinero,  254–257   miembro  get(),  90
objeto  nulo,  257–260   proyectos  greenfield,  5
Observador,  245–250  
fábrica  simple,  250–252  
singleton,  222   H
Patrón  de  objeto  de  caso  especial,  66   Mango,  93
Estrategia,  231–235   Guardia  de  cabecera,  260
Diseño  impulsado  por  el  dominio  (DDD),  44–45 Informática  de  alto  rendimiento  (HPC),  106

mi yo,  j
Unidad  de  control  del  motor  (ECU),  135 Especificación  de  instancia,  273
Calidad  externa,  1   Desarrollo  Integrado
Programación  eXtreme  (XP),  28,  191 Medio  Ambiente  (IDE),  242
Principio  de  segregación  de  interfaz  (ISP),  149–150
calidad  interna,  1
F
Patrón  de  fachada,  253
Falta  de  transparencia,  126. k
Objetos  falsos,  22 Estilo  Kernighan  y  Ritchie  (K&R),  6

288
Machine Translated by Google

■  ÍNDICE

L DIP  (ver  Principio  de  inversión  de  dependencia
(ADEREZO))
Introductor  lambda,  182 Motor,  163–164  
Aplicación  de  la  ley  de  Demeter   Bomba  de  
(LOD),  162   combustible,  165  
descomposición  del  automóvil,   Clases  de  Dios,  136  ISP  (ver  Principio  de  segregación  
158  conductor  de  clase,   de  interfaz  (ISP)),  
159–160   149–150  LOD  (ver  Ley  de  Demeter  (LOD))
reglas,  161  diseño  de  software,  162 LSP  (ver  Principio  de  sustitución  de  Liskov  (LSP))
Sistemas  heredados,  17
Código  de  línea  numerada,  6 OCP  ( consulte  Principio  abierto­cerrado  (OCP)),  
Herencia  del  principio  de  sustitución  de  Liskov   137  SRP  
(LSP),  147–148  dilema   (consulte  Principio  de  responsabilidad  única  (SRP)),  
cuadrado­rectángulo  clase  abstracta,   137  Funciones  
140  biblioteca  de  clases,   de  miembros  estáticos,  165–166  CORBA,  
138–139  clase  explícita   134  Simula­67,  
Cuadrado,  146–147  implementación,   133  Xerox  PARC,  
140–142  instancia  de,  143 134  Una  vez  y  solo  
una  vez  ( OAOO),  29  Principio  abierto­cerrado  
RTTI,  145–146   (OCP),  137,  231  OpenOffice.org  (OOo),  42  Anular  
reglas,  144–145   especificador,  64
setEdges(),  setWidth()  y  setHeight(),  143–144

Registradores,  
219  referencia  de  valor  l,  95 P,  Q  Plain  

Old  Unit  Testing  (POUT),  192–193  pruebas  de  
aceptación,  12  AT&T  
METRO
crash,  11  Cup  Cake  
Macroguardia,  260 Anti­Pattern,  12  bases  de  datos,  19  
Singleton  de  Meyers,  220 nombres  
Maquetas,  22 expresivos  y  descriptivos,  15–16  sistemas  externos,  19  
Modelo­Vista­Controlador  (MVC),  245 subsistema  Fly­by­Wire ,  
10  getters  y  setters,  18  Ice  Cream  
Cone  Anti­Pattern,  12  
norte
inicialización,  18  pruebas  de  sistemas  
Centro  de  Computación  de  Noruega  (NCC),  133 grandes,  12  nave  
Síndrome  de  No  inventado  aquí  (NIH)   espacial  Mariner  1,  9  una  
<algoritmo>,  118 afirmación  por  prueba,  17–18  
biblioteca  Boost,  122   código  de  producción,  19–22  
comparación  de  dos  secuencias,  122   departamento  de  control  de  
estructuras  de  datos  concurrentes,  123   calidad,  14–15  seguridad­  
utilidades  de  fecha  y  hora,  123   funciones  críticas,  10  errores  de  
biblioteca  de  sistema  de   software,  10  calidad  
archivos,  123   del  software,  22  calidad  
paralelización,  120   del  código  de  prueba,  15  
biblioteca  de  rango,  123  biblioteca  de   dobles  de  prueba,  25  
expresiones  regulares,  123  clasificación  y  salida,  123 pirámide  de  prueba,  11–12  
Therac­25,  10  

O código  de  terceros,  18  
semáforo,  22  
Clases  de  orientación  a  objetos   pruebas  basadas  en  UI,  
(OO) 12  prueba  unitaria ,  13,  
Principio  de  dependencia  acíclica,  150–153  clases   16–17  xUnit,  
anémicas,  163  definición,   14  Puntero  a  implementación  (PIMPL),  267  POUT.  
135 Consulte  Pruebas  unitarias  simples  (POUT)

289
Machine Translated by Google

■  ÍNDICE

Preprocesador,  82 semántica,  95  
Principio  del  menor  asombro  (POLA/PLA),  39 desarrolladores  de  software,  85  
Principio  de  la  menor  sorpresa  (POLS),  39 pila,  86  std,  
Optimizaciones  guiadas  por  perfiles,  60 99  
comportamiento  indefinido,  110

R Información  de  tipo  de  tiempo  de  ejecución  (RTTI),  145–146

Algoritmo  inverso,  118
La  adquisición  de  recursos  es  instancia  de  inicialización  
S
(RAII),  87   Principio  de  separación  de  preocupaciones  (SoC),  245,  250  método  
nuevos  y  borrados  explícitos,  93   setLoyaltyDiscountInPercent(),  49
recursos  propietarios,  94  punteros   Setter  inyección,  229
inteligentes,  87  estándar,   Cirugía  de  escopeta,  35
88–90,  93  tipos,   Barras  laterales,  5

87 Simula­67,  133
Deducción  de  tipo  automático  del   Principio  de  Responsabilidad  Única  (SRP),  35,  137
compilador   Desarrollo  de  software
de  administración  de  recursos,  105   Boy  scouts,  40  
cálculos,  107  principios,   ventajas  de  ocultar  
102  plantillas   información,  29  
variables,  109  preocupaciones   dirección  automática  de  puertas,  30–32  
transversales  excepción  básica­ tipos  de  enumeraciones,  31  
seguridad,  125  cláusula  atrapada,   pieza  de  código,  29  
131  referencia   dependiente  del  lenguaje  de  programación,  30
constante,  131  no  excepción­ principio  KISS,  28  
seguridad,  125  cliente  no   acoplamiento  débil,  35–38  
encontrado,  129  no  ­garantía  de   optimizaciones,  39
tiro,  127 POLA/PLA,  39  
Programador  pragmático,  129   cohesión  fuerte,  32–35  cohesión  
prevención,  125   débil,  35
seguridad  de  excepción  fuerte,  125–126   Escopeta  antipatrón,  34
manejo  de  transacciones,  124  tipos   YAGNI,  28
de  excepción  específicos  del  usuario,  130   Entropía  del  software,  2
montón,  86   Big  Ball  Of  Mud,  1  código  
lvalue  y  rvalue,  96 de  olor,  48,  58
Orbitador  climático  de  Marte,  116 Patrón  de  objeto  de  caso  especial,  66
Síndrome  NIH   Funciones  especiales  de  los  miembros,  21–22
<algoritmo>,  118 El  fallo  de  sustitución  no  es  un  error  (SFINAE),
biblioteca  Boost,  122   262–265
comparación  de  dos  secuencias,  122   Sistema  bajo  prueba  (SUT),  15
estructuras  de  datos  concurrentes,  123  
utilidades  de  fecha  y  hora,  123  
biblioteca  de  sistema  de  
T
archivos,  123  paralelización,   Metaprogramación  de  plantillas  (TMP),  3,  171
120  biblioteca  de  rango,   composición  y  funciones  de  orden  superior,  168  definición,  
123  biblioteca  de  expresiones  regulares,   171,  172  fácil  de  probar,  
123  clasificación  y  salida,  123 169  datos  inmutables,  
168  sin  efectos  secundarios,  
instancia  RAII,  87   168  paralelización,  169  
nuevos  y  eliminar  explícitos,  93   plantilla  variádica,  66
recursos  propietarios,  94  punteros  
inteligentes,  87  estándar,   Dobles  de  prueba,  22
88–90,  93  tipos,   Ventajas  del  desarrollo  basado  en  pruebas  
87 (TDD),  213–214  definición,  
Regla  de  cero,  102   191
referencias  de  valor  de  r,  97 POUT,  192–193

290
Machine Translated by Google

■  ÍNDICE

creación  de  prototipos,  215 asociación,  278–280  
código  de  números  romanos  kata agregación,  279  
Convertidor  de  números  árabes  a  romanos agregación  compuesta,  280  
TestCase.cpp,  198   creación,  278,  279  
función  modificada,  200   uso,  279  
caracteres,  197   notación  esférica,  277  clase,  273–
limpieza,  207–210   275  componentes,  
duplicaciones  de  código,   282  dependencia,  
204  función  de  conversión,   281–282  agregación  
199  función  convertArabicNumberToRoman   compuesta  creación,  282  
Numeral,  201  aserción   uso,  282  
personalizada,  205–207  
ejecución,  198–199   generalización ,  280–281
prueba  unitaria  fallida,   Especificación  de  instancia,  280  
200  GitHub,  211  – interfaz,  275–277  
213  decisiones  if­else,   realización  de  interfaz,  276  
203  regla  de  mapeo,   estereotipos,  283  
210  movimiento  de  artesanía  de  software,  196   multiplicidad,  279,  280
concatenación  de  cadenas,   Lenguaje  de  modelado  unificado  (UML),  7
202  notación  de  resta,  210   Clase  UserOfConversionService,  25
bucle  while,  202   Clases  de  utilidad,  219
flujo  de  trabajo  de,  193–
196  pirámide  de  
prueba,  11   W
acoplamiento  apretado,  36  TMP.  Ver  Metaprogramación  de  plantillas  Sitio  
(TMP) web  y  repositorio  de  código  fuente,  7
Función  Win32  CreateWindowEx(),  62

U,  V
Agregación  de  lenguaje  de  modelado   X,  Y,  Z
unificado  (UML),  279 Xerox  PARC,  134

291

También podría gustarte