Clase con un solo método: ¿el mejor enfoque?

173

Digamos que tengo una clase destinada a realizar una sola función. Después de realizar la función, se puede destruir. ¿Hay alguna razón para preferir uno de estos enfoques?

// Initialize arguments in constructor
MyClass myObject = new MyClass(arg1, arg2, arg3);
myObject.myMethod();

// Pass arguments to method
MyClass myObject = new MyClass();
myObject.myMethod(arg1, arg2, arg3);

// Pass arguments to static method
MyClass.myMethod(arg1, arg2, arg3);

Estaba siendo intencionalmente vago sobre los detalles, para tratar de obtener pautas para diferentes situaciones. Pero realmente no tenía en mente funciones de biblioteca simples como Math.random (). Estoy pensando más en las clases que realizan tareas específicas y complejas, pero solo requieren un método (público) para hacerlo.

JW
fuente

Respuestas:

264

Solía ​​amar las clases de utilidad llenas de métodos estáticos. Hicieron una gran consolidación de los métodos auxiliares que de otra manera se encontrarían causando redundancia y un infierno de mantenimiento. Son muy fáciles de usar, no se crean instancias, no se eliminan, solo dispara y olvida. Supongo que este fue mi primer intento involuntario de crear una arquitectura orientada a servicios: muchos servicios sin estado que solo hicieron su trabajo y nada más. Sin embargo, a medida que el sistema crece, vendrán dragones.

Polimorfismo
Digamos que tenemos el método UtilityClass.SomeMethod que felizmente suena. De repente, necesitamos cambiar ligeramente la funcionalidad. La mayor parte de la funcionalidad es la misma, pero debemos cambiar algunas partes. Si no hubiera sido un método estático, podríamos hacer una clase derivada y cambiar el contenido del método según sea necesario. Como es un método estático, no podemos. Claro, si solo necesitamos agregar funcionalidad antes o después del método anterior, podemos crear una nueva clase y llamar a la anterior dentro de ella, pero eso es simplemente asqueroso.

Problemas de interfaz
Los métodos estáticos no se pueden definir a través de interfaces por razones lógicas. Y dado que no podemos anular los métodos estáticos, las clases estáticas son inútiles cuando necesitamos pasarlos por su interfaz. Esto nos hace incapaces de usar clases estáticas como parte de un patrón de estrategia. Podríamos solucionar algunos problemas pasando delegados en lugar de interfaces .

Prueba
Esto básicamente va de la mano con los problemas de interfaz mencionados anteriormente. Como nuestra capacidad de intercambiar implementaciones es muy limitada, también tendremos problemas para reemplazar el código de producción con código de prueba. Nuevamente, podemos envolverlos, pero requerirá que cambiemos grandes partes de nuestro código solo para poder aceptar envoltorios en lugar de los objetos reales.

Fomenta los blobs
Como los métodos estáticos generalmente se usan como métodos de utilidad y los métodos de utilidad generalmente tendrán diferentes propósitos, terminaremos rápidamente con una gran clase llena de funcionalidad no coherente; idealmente, cada clase debería tener un único propósito dentro del sistema . Prefiero tener cinco veces más clases siempre que sus propósitos estén bien definidos.

Parámetro de fluencia
Para empezar, ese pequeño método estático lindo e inocente podría tomar un solo parámetro. A medida que la funcionalidad crece, se agregan un par de parámetros nuevos. Pronto se agregan parámetros adicionales que son opcionales, por lo que creamos sobrecargas del método (o simplemente agregamos valores predeterminados, en idiomas que los admiten). En poco tiempo, tenemos un método que toma 10 parámetros. Solo los primeros tres son realmente necesarios, los parámetros 4-7 son opcionales. Pero si se especifica el parámetro 6, también se deben completar 7-9 ... Si hubiéramos creado una clase con el único propósito de hacer lo que hizo este método estático, podríamos resolver esto tomando los parámetros requeridos en el constructor, y permite al usuario establecer valores opcionales a través de propiedades o métodos para establecer múltiples valores interdependientes al mismo tiempo. Además, si un método ha crecido a esta cantidad de complejidad,

