¿Cuándo escribir métodos de extensión para tus propias clases?

8

Recientemente vi una base de código que tenía una clase de datos Addressdefinida en algún lugar y luego en un lugar diferente:

fun Address.toAnschrift() = let { address ->
    Anschrift().apply {
        // mapping code here...
    }
}

Me pareció confuso no tener este método en la dirección directamente. ¿Existen patrones establecidos o mejores prácticas para utilizar los métodos de extensión? ¿O el libro sobre eso todavía tiene que ser escrito?

Tenga en cuenta que a pesar de la sintaxis de Kotlin en mi ejemplo, estoy interesado en las mejores prácticas generales, ya que también se aplicaría a C # u otros lenguajes con esas capacidades.

Robert Jack Will
fuente
44
Dado que "Anschrift" es una versión alemana de "dirección", parece ser una conversión específica de país de un tipo de dirección neutral. Querer mantener el código específico del país en otro lugar tiene sentido. Esta no es una respuesta a su pregunta real, pero explica por qué lo hicieron en este ejemplo .
R. Schmitz
1
»Los métodos de extensión le permiten" agregar "métodos a los tipos existentes sin crear un nuevo tipo derivado, volver a compilar o modificar el tipo original« docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… . »Para una biblioteca de clases que implementó, no debe usar métodos de extensión ... Si desea agregar una funcionalidad significativa a una biblioteca para la que posee el código fuente, debe seguir las pautas estándar de .NET Framework para el control de versiones de ensamblado« tl; dr si posee el código, no necesita métodos de extensión.
Thomas Junk

Respuestas:

15

Los métodos de extensión no proporcionan algo que no se puede hacer de otras maneras. Son azúcar sintáctico para hacer que el desarrollo sea más agradable, sin proporcionar un beneficio técnico real.


Los métodos de extensión son relativamente nuevos. Los estándares y las convenciones generalmente se deciden por opinión popular (además de cierta experiencia a largo plazo sobre lo que termina siendo una mala idea), y no creo que estemos en el punto en el que podamos trazar una línea definitiva con la que todos estén de acuerdo.

Sin embargo, puedo ver varios argumentos, basados ​​en las cosas que escuché de mis compañeros de trabajo.

1. Generalmente están reemplazando los métodos estáticos.

Los métodos de extensión se basan más comúnmente en cosas que de otro modo habrían sido métodos estáticos, no métodos de clase. Ellos se ven como métodos de clase, pero no lo son en realidad.

Los métodos de extensión simplemente no deberían considerarse una alternativa para los métodos de clase; sino más bien como una forma de dar a los métodos estáticos de manera sintáctica una apariencia y sensación de "clase de método".

2. Cuando su método previsto es específico para un proyecto consumidor pero no la biblioteca fuente.

El hecho de que desarrolle tanto la biblioteca como el consumidor no significa que esté dispuesto a poner la lógica donde sea que encaje.

Por ejemplo:

  • Tienes una PersonDtoclase proveniente de tu DataLayerproyecto.
  • Su WebUIproyecto quiere poder convertir PersonDtoa a PersonViewModel.

No puede (y no querría) agregar este método de conversión a su DataLayerproyecto. Dado que este método tiene un alcance para el WebUIproyecto, debe vivir dentro de ese proyecto.

Un método de extensión le permite tener este método accesible globalmente dentro de su proyecto (y posibles consumidores de su proyecto), sin requerir que la DataLayerbiblioteca lo implemente.

3. Si cree que las clases de datos solo deben contener datos.

Por ejemplo, su Personclase contiene las propiedades de una persona. Pero desea poder tener algunos métodos que formateen algunos de los datos:

public class Person
{
    //The properties...

    public SecurityQuestionAnswers GetSecurityAnswers()
    {
        return new SecurityQuestionAnswers()
        {
            MothersMaidenName = this.Mother.MaidenName,
            FirstPetsName = this.Pets.OrderbyDescending(x => x.Date).FirstOrDefault()?.Name
        };
    }
}

