Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 20 de enero de 2015
Framework design guidelinesHace relativamente poco tiempo, y como parte del popular salto al open source del nuevo stack de Microsoft, se publicaron en Github las directrices o normas de codificación que deben seguirse para contribuir en el desarrollo de .NET Core. Se trata de un resumen simplificado del contenido del libro “Framework design guidelines” publicado por Krzysztof Cwalina y Brad Abrams en 2008.

Aunque creo que este mismo documento o versiones anteriores lo he leído en otras ocasiones, la verdad es que sigue resultándome muy interesante porque me recuerda normas y convenciones que conviene tener en cuenta al desarrollar marcos de trabajo y componentes. No hay que olvidar que estas directrices han sido refinadas y mejoradas con los años, acumulando ya la experiencia de muchos años y muchos desarrolladores que han trabajado en .NET framework, así que se trata de una base de conocimiento nada despreciable. De hecho, estas pautas son las bases del diseño de frameworks dentro de la propia Microsoft.

Y como a menudo me encuentro con equipos que no tienen guías de estilo ni convenciones de codificación formalizadas, he pensado que quizás sería interesante traducirlas para que puedan ser usadas como punto de partida para crear sus propias normas y, en cualquier caso, siempre dan buenas ideas que podemos aplicar en el día a día para hacer mejor nuestros componentes.

Es una lectura cortita, ¡que aproveche!

1. Principios generales de diseño

1.1. Diseño guiado por escenarios

Comienza el proceso de diseño de tu API público definiendo los escenarios más importantes de cada área funcional. Escribe el código que te gustaría que escribieran los usuarios finales cuando vayan a implementar esos escenarios usando tu API. Después, diseña tu API basándote en el código de ejemplo que escribiste.

Por ejemplo, si diseñas un API que permita tiempos, podrías escribir el siguiente código de ejemplo para los escenarios:
// Escenario #1 : medir el tiempo transcurrido
Stopwatch watch = Stopwatch.StartNew();
DoSomething();
Console.WriteLine(watch.Elapsed);

// Escenario #2 : reutilizar el cronómetro
Dim watch As Stopwatch = Stopwatch.StartNew()
DoSomething();
Console.WriteLine(watch.Elapsed)
watch.Reset();
watch.Start();
DoSomething()
Console.WriteLine(watch.Elapsed)

// Escenario #3: ...

1.2. Estudios de usabilidad

Realiza pruebas de usabilidad de tu API. Haz que desarrolladores que no estén familiarizados con tu API implementen los principales escenarios, e intenta identificar qué partes no son intuitivas.

1.3. API auto-documentada

Los desarrolladores que utilicen tu API deberían ser capaces de implementar los principales escenarios sin leer la documentación. Ayuda a los usuarios a descubrir qué tipos y métodos necesitan utilizar dándoles nombres intuitivos para los tipos y miembros más frecuentes. Hablad sobre la elección de los nombres durante las revisiones.

1.4. Entiende a tu cliente

Ten en cuenta que la mayoría de tus clientes no son como tú. Deberías diseñar el API para tus clientes, no para los desarrolladores más cercanos, que, a diferencia de tus clientes, son expertos en la tecnología que estás intentando exponer.

2. Reglas de nombrado

El uso de mayúsculas y minúsculas sólo deben ser tenidas en cuenta en identificadores public y protected, así como en implementaciones privadas de miembros de interfaz. Sin embargo, los equipos de trabajo pueden elegir libremente sus propio estilo para los identificadores internal y private.
  • Utiliza “PascalCasing” (primera letra de cada palabra en mayúsculas) para todos los identificadores excepto para los nombre de parámetro. Por ejemplo, usa TextColor en vez de Textcolor o Text color.

  • Utiliza “camelCasing” (primera letra de cada palabra excepto la primera en mayúsculas) para todos los nombres de parámetro. Precede los nombres de parámetros genéricos con la letra “T”:
    public interface ISessionChannel<TSession>
        where TSession : ISession
    {
        TSession Session { get; }
    }
  • Considera utilizar simplemente T como nombre de parámetro genérico de una única letra.

  • Utiliza “PascalCasing” o “camelCasing” para acrónimos con más de dos caracteres. Por ejemplo, utiliza HtmlButton en lugar de HTMLButton, pero System.IO en lugar de System.Io.

  • No uses acrónimos que no estén aceptados generalmente en el campo o contexto de la aplicación.

  • Utiliza acrónimos bien conocidos sólo cuando sean absolutamente necesarios. Por ejemplo, puedes utilizar UI para user interface (interfaz de usuario) o Html para Hyper-Text Markup Language (lenguaje de marcas hipertexto).
  • No uses contracciones como partes de nombres de identificador. Por ejemplo, usa GetWindow en lugar de GetWin.

  • No uses guiones, ni guiones bajos ni cualquier otro tipo de caracteres alfanuméricos. 

  • No uses la Notación Húngara.
     
  • Nombra tipos y propiedades usando sustantivos o frases nominales. 

  • Nombra métodos y eventos con verbos o frases verbales. Para los eventos, elije siempre nombres que indiquen si se ha lanzado antes o después de que la acción se produzca utilizando el gerundio y el pasado perfecto, respectivamente. Por ejemplo, si un evento es lanzado antes de cerrar un formulario, debería llamarse Closing; si es lanzado después de que sea cerrado, debería llamarse Closed.

  • No utilices prefijos “Before” o “After” en el nombre de los eventos para indicar si la acción se realizó o no (cíñete al punto anterior). 

  • Utiliza I como prefijo para todos los nombres de interfaz.

  • Utiliza las siguientes terminaciones:

    • Exception para tipos que hereden de System.Exception.
    • Collection para tipos que implementen IEnumerable.
    • Dictionary para tipos que implementen IDictionary o IDictionary<K,V>.
    • EventArgs para tipos que hereden de System.EventArgs.
    • EventHandler para tipos que hereden de System.Delegate.
    • Attribute para tipos que hereden de System.Attribute.

  • No utilices las terminaciones anteriores para ningún otro tipo. 

  • No uses como terminación de nombres las palabras Flags o Enum.

  • Utiliza nombres o frases nominales en plural para las enumeraciones de flags (las que soportan operaciones de bit) y en singular para las enumeraciones normales.

  • Utiliza el siguiente esquema para los espacios de nombres:
    <Company>.<Technology>[.<Feature>].
    Un ejemplo sería Microsoft.Office.ClipGallery. Sin embargo, los componentes de sistema operativo deberían usar espacios de nombres comenzando por System.

  • No utilices jerarquías de la organización para organizar los espacios de nombre. Éstos deben corresponder con escenarios, independientemente de los equipos que hayan creado las APIs.

