A que estar atento en los namespaces de PHP 5.3
Introducción
Se viene PHP 5.3 así como quedo, algo feo para mi gusto. Veamos a que atenerse en cuanto a namespaces.
Los namespaces de PHP son bastante distintos a similares construcciones de otros lenguajes. Consideremos este escenario: dos desarrolladores PHP están trabajando en componentes distintos, sin posibilidad de saber uno lo que hace el otro, ¿cómo evitar que al integrar los dos componentes falle la aplicación por colisiones de nombres? Una opción es hacer lo que venimos haciendo hasta ahora, prefijar todos los nombres con el nombre del componente. La nueva opción es indicar como namespace el nombre del componente, que básicamente es lo mismo que el prefijo (porque más que eso no hace).
Namespace ahora es un keyword
Y sí, rompieron la compatibilidad hacia atrás, y no es un cambio mayor de versión.
Después de varias idas y vueltas, cotejando el impacto de usar un keyword u otro, quedo namespace como keyword. No importa cual hubieran elegido entre los dos favoritos, package y namespace, cualquier de los dos son palabras de uso frecuente (por ejemplo, xml tiene namespaces). Cualquier clase que tuviera como nombre de variable a namespace, ahora va a dejar de funcionar. JODANSE.
Consejo: nada que hacer. Refactoring. Con suerte no usas namespace como nombre.
Lo que hubiera estado bueno: no hacia falta tomar un keyword nuevo. Había opciones que no requerían un statement para indicar el namespace. Y si no quedaba otra… hubiera sido para PHP6 el cambio.
El namespace implicito y especial de nombres internos
Segundo gotcha! del tema de los namespaces. Hay todo un conjunto de nombres internos, que corresponden a funciones y clases de la librería estándar y extensiones, que debería ser “visible” desde cualquier namespace, para que uno pueda fácilmente refactorizar código sin namespace a código con namespace. O sea, si yo indico que trabajo en un namespace igual quiero poder usar count(), strlen(), etc tal y como siempre lo hice.
El tema acá es que una función o clase interna se puede “sobrecargar” en un namespace, o sea, redefinirla. Esto es útil para los componentes que definen su propia clase de excepciones, por ejemplo. Pero hay que estar atento que la función count() adentro de un namespace puede que no devuelva la cantidad de elementos de un array sino lo que al desarrollador de ese componente le parecio que count() debía hacer.
Consejo: usar namespaces solo para clases, evitar declarar funciones dentro de namespaces a menos que uno sepa exactamente lo que está haciendo. También uno puede referirse inequívocamente a la función del espacio interno anteponiendo ::, por ejemplo, ::count().
Lo que hubiera estado bueno: que no se pueda sobrecargar los nombres del espacio interno, pero que exista un namespace inicial con ciertos nombres que resulta práctico sobrecargar, por ejemplo: Exception. De esta manera se evitan prácticas que pueden resultar perjudiciales y no tienen sentido (como sobrecargar las funciones básicas), y se le da la flexibilidad al desarrollador para sobrecargar los nombres que si tiene sentido sobrecargar.
Colisión de nombres no detectada, función pisa metodo estatico
Acá la pregunta que seguro se van a hacer todos es “¿cómo? ¿no era que con esto resolvíamos el tema de las colisiones de nombres?”, bueno, no del todo… hay un “pequeño” problema.
Supongamos que tengo una función llamada bar() en el namespace test::foo. Ahora supongamos que tengo una clase llamada foo, que tiene un método estático llamado bar(), y se encuentra en el namespace test. Históricamente en PHP los nombres de funciones y nombres de métodos no tenían chances de colisionar, pero… a ver quien me dice a que corno apunta test::foo::bar()!
Lo cierto es que esto que acabo de ejemplificar es un caso de colisión de nombres del que php5.3 no te da aviso alguno. Si intentas el ejemplo, PHP va a llamar a la función, no al método estático. De manera que ahora existe una manera de “pisar” métodos estáticos, o sea, desde otro namespace se vuelve inaccesible el método estático de la clase.
Consejo: no declarar funciones en namespaces (¿para qué?) o evitar en lo posible anidar namespaces.
Lo que hubiera estado bueno: realmente no hacia falta namespaces que contuvieran funciones ni tampoco varios niveles de anidación. Cuanto más simple, mejor.
Multiples namespaces en un mismo archivo
No se si esto esta documentado o explicado en detalle en la documentación, pero se puede declarar el namespace más de una vez en una archivo. Esto fue para resolver una situación muy ad-hoc y es un moco feo que no debería haber llegado a producción. Se puso para que se pudieran concatenar archivos php para generar un único archivo que contuviera todo, una optimización medio fea que ya no tiene sentido teniendo a disposición phar.
Consejo: por el amor de todo lo que es buen diseño, ¡NO DECLAREN EL NAMESPACE MÁS QUE UNA SOLA VEZ POR ARCHIVO! Y castiguen a todo el que lo haga.
Lo que hubiera estado bueno: que quitaran esto.
El “use” (import/alias de nombres) solo funciona para clases y namespaces.
Otra de las cosas que no se arreglaron. El use es la herramienta más útil que nos da esto de los namespaces, y en realidad ni siquiera nos hace falta usar el resto. El “use” es básicamente un generador de alias de nombres, con el podemos hacer de esos nombre realmente largo en nombre cortos, por ejemplo:
use Really_Long_Long_Long_Name as ShortName;
Pero hay un tema, solo se puede renombrar clases y namespaces, pero no así funciones ni constantes, que son elementos que también se encuentran dentro de un namespace. O sea, sirve para acortar nombres de clases, que es genial y que es lo que queríamos, pero no para acortar nombres de funciones o constantes. Con esto surge la pregunta: entonces, ¿para qué esta la opción de tener funciones y constantes dentro de un namespace?
Consejo: creo que lo dije ya, esto de los namespaces en PHP solo viene bien para usarlos con clases.
Lo que hubiera estado bueno: que se enfocaran más en esta herramienta que era lo que realmente importaba.
El “use” no permite importar todo un namespace
Esto es lo que todos los que usan namespaces en otros lenguajes van a pedir seguro, pero no hay caso… esto es PHP! (cualquier alusión a la novela gráfica 300 es pura coincidencia). Hay que entender que el use no es un import, en realidad es solo una manera de asignar nombres más cortos dentro del scope de un archivo php.
Impacto en performance
Infaltable este tema. La promesa era que la implementación de namespaces no iba a afectar la performance, que el mayor impacto iba a ocurrir en la compilación, de manera que con un opcode cache se podía salvar el overhead. Lo cierto es que esto no es así, algo hay que pagar por el uso de los namespaces. De todas maneras estamos hablando de mediciones en el rango de las millonesimas de segundo. No hay sistema PHP que tenga suficiente procesamiento como para que se note realmente el impacto del uso de namespaces. Igual vale la pena notar que nada es gratis.
En los análisis de tiempo que hice observe impactos de performance en llamadas a métodos estáticos y en la construcción de objetos, lugares donde la lógica de resolución de nombres tiene sus vueltas (en realida solo esperaba un impacto importante en las llamadas estáticas, no comprendo porque hay un overhead importante con la construcción de objetos).
De PHP5.2 a PHP5.3 sin declarar namespace, el impacto en las llamadas a métodos estáticos es mínimo. Tarda un 4% más (que es solo 1 millonésima de segundo medido en mi pc: de 25 millonésimas a 26).
Declarando un namespace, el impacto en las llamadas a métodos estáticos es de un 20% más de tiempo (5 millonésimas de segundo en mi PC: de 25 millonésimas a 30).
Declarando un namespace, el impacto en la construcción de objetos es de un 33% más de tiempo (3 millonésimas de segundo en mi PC: de 9 millonésimas a 12).
Consejo: no creo que haya que preocuparse por el overhead, pero este existe, cuando se empiece a probar en producción se vera si hay que considerar o no el impacto en performance. Si no es aceptable el overhead, no hace falta usar namespaces, con el prefijo en nombres nos veníamos arreglando bastante bien, y el “use” alivia el tema de los nombres largos aún si no usamos namespaces (notese que el “use” se resuelve siempre en compilación así que el overhead lo amortigua el opcode cache, por lo que no es una preocupación).
¿Y los features de encapsulación?
La otra pregunta que varios se van a hacer es qué sentido tienen los namespaces si no puedo ajustar los limitadores de visibilidad que me permiten manejar la encapsulación. Así como una clase tiene funciones privada, protegidas y publicas, un namespace debería poder indicar que nombres se exportan y que nombres son internos al paquete.
PD: Desactualización de las IDEs
La lógica de resolución de nombres no es nada parecido a cuanto se viene haciendo en PHP. Las IDEs van a tener que actualizarse, y esta actualización no es coser y cantar.
Mitos de la programación… PHP: require vs require_once
Se que más adelante voy a lamentar andar mirando el twitter de phpsenior en lugar de dedicarme a adelantar trabajo atrasado, pero bueh… ya está hecho el daño. Ví este artículo sobre varias curiosidades de PHP y me detuve un segundo sobre uno en particular que me llamó la atención. En uno de los puntos asegura que require es más rápido que require_once… esto es mentira, un mito del mundo PHP… incluso, por alguna razón que desconozco, en las últimas versiones de PHP, require_once funciona de manera más óptima que require. Deben haber estado tweakeandolo, ya que se recomienda el uso de require_once sobre require (para cargar includes que solo deberían cargarse una vez).
De todas maneras, nunca hubo una ventaja en velocidad de procesamiento por usar un keyword u el otro, (salvo recientemente que parece que se optimizo uno y el otro no). Explico porque. Tuve la oportunidad de andar investigando como funciona internamente el zend engine hace un año cuando había mucho revuelo por el tema de los namespaces en php, e inocentemente pense que andar tocando el engine no podía ser tan difícil, solo hacía falta sacarle el polvo a mis habilidades en C… En fín, esa es otra historia.
En teoría no debería haber cambiado mucho la lógica que distingue a require de require_once desde la última vez que la ví. Básicamente, se utiliza una hash table (de las mismas que se usan para los arrays) para recordar que archivos fueron cargados y cargarlos de vuelta cuando se usa require_once. Lo que sucede es que ambos hacen la busqueda del bucket en la hash table, porque tanto require como require_once tienen que anotar que el archivo fue incluido. La busqueda en la hash table es el trabajo pesado ahí, después fallar si se encontro el archivo, no hace diferencia a la performance. En definitiva la diferencia entre require y require_once es minima, no hay ninguna “llamada extra al sistema”, eso es un mito del mundo php.
Ahora, hay muchas “resultados estadisticos” generados por entusiastas del tema que miden el tiempo de ejecución y comparan resultados. Hay algunos que le dan la razón a la versión mito y otros que muestran lo contrario. El problema con estos resultados, como cualquier medición de este estilo, es que hay que tener cuidado en observar bien que es lo que realmente se está midiendo.
Con require y require_once hay un problema en los resultados de la medición: el acceso a disco. Estas operaciones estan sujetas a la disponibilidad del sistema operativo (recordemos que el tiempo de procesador es compartido en sistemas multitarea), y en comparación con los tiempos reales que tarda lo que realmente queremos medir, el tiempo ocupado por el IO es mucho mayor. O sea, los resultados obtenidos son poco estables y poco confiables… pero podríamos salvarlo si evitaramos el acceso a disco.
Para esto algunos a recurrido a los mismos sistemas que se usan en producción para evitar el acceso a disco (APC, memcache, etc), pero estos también son metodos que están sujetos a los caprichos del sistema operativo. Lo mejor sería evitar completamente el acceso a disco… Y hay una manera.
Otro de los features poco usados de PHP son los stream wrappers. Esto te permite crear tipos de “streams”, objetos utilizados en operaciones de IO. En el manual de php hay un ejemplo de stream wrapper que implementa streams para variables globales, y lo interesante es que podemos usarlos con require y require_once. Use este ejemplo del manual de php para hacer un profile de require y require_once que no considere el acceso a disco:
function mean($data) {
sort($data);
$error = ceil(count($data) * 0.03);
$data = array_slice($data, $error, -$error);
return array_sum($data) / count($data);
}
$n = 1000;
for ($i = 0; $i < $n; $i++) {
$GLOBALS["var$i"] = '';
}
$times = array();
for ($i = 0; $i < $n; $i++) {
$file = "var://var$i";
$start = microtime(true);
require $file;
$end = microtime(true);
$times[] = $end - $start;
}
printf("%f\n", mean($times));
Al correr el test con estos valores los resultados se muestran muy estables. Estos son (en mi computadora):
require: 0.000037 segundos
require_once: 0.000023 segundos
Hacer la prueba para include e include_once me reporta los mismos resultados, como era de esperarse (ya que tampoco hay una diferencia de importancia entre ambos… salvo cuando ocurre un error). Lo que me resulto extraño es encontrar un diferencia a favor de require_once… seguramente deben haber estado realizando mejoras que no se aplicaron por igual… vaya uno a saber… el zend engine es spaghetti code.
Mitos de la programación – ¿OOP? II
Siguiendo con el post anterior. Esto me recuerda uno de los mitos más difíciles de desterrar de la programación orientada a objetos, y uno de los más polémicos. En general un buen diseño en objetos te asegura un buen grado de reusabilidad, pero no es una garantía. Decir que trabajar con objetos te asegura la reusabilidad es una exageración, una falacia. No alcanza con usar programación orientada a objetos para hacer tu código reusable, y tampoco un lenguaje que no provee features del OOP está menos predispuesto a la reusabilidad solo por el hecho de no soportar OOP.
Por ahí acá resulta medio contradictorio lo que digo, siendo que es inevitable para mi hablar de reusabilidad cuando hablo del paradigma de objetos. En parte esto es porque no considero como un uso apropiado del paradigma aquello que no busque una buena reusabilidad de los algoritmos creados. Sin embargo hay que evitar enseñar a los que introducen en este paradigma que la reusabilidad se obtiene sin costo alguno. En particular hay que evitar dar a entender la reusabilidad como una característica del OOP. Se puede hacer tanto código reusable como pasta-code en OOP como en paradigma procedural o en el paradigma funcional.
En pocas palabras, la reusabilidad no es una característica del OOP, no hay garantías de ello prácticamente en ningún paradigma. PERO, diseñar a partir de simular los procesos reales (característico de OOP), sumado a un cuidado diseño orientado a la reusabilidad, generalmente concluyen en un sistema fácilmente mantenible.
En el foro de ADVA tuvimos con algunos usuarios una discusión bastante acalorada sobre el lenguaje C++ y en general sobre ciertos aspectos del paradigma de objetos. Uno de los temas tocados fue sobre la reusabilidad. Quiero extraer algunos fragmentos de esta discusión. Por ejemplo acá comento sobre la crítica academica al OOP en temas de reusabilidad y la propuesta del GoF y los patrones de diseño:
Algo en lo que insisto siempre es en destronar el mito del OOP como facilitador de la reusabilidad. Usar OOP no garantiza reusabilidad, por el contrario, una de las críticas académicas más fuertes al OOP es que la reusabilidad por herencia resulta en malas prácticas que afecta la mantenibilidad (esa palabra es un trabalenguas) del sistema. En contraposición, el paradigma procedural tiene un modelo de reusabilidad mucho más organizado, que genera una mayor independencia entre componentes.
Sobre esto, el “Grupo de los Cuatro” (Gang of Four) popularizo los términos de “white-box reuse” (reusabilidad a caja abierta) y “black-box reuse” (reusabilidad a caja cerrada). La reusabilidad a caja abierta, o reusabilidad por herencia, es la que es criticada por generar dependencias entre los componentes a la hora de hacer tareas de mantenimiento. La reusabilidad a caja cerrada, o reusabilidad por interfases, es la usada en el paradigma procedural y la que el GoF propone recuperar en el paradigma OO mediante patrones de diseño.
Ahí tengo que admitir que elegí mal las palabras. Puntualmente el OOP, bien usado, puede resultar un facilitador de la reusabilidad. El punto importante es que la reusabilidad no es una garantía, en ningún paradigma de la programación. En particular es una crítica académica que el OOP favorece practicas incorrectas de reusabilidad, pero esto está originado en el mismo ámbito académico que enseña mal el paradigma de objetos.
Me tome el tiempo de releer los capítulos iniciales del libro de Stroustrup. En ningún momento plantea el paradigma de objetos como una mejora a la reusabilidad, lo cual era de esperarse, sería una promesa ridícula. En el prefacio habla del OOP como una técnica de abstracción de datos más “flexible y eficiente”, lo cual es cierto. Dice “Cuando se les utiliza bien, estas técnicas producen programas más cortos y más fáciles de comprender y mantener”. Esto último es en general cierto con el paradigma de objetos. El punto clave es el “buen uso”. Después habla del type safety sobre todo.
En el primer capítulo hace una reseña filosófica sobre el lenguaje, donde dice que el lenguaje C está pensado como un “lenguaje cercano a la máquina”, y los agregados que se hicieron en C++ son para hacer un “lenguaje cercano al problema a resolver”. Hace una reseña bastante interesante que quiero compartir:
“El lenguaje proporciona al programador un conjunto de herramientas conceptuales; si éstas resultan inadecuadas para una tarea, sencillamente se las ignorara. (…) No se puede garantizar el buen diseño y la ausencia de errores sólo por la presencia o ausencia de características específicas del lenguaje.”
Es rescatable esto último que dice Stroustrup, y sobre como la intención de C++ es acercar al programador al problema a resolver, esto es en realidad cierto no solo para C++ sino para el paradigma de objetos en general.
Continuo más adelante sobre el tema para no alargar innecesariamente el post.