He visto muchos compañeros de trabajo que odian la idea de mezclar datos y lógica en una clase. No estoy del todo de acuerdo con cosas simples como formatear datos, pero reconozco que en el ejemplo anterior, se siente sucio Persontener que depender SecurityQuestionAnswers.

Al poner este método en un método de extensión, se evita el ensuciamiento de la clase de datos pura.

Tenga en cuenta que este argumento es de estilo. Esto es similar a las clases parciales. Hacerlo tiene poco o ningún beneficio técnico, pero le permite separar el código en varios archivos si lo considera más limpio (por ejemplo, si muchas personas usan la clase pero no se preocupan por sus métodos personalizados adicionales).

4. Métodos auxiliares

En la mayoría de los proyectos, tiendo a terminar con clases de ayuda. Estas son clases estáticas que generalmente proporcionan una colección de métodos para formatear fácilmente.

Por ejemplo, una empresa en la que trabajé tenía un formato de fecha y hora particular que querían usar. En lugar de tener que pegar la cadena de formato en todo el lugar, o hacer que la cadena de formato sea una variable global, opté por un método de extensión DateTime:

public static string ToCompanyFormat(this DateTime dt)
{
    return dt.ToString("...");
} 

¿Es eso mejor que un método auxiliar estático normal? Creo que sí. Limpia la sintaxis. En vez de

DateTimeHelper.ToCompanyFormat(myObj.CreatedOn);

Yo podría hacer:

myObj.CreatedOn.ToCompanyFormat();

Esto es similar a una versión fluida de la misma sintaxis. Me gusta más, a pesar de que no hay un beneficio técnico de tener uno sobre el otro.


Todos estos argumentos son subjetivos. Ninguno de ellos cubre un caso que de otra manera nunca podría estar cubierto.

  • Cada método de extensión puede reescribirse fácilmente en un método estático normal.
  • El código se puede separar en archivos usando clases parciales, incluso si los métodos de extensión no existieran.

Pero, de nuevo, podemos tomar su afirmación de que nunca son necesarios al extremo:

  • ¿Por qué necesitamos métodos de clase, si siempre podemos hacer métodos estáticos con un nombre que indique claramente que es para una clase en particular?

La respuesta a esta pregunta, y la suya, sigue siendo la misma:

  • Mejor sintaxis
  • Mayor legibilidad y menos pedantería de código (el código llegará al punto en menos caracteres)
  • Intellisense proporciona resultados contextualmente significativos (los métodos de extensión solo se muestran en el tipo correcto. Las clases estáticas se pueden usar en todas partes).

Sin embargo, una mención extra particular:

  • Los métodos de extensión permiten un estilo de codificación de encadenamiento de métodos; que se ha vuelto más popular últimamente, a diferencia de la sintaxis de envoltura del método más vainilla. Se pueden ver preferencias sintácticas similares en la sintaxis de métodos de LINQ.
  • Si bien el encadenamiento de métodos también se puede hacer usando métodos de clase; tenga en cuenta que esto no se aplica a los métodos estáticos. Los métodos de extensión se basan más comúnmente en cosas que de otro modo habrían sido métodos estáticos, no métodos de clase.
Flater
fuente
55
El punto 2. es el que me parece más convincente, y la razón por la que lo hice yo mismo. A veces tiene una clase que se usa ampliamente en toda su base de código, y un método que parece que "debería" estar en la clase, excepto que involucra algún proyecto o biblioteca de terceros que desea encapsular. Los métodos de extensión para sus propias clases son una forma lógica de manejar esto.
Astrid
@Astrid: Creo que esa es la razón más técnica para hacerlo; si.
Flater
Muy buena respuesta. Un caso de uso particular de C # es que las interfaces no tienen comportamiento. Los métodos de extensión nos permiten darles métodos.
RubberDuck
¿Qué quiere decir con "beneficios técnicos"? ¿Cosas como el rendimiento? La legibilidad del código es igualmente importante, si no más.
Robert Harvey
@RobertHarvey Por técnica, me refiero a cualquier cosa que no sea solo azúcar sintáctico. Por ejemplo, difundir una clase parcial sobre muchos archivos en el mismo ensamblaje no tiene ninguna ventaja técnica. Difundir una clase y sus métodos de extensión en diferentes ensamblajes sí. La legibilidad del código, por supuesto, sigue siendo importante, pero no desde un punto de vista técnico.
Flater
5