Exigir a los consumidores que creen una instancia de clases sin motivo.
Uno de los argumentos más comunes es, ¿por qué exigir que los consumidores de nuestra clase creen una instancia para invocar este método único, sin tener que usar la instancia después? Crear una instancia de una clase es una operación muy muy barata en la mayoría de los idiomas, por lo que la velocidad no es un problema. Agregar una línea adicional de código al consumidor es un costo bajo para sentar las bases de una solución mucho más fácil de mantener en el futuro. Y, por último, si desea evitar la creación de instancias, simplemente cree un contenedor singleton de su clase que permita una fácil reutilización, aunque esto exige que su clase no tenga estado. Si no es apátrida, aún puede crear métodos de envoltura estática que manejen todo, al tiempo que le brindan todos los beneficios a largo plazo. Finalmente,

Solo un Sith trata en términos absolutos
Por supuesto, hay excepciones a mi disgusto por los métodos estáticos. Las verdaderas clases de utilidad que no presentan ningún riesgo de hinchazón son casos excelentes para los métodos estáticos: System.Convert como ejemplo. Si su proyecto es único, sin requisitos para el mantenimiento futuro, la arquitectura general realmente no es muy importante, estática o no estática, realmente no importa; sin embargo, la velocidad de desarrollo sí lo es.

¡Estándares, estándares, estándares!
El uso de métodos de instancia no le impide usar también métodos estáticos, y viceversa. Mientras haya un razonamiento detrás de la diferenciación y esté estandarizado. No hay nada peor que mirar una capa de negocios en expansión con diferentes métodos de implementación.

Mark S. Rasmussen
fuente
Como dice Mark, el polimorfismo, poder pasar los métodos como parámetros de "estrategia" y poder configurar / configurar opciones son todas razones para usar una instancia . Por ejemplo: un ListDelimiter o DelimiterParser podría configurarse en cuanto a qué delimitadores usar / aceptar, si se debe recortar el espacio en blanco de los tokens analizados, si se debe poner entre corchetes la lista, cómo tratar una lista en blanco / nula ... etc.
Thomas W
44
"A medida que un sistema crece ..." ¿refactoriza?
Rodney Gitzel
3
@ user3667089 Argumentos generales necesarios para que la clase exista lógicamente, aquellos que pasaría en el constructor. Estos normalmente se utilizarían en la mayoría / todos los métodos. Argumentos específicos del método que pasaría en ese método específico.
Mark S. Rasmussen
1
Realmente, lo que está haciendo con una clase estática es volver a la C sin procesar, no orientada a objetos (aunque administrada por la memoria): todas las funciones están en un espacio global, no existe this, debe administrar el estado global, etc. si te gusta la programación de procedimientos, haz esto. Pero aprecie que luego pierde muchos de los beneficios estructurales de OO.
Ingeniero
1
La mayoría de estos motivos tienen que ver con la mala gestión del código, no por el uso de métodos estáticos. En F # y otros lenguajes funcionales usamos métodos básicamente estáticos (funciones sin estado de instancia) en todas partes y no tenemos estos problemas. Dicho esto, F # proporciona un mejor paradigma para usar funciones (las funciones son de primera clase, las funciones pueden ser currificadas y parcialmente aplicadas) lo que las hace más factibles que en C #. Una razón más importante para usar las clases es porque para eso está construido C #. Todas las construcciones de Inyección de dependencias en .NET Core giran en torno a clases de instancia con deps.
Justin J Stark
89

Prefiero la forma estática. Como la clase no representa un objeto, no tiene sentido hacer una instancia de él.

Las clases que solo existen para sus métodos deben dejarse estáticas.

jjnguy
fuente
19
-1 "Las clases que solo existen para sus métodos deben dejarse estáticas".
Novato
8
@Rookian, ¿por qué no estás de acuerdo con eso?
jjnguy
66
Un ejemplo sería el patrón de repositorio. La clase contiene solo métodos. Seguir su enfoque no permite usar interfaces. => aumentar el acoplamiento
Rookian
1
@Rook, en general las personas no deberían crear clases que se usen solo para métodos. En el ejemplo que diste, los métodos estáticos no son una buena idea. Pero para métodos de utilidad simples, la forma estática es la mejor.
jjnguy
9
Absolutamente correcto :) Con su condición "para métodos de utilidad simples", estoy completamente de acuerdo con usted, pero usted hizo una regla general en su respuesta que es incorrecta.
Novato
18