3. Pautas generales de diseño

  • Usa el tipo más específico para los valores de retorno, y el menos específico para los parámetros de entrada. Por ejemplo, acepta IEnumerable como parámetro de entrada, pero retorna Collection<string>

  • Provee de un claro punto de entrada a tu API para cada escenario. Cada área funcional debería tener preferiblemente uno, aunque aunque a veces sean más, tipos que actúan como puntos de entrada para explorar la tecnología. Lo llamamos “componentes agregados”. La implementación de la gran mayoría de escenarios en una tecnología determinada deberían comenzar desde uno de esos componentes agregados.

  • Escribe código de ejemplo para los escenarios de uso más frecuentes. El primer tipo usado en esos ejemplos debería ser un componente agregado y el código debería ser trivial. Si el código va más allá de unas cuantas líneas, necesitas rediseñarlo. Por ejemplo, escribir a un log de eventos en el API Win32 tomaba alrededor de 100 líneas de código; en .NET, escribir al EventLog necesita una única línea de código.

  • Modela en componentes agregados los conceptos de mayor nivel (objetos físicos) en lugar de tareas a nivel de sistema. For example File, Directory o Drive son más sencillos de comprender y utilizar que Stream, Formatter o Comparer.

  • No obligues a que los usuarios de tus APIs instancien múltiples objetos en los principales escenarios. Las tareas simples deberían hacerse con un único new.

  • Permite que tus APIs sean usadas mediante el estilo “crear-asignar-llamar” en todos los componentes agregados. Debería ser posible instanciar todos los componentes con el constructor por defecto, establecer una o más propiedades y llamar a métodos o responder a eventos:
    var applicationLog = new EventLog();
    applicationLog.Source = "MySource";
    applicationLog.WriteEntry(exception.Message);
  • No requieras demasiadas inicializaciones antes de que los componentes agregados puedan ser utilizados. Si alguna inicialización es necesaria, la excepción lanzada cuando el componente no haya sido correctamente inicializado debería explicar claramente qué hay que hacer para que funcione.
  • Elije cuidadosamente los nombres para tus tipos, métodos y parámetros. Dedica tiempo a pensar cuál es el primer nombre que la gente intentará teclear cuando esté explorando tu API, y resérvalo para tu componente agregado. Un error común es usar el “mejor” nombre para un tipo base. Pasa FxCop a tus bibliotecas.

  • Asegúrate de que tu biblioteca es CLS compliant. Aplica CLSCompliantAttribute a tus ensamblados. 

  • Ten preferencia por usar clases en lugar de interfaces.

  • No crees tipos sellados a menos que tengas buenas razones para hacerlo.

  • No crees tipos valor mutables.

  • No proveas abstracciones como interfaces o clases abstractas sin proveer al menos de un tipo concreto que implemente cada abstracción. Esto ayudará a validar el diseño.

  • No proveas interfaces sin disponer al menos de un API que lo consuma, es decir, un método que requiera un parámetro del tipo del interfaz. Esto ayudará a validar el diseño del interfaz.

  • Evita crear tipos anidados públicos.

  • Aplica el atributo FlagsAttribute a enumeraciones de flags.

  • Utiliza principalmente colecciones en lugar de arrays en los APIs públicos.

  • No utilices ArrayList, List<T>, Hashtable, o Dictionary<K,V> in APIs públicas. Utiliza en su lugar Collection<T>, ReadOnlyCollection<T>, KeyedCollection<K,V>, o los subtipos de  CollectionBaseinstead. Ten en cuenta que las colecciones genéricas son sólo soportadas en .NET framework 2.0 o superior.

  • No utilices códigos de error para notificar fallos. Usa excepciones en su lugar. 

  • No lances excepciones de tipo Exception o SystemException.

  • Evita capturar excepciones del tipo base Exception.

  • Opta preferiblemente por lanzar excepciones ya existentes de propósito general como ArgumentNullException, ArgumentOutOfRangeException o InvalidOperationException en lugar de definir tus propias excepciones. Lanza siempre la excepción más específica posible.

  • Asegúrate de que los mensajes de excepción son claros y aplicables.

  • Utiliza EventHandler<T> para eventos en lugar de definir manualmente manejadores de delegados.

  • Usa preferiblemente APIs basadas en eventos en lugar de usar delegados.

  • Usa preferiblemente constructores en lugar de métodos de factoría.

  • No expongas directamente campos públicos. Utiliza propiedades en su lugar.

  • Usa preferiblemente propiedades para almacenamiento, pero utiliza métodos en los siguientes casos:

    • Cuando la operación sea una conversión (como en Object.ToString()) .
    • Cuando la operación puede ser costosa (órdenes de magnitud más lento de lo que podría ser el establecimiento de un campo).
    • Cuando el hecho de obtener el valor de una propiedad utilizando el getter tenga un efecto lateral observable.
    • Cuando llamar varias veces consecutivas al miembro pudiera retornar distintos resultados.
    • Cuando el miembro retorne un array. Nota: los miembros que retornen un array deberían retornar copias del array interno, no una referencia al mismo.

  • Permite que las propiedades sean establecidas en cualquier orden. Las propiedades deberían ser stateless respecto a otras propiedades.

  • No hagas que un miembro sea virtuales a menos que tengas razones de peso para hacerlo. 

  • Evita los finalizadores de clases.

  • Implementa IDisposable en todos los tipos que adquieran recursos nativos, así como en los que dispongan de finalizadores.

  • Sé consistente en el orden y nombrado de los parámetros de métodos. Es frecuente encontrar métodos sobrecargados con un número creciente de parámetros para permitir al desarrollador especificar el nivel de información que necesite.

  • Asegúrate de que todas las sobrecargas muestran parámetros consistentes en su orden (el mismo parámetro se encuentra en el mismo lugar en la signatura) y nombre. Entre las sobrecargas, el único método que debería ser virtual es el que tiene más parámetros y sólo cuando la extensibilidad es necesaria.
    public class Foo
    {
        private readonly string _defaultForA = "default value for a";
        private readonly int _defaultForB = 42;
    
        public void Bar()
        {
            Bar(_defaultForA, _defaultForB);
        }
    
        public void Bar(string a)
        {
            Bar(a, _defaultForB);
        }
    
        public void Bar(string a, int b)
        {
            // core implementation here
        }
    }
  • Evita el uso de parámetros out y ref.