Estoy de acuerdo con Flater en que no ha habido suficiente tiempo para que la convención se cristalice, pero sí tenemos el ejemplo de las bibliotecas de clases de .NET Framework. Tenga en cuenta que cuando los métodos LINQ se agregaron a .NET, no se agregaron directamente IEnumerable, sino como métodos de extensión.

¿Qué podemos sacar de esto? LINQ es

  1. Un conjunto coherente de funcionalidades que no siempre es necesario,
  2. está destinado a ser utilizado en el estilo de encadenamiento de métodos (por ejemplo, en A.B().C()lugar de C(B(A))), y
  3. no requiere acceso al estado privado del objeto

Si tiene las tres características, los métodos de extensión tienen mucho sentido. De hecho, yo diría que si un conjunto de métodos tiene la función # 3 y cualquiera de las dos primeras funciones, debería considerar hacerlas como métodos de extensión.

Métodos de extensión:

  1. Le permite evitar contaminar la API principal de una clase,
  2. No solo permite el estilo de encadenamiento de métodos, sino que le permite manejar nullvalores de manera más flexible ya que un método de extensión llamado desde un nullvalor simplemente recibe el nullcomo su primer argumento,
  3. Evitar que acceda / modifique accidentalmente el estado privado / protegido en un método que no debería tener acceso a él

Los métodos de extensión tienen un beneficio más: todavía se pueden usar como un método estático y se pueden pasar directamente como un valor a una función de orden superior. Entonces, si MyMethodes un método de extensión, en lugar de decir, myList.Select(x => x.MyMethod())simplemente podría usar myList.Select(MyMethod). Eso me parece más agradable.

James Jensen
fuente
44
La razón por la cual LINQ se implementó como un conjunto de métodos de extensión es que está destinado a ser un conjunto común de operaciones sobre una interfaz y no se pueden implementar implementaciones en las interfaces. Realmente no tiene nada que ver con sus puntos (1) y (2). Esto contrasta con la solución de Java para el mismo problema, que es permitir implementaciones en las definiciones de interfaz (que abre la puerta a un mundo de abuso de interfaz).
Ant P
"y no puedes poner implementaciones en las interfaces". Todavía no, pero esa declaración ya no será cierta cuando C # 8 llegue a nuestras máquinas :)
David Arno
@DavidArno realmente? He estado fuera del mundo de C # por un tiempo. Es una pena. Parece un paso atrás para mí ...
Ant P
4

La fuerza impulsora detrás de los métodos de extensión es la capacidad de agregar la funcionalidad que necesita a todas las clases o interfaces de un determinado tipo, independientemente de si están selladas o en otro conjunto. Puede ver evidencia de esto con el código LINQ, agregando funciones a todos los IEnumerable<T>objetos, ya sean listas o pilas, etc. El concepto era probar las funciones en el futuro para que si se le ocurriera el suyo IEnumerable<T>pudiera usar LINQ automáticamente con él.

Dicho esto, la función de idioma es lo suficientemente nueva como para que no tenga pautas sobre cuándo usarlas o no. Sin embargo, sí permiten varias cosas que antes no eran posibles:

  • API fluidas (consulte Afirmaciones fluidas para ver un ejemplo de cómo agregar API fluidas a casi cualquier objeto)
  • Integración de características (NetCore Asp.NET MVC usa métodos de extensión para conectar dependencias y características en el código de inicio)
  • Localización del impacto de una característica (su ejemplo en la declaración de apertura)