Si no hay ninguna razón para crear una instancia de la clase para ejecutar la función, utilice la implementación estática. ¿Por qué hacer que los consumidores de esta clase creen una instancia cuando no se necesita?

Ray Jezek
fuente
16

Si no necesita guardar el estado del objeto, no hay necesidad de instanciarlo en primer lugar. Iría con el método estático único al que le pasas los parámetros.

También advertiría contra una clase gigante de Utils que tiene docenas de métodos estáticos no relacionados. Esto puede ser desorganizado y difícil de manejar a toda prisa. Es mejor tener muchas clases, cada una con pocos métodos relacionados.

Bill el lagarto
fuente
6

Yo diría que el formato del método estático sería la mejor opción. Y también haría que la clase sea estática, de esa manera no tendría que preocuparse por crear accidentalmente una instancia de la clase.

Nathen Silver
fuente
5

Realmente no sé cuál es la situación aquí, pero consideraría ponerlo como un método en una de las clases a las que pertenecen arg1, arg2 o arg3. Si puede decir semánticamente que una de esas clases sería propietaria del método.

Chris Cudmore
fuente
4

Sugeriría que es difícil de responder en función de la información proporcionada.

Mi instinto es que si solo vas a tener un método y vas a tirar la clase de inmediato, entonces conviértela en una clase estática que tome todos los parámetros.

Por supuesto, es difícil decir exactamente por qué necesita crear una sola clase solo para este método. ¿Es la típica situación de "clase de utilidades" como la mayoría asume? ¿O está implementando algún tipo de clase de regla, de la cual podría haber más en el futuro?

Por ejemplo, haga que esa clase sea conectable. Luego, desearía crear una interfaz para su único método, y luego desearía que todos los parámetros pasen a la interfaz, en lugar de al constructor, pero no querría que fuera estática.

Mella
fuente
3

¿Puede su clase hacerse estática?

Si es así, lo convertiría en una clase de 'Utilidades' en la que pondría todas mis clases de una función.

Robert
fuente
2
Estoy un poco en desacuerdo. En los proyectos en los que participo, su clase de Utilidades podría tener cientos de métodos no relacionados.
Paul Tomblin
3
@Paul: De acuerdo en general. Odio ver clases con docenas (o sí, cientos) de métodos totalmente ajenos. Si se debe adoptar este enfoque, al menos divídalos en conjuntos de utilidades más pequeños y relacionados (EG, FooUtilities, BarUtilities, etc.).
John Rudy
9
una responsabilidad de clase uno.
Andreas Petersson el
3

Si este método no tiene estado y no necesita pasarlo, entonces tiene más sentido definirlo como estático. Si necesita pasar el método, podría considerar usar un delegado en lugar de uno de sus otros enfoques propuestos.

Joel Wietelmann
fuente
3

Para aplicaciones simples y internalayudantes, usaría un método estático. Para aplicaciones con componentes, me encanta el Marco de Extensibilidad Administrada . Aquí hay un extracto de un documento que estoy escribiendo para describir los patrones que encontrarás en mis API.

  • Servicios
    • Definido por una I[ServiceName]Serviceinterfaz.
    • Exportado e importado por el tipo de interfaz.
    • La aplicación host proporciona la implementación única y la consumen internamente y / o las extensiones.
    • Los métodos en la interfaz de servicio son seguros para subprocesos.

Como un ejemplo artificial:

public interface ISettingsService
{
    string ReadSetting(string name);

    void WriteSetting(string name, string value);
}

[Export]
public class ObjectRequiringSettings
{
    [Import]
    private ISettingsService SettingsService
    {
        get;
        set;
    }