¡Y eso es todo! Claramente hay algunas normas a las que se les nota el paso del tiempo, e incluso puede haber alguna discutible, pero espero que os resulten interesantes y os sean útiles como base si aún no tenéis directrices claras. Y si queréis contribuir al desarrollo de .NET, son las reglas a las que hay que ceñirse ;)

Publicado en Variable not found.

7 Comentarios:

Anónimo dijo...

Buen articulo

El orden es vital para que las cosas vayan bien

Anónimo dijo...

No sé si está al día este post, pero me llama la atención que uno de los puntos sea:

No uses guiones, ni guiones bajos ni cualquier otro tipo de caracteres alfanuméricos.

Y en algún ejemplo (y en muchos proyectos que veo), aparecen variables comenzando con guiones bajos. ¿qué tipo de convención es?

José María Aguilar dijo...

Hola!

Sí, parece que las convenciones han evolucionado algo en los últimos meses. Ahora están actualizadas aquí:

https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md.

Saludos!

Anónimo dijo...

Hola!

Soy novatillo y no pillo la necesidad de un tipo anónimo en el ejemplo:

public interface ISessionChannel
where TSession : ISession
{
TSession Session { get; }
}

No es lo mismo que:

public interface ISessionChannel
{
ISession Session { get; }
}

Saludos.

Anónimo dijo...

Anónimo Anónimo dijo...
Hola!

Se come los menor y mayor que (los sustituyo por la @)

public interface ISessionChannel@TSession@
where TSession : ISession
{
TSession Session { get; }
}

¿No es lo mismo que?:

public interface ISessionChannel@ISession@
{
ISession Session { get; }
}

Saludos.

Anónimo dijo...

Hola!

Buff, se me coló el where en el ejemplo anterior.

José María Aguilar dijo...

Hola!

Bueno, al final creo que no he podido leer bien el tema, pero supongo que te refieres a los tipos parametrizados o generics.

La diferencia entre esos dos ejemplos es que, en el segundo de ellos, la propiedad Session debe ser siempre ISession, mientras que usando un parámetro genérico <T> (como en tu primer ejemplo) abres la posibilidad de que sea cualquier tipo T. Al final, los genéricos lo que te dan es una flexibilidad mucho mayor a la hora de reutilizar tus clases, interfaces o métodos.

Saludos!