Para ser justos, solo el tiempo dirá qué tan lejos está demasiado lejos, o en qué punto los métodos de extensión se convierten en un obstáculo en lugar de una bendición. Como se mencionó en otra respuesta, no hay nada en un método de extensión que no se pueda hacer con un método estático. Sin embargo, cuando se usan juiciosamente, pueden hacer que el código sea más fácil de leer.

¿Qué pasa con el uso intencional de métodos de extensiones en el mismo ensamblado?

Creo que es valioso tener una funcionalidad central bien probada y confiable, y luego no jugar con ella hasta que sea absolutamente necesario. El nuevo código que depende de él, pero necesita que brinde funcionalidad únicamente para el beneficio del nuevo código, puede usar los métodos de extensión para agregar cualquier característica que admita el nuevo código. Esas funciones solo son de interés para el nuevo código, y el alcance de los métodos de extensión se limita al espacio de nombres en el que se declaran.

No descartaría ciegamente el uso de métodos de extensión, pero consideraría si la nueva funcionalidad realmente debería agregarse al código central existente que está bien probado.

Berin Loritsch
fuente
1
+1 para apis fluidos.
Robert Harvey
@RobertHarvey, ¿Mi último párrafo bajo el encabezado "dentro de la misma asamblea"? Si tiene el código fuente para ese ensamblaje, puede hacer lo que quiera con él, pero una vez que se compila, se sella.
Berin Loritsch
Eliminé mi comentario, ya que aparentemente no estaba comunicando mi punto de manera efectiva.
Robert Harvey
1

¿Existen patrones establecidos o mejores prácticas para utilizar los métodos de extensión?

Una de las razones más comunes para usar métodos de extensión es como un medio de "extender, no modificar" las interfaces. En lugar de tener IFooy IFoo2, donde este último introduce un nuevo método Bar, Barse agrega a IFootravés de un método de extensión.

Una de las razones por las que Linq usa métodos de extensión Where, Selectetc. es para permitir a los usuarios definir sus propias versiones de estos métodos, que luego son utilizados por la sintaxis de consulta de Linq también.

Más allá de eso, el enfoque que uso, que parece ajustarse a lo que comúnmente veo en .NET framework al menos es:

  1. Poner métodos directamente relacionados con la clase dentro de la clase misma,
  2. Ponga todos los métodos que no están relacionados con la funcionalidad principal de la clase en los métodos de extensión.

Así que si tengo una Option<T>, me gustaría poner cosas como HasValue, Value, ==, etc dentro del tipo en sí. Pero algo así como una Mapfunción, que convierte la mónada de T1a T2, pondría un método de extensión:

public static Option<TOutput> Map<TInput, TOutput>(this Option<TInput> input, 
                                                   Func<TInput, TOutput> f) => ...
David Arno
fuente
1
One of the reasons Linq uses extension methods for Where, Select etc is in order to allow users to define their own versions of these methods, which are then used by the Linq query syntax too. -- ¿Estás seguro de eso? La sintaxis de Linq hace un uso extensivo de las expresiones lambda (es decir, funciones de primera clase), por lo que no es necesario escribir sus propias versiones de Selecty Where. No conozco casos en los que las personas estén lanzando sus propias versiones de estas funciones.
Robert Harvey
Dicho esto, encuentro su respuesta la más convincente de todas las respuestas hasta ahora. Los métodos de extensión son una excelente manera de evolucionar las interfaces, razón por la cual se crearon para Linq.
Robert Harvey
@RobertHarvey, no tienes que hacerlo, pero puedes .
David Arno
@RobertHarvey Si cambia el if (!predicate(item))código en ese if (predicate(item)), los resultados cambian a medida que usa mi versión de Where.
David Arno
Si entiendo. Pero nadie haría esto nunca; ellos simplemente cambiarían el predicado. Ese es el punto de tener predicados: cambiar la condición sin cambiar la función.
Robert Harvey