    private void Foo()
    {
        if (SettingsService.ReadSetting("PerformFooAction") == bool.TrueString)
        {
            // whatever
        }
    }
}
Sam Harwell
fuente
2

Simplemente haría todo en el constructor. al igual que:

new MyClass(arg1, arg2, arg3);// the constructor does everything.

o

MyClass my_object(arg1, arg2, arg3);
pravprab
fuente
¿Qué sucede si necesita devolver algo además de un objeto de tipo MyClass?
Bill the Lizard
13
Considero una mala práctica poner la lógica de ejecución en un constructor. El buen desarrollo se trata de semántica. Semánticamente, existe un constructor para construir un objeto, no para realizar otras tareas.
mstrobl
bill, si necesita un valor de retorno, pase la referencia al valor de ret: MyClass myObject (arg1, arg2, arg3, retvalue); mstrobl, si el objeto no es necesario, ¿por qué crearlo? y este truco realmente puede ayudarte en algunos casos.
Si un objeto no tiene estado, es decir, no tiene vars, la instancia no generará ninguna asignación, ni en el montón ni en la pila. Puede pensar en los objetos en C ++ como solo llamadas a funciones de C con un primer parámetro oculto que apunta a una estructura.
mstrobl
mstrobl, le gusta la función ac en los esteroides porque puede definir funciones privadas que el ctor puede usar. También puede usar las variables de miembro de clase para ayudar a pasar datos a las funciones privadas.
0

Una cuestión más importante a considerar es si el sistema se estaría ejecutando en un entorno multiproceso y si sería seguro para subprocesos tener un método estático o variables ...

Debe prestar atención al estado del sistema.

NZal
fuente
0

Es posible que pueda evitar la situación todos juntos. Intenta refactorizar para obtenerarg1.myMethod1(arg2, arg3) . Cambie arg1 con arg2 o arg3 si tiene más sentido.

Si no tienes control sobre la clase de arg1, decóralo:

class Arg1Decorator
    private final T1 arg1;
    public Arg1Decorator(T1 arg1) {
        this.arg1 = arg1;
    }
    public T myMethod(T2 arg2, T3 arg3) {
        ...
    }
 }

 arg1d = new Arg1Decorator(arg1)
 arg1d.myMethod(arg2, arg3)

El razonamiento es que, en OOP, los datos y los métodos que procesan esos datos van juntos. Además, obtienes todas las ventajas que Mark mencionó.

Hugo Wood
fuente
0

Creo que si las propiedades de su clase o la instancia de clase no se utilizarán en los constructores o en sus métodos, no se sugiere que los métodos se diseñen como un patrón 'estático'. El método estático siempre debe pensarse en forma de "ayuda".

usuario7545070
fuente
0

Dependiendo de si solo quieres hacer algo o hacer y devolver algo, puedes hacer esto:

public abstract class DoSomethingClass<T>
{
    protected abstract void doSomething(T arg1, T arg2, T arg3);
}

public abstract class ReturnSomethingClass<T, V>
{
    public T value;
    protected abstract void returnSomething(V arg1, V arg2, V arg3);
}

public class DoSomethingInt extends DoSomethingClass<Integer>
{
    public DoSomethingInt(int arg1, int arg2, int arg3)
    {
        doSomething(arg1, arg2, arg3);
    }

    @Override
    protected void doSomething(Integer arg1, Integer arg2, Integer arg3)
    {
        // ...
    }
}

public class ReturnSomethingString extends ReturnSomethingClass<String, Integer>
{
    public ReturnSomethingString(int arg1, int arg2, int arg3)
    {
        returnSomething(arg1, arg2, arg3);
    }

    @Override
    protected void returnSomething(Integer arg1, Integer arg2, Integer arg3)
    {
        String retValue;
        // ...
        value = retValue;
    }
}

public class MainClass
{
    static void main(String[] args)
    {
        int a = 3, b = 4, c = 5;

        Object dummy = new DoSomethingInt(a,b,c);  // doSomething was called, dummy is still around though
        String myReturn = (new ReturnSomethingString(a,b,c)).value; // returnSomething was called and immediately destroyed
    }
}
Mark Walsh
fuente