¿Pasar objeto dos veces al mismo método o consolidar con una interfaz combinada?

15

Tengo un método que crea un archivo de datos después de hablar con una pizarra digital:

CreateDataFile(IFileAccess boardFileAccess, IMeasurer boardMeasurer)

Aquí boardFileAccessy boardMeasurerson la misma instancia de un Boardobjeto que implementa ambos IFileAccessy IMeasurer. IMeasurerse utiliza en este caso para un método único que activará un pin en el tablero para hacer una medición simple Los datos de esta medición se almacenan localmente en el tablero usando IFileAccess. BoardSe encuentra en un proyecto separado.

Llegué a la conclusión de que CreateDataFileestá haciendo una cosa al hacer una medición rápida y luego almacenar los datos, y hacer ambas cosas con el mismo método es más intuitivo para alguien que usa este código y luego tiene que hacer una medición y escribir en un archivo como llamadas a métodos separados.

Para mí, parece incómodo pasar el mismo objeto a un método dos veces. He considerado hacer una interfaz local IDataFileCreatorque se extienda IFileAccessy IMeasurerluego tenga una implementación que contenga una Boardinstancia que solo llame a los Boardmétodos requeridos . Teniendo en cuenta que el mismo objeto de pizarra siempre se usaría para la medición y la escritura de archivos, ¿es una mala práctica pasar el mismo objeto a un método dos veces? Si es así, ¿está utilizando una interfaz local e implementación una solución adecuada?

Pavuxun
fuente
2
Es difícil o imposible distinguir la intención de su código a partir de los nombres que está utilizando. Una interfaz llamada IDataFileCreator que se pasa a un método llamado CreateDataFile es alucinante. ¿Están compitiendo por la responsabilidad de persistir los datos? ¿De qué clase es CreateDataFile un método de todos modos? La medición no tiene nada que ver con los datos persistentes, por lo que queda claro. Su pregunta no es sobre el mayor problema que tiene con su código.
Martin Maat
¿Es concebible que su objeto de acceso a archivos y su objeto de medición puedan ser dos objetos diferentes? Yo diría que sí. Si lo cambia ahora, tendrá que volver a cambiarlo en la versión 2 que admite tomar medidas en toda la red.
user253751
2
Sin embargo, aquí hay otra pregunta: ¿por qué el acceso al archivo de datos y los objetos de medición son los mismos en primer lugar?
user253751

Respuestas:

40

No, esto está perfectamente bien. Simplemente significa que la API está sobredimensionada con respecto a su aplicación actual .

Pero eso no prueba que nunca habrá un caso de uso en el que la fuente de datos y el medidor sean diferentes. El objetivo de una API es ofrecer al programador de aplicaciones posibilidades, no todas las cuales se utilizarán. No debe restringir artificialmente lo que pueden hacer los usuarios de API a menos que complique la API de modo que la comprensibilidad neta disminuya.

Kilian Foth
fuente
7

De acuerdo con la respuesta de @ KilianFoth de que esto está perfectamente bien.

Aún así, si lo desea, puede crear un método que tome un solo objeto que implemente ambas interfaces:

public object CreateDataFile<T_BoardInterface>(
             T_BoardInterface boardInterface
    )
    where T_BoardInterface : IFileAccess, IMeasurer
{
    return CreateDataFile(
                boardInterface
            ,   boardInterface
        );
}

No hay una razón general por la que los argumentos deban ser objetos diferentes, y si un método requiriera que los argumentos fueran diferentes, ese sería un requisito especial que su contrato debería aclarar.

Nat
fuente
4

Llegué a la conclusión de que CreateDataFileestá haciendo una cosa al hacer una medición rápida y luego almacenar los datos, y hacer ambas cosas con el mismo método es más intuitivo para alguien que usa este código y luego tiene que hacer una medición y escribir en un archivo como llamadas a métodos separados.

Creo que este es tu problema, en realidad. El método no está haciendo una cosa. Realiza dos operaciones distintas que involucran E / S a diferentes dispositivos , y ambas se descargan a otros objetos:

  • Obtener una medida
  • Guarde ese resultado en un archivo en algún lugar

Estas son dos operaciones de E / S diferentes. Notablemente, el primero no muta el sistema de archivos de ninguna manera.

De hecho, debemos tener en cuenta que hay un paso medio implícito:

  • Obtener una medida
  • Serializar la medida en un formato conocido
  • Guardar la medida serializada en un archivo

Su API debe proporcionar cada uno de estos por separado en alguna forma. ¿Cómo sabe que una persona que llama no querrá tomar una medida sin almacenarla en algún lugar? ¿Cómo sabe que no querrán obtener una medición de otra fuente? ¿Cómo sabes que no querrán almacenarlo en otro lugar que no sea el dispositivo? Hay buenas razones para desacoplar las operaciones. En un desnudo mínimo, cada pieza debe estar disponible a cualquier persona que llama. No debería ser obligado a escribir la medida en un archivo si mi caso de uso no lo requiere.

Como ejemplo, puede separar las operaciones de esta manera.

IMeasurer tiene una forma de obtener la medida:

public interface IMeasurer
{
    IMeasurement Measure(int someInput);
}

Su tipo de medición podría ser algo simple, como a stringo decimal. No insisto en que necesita una interfaz o clase para ello, pero hace que el ejemplo aquí sea más general.

IFileAccess tiene algún método para guardar archivos:

interface IFileAccess
{
    void SaveFile(string fileContents);
}

Entonces necesita una forma de serializar una medición. Construya eso en la clase o interfaz que representa una medida, o tenga un método de utilidad:

interface IMeasurement
{
    // As part of the type
    string Serialize();
}

// Utility method. Makes more sense if the measurement is not a custom type.
public static string SerializeMeasurement(IMeasurement m)
{
    return ...
}

No está claro si todavía tiene esta operación de serialización separada.

Este tipo de separación mejora su API. Le permite a la persona que llama decidir qué necesita y cuándo, en lugar de forzar sus ideas preconcebidas sobre lo que debe realizar la E / S. Las personas que llaman deben tener el control para realizar cualquier operación válida , si crees que es útil o no.

Una vez que tenga implementaciones separadas para cada operación, su CreateDataFilemétodo se convierte en una abreviatura para

fileAccess.SaveFile(SerializeMeasurement(measurer.Measure()));

En particular, su método agrega muy poco valor una vez que haya hecho todo esto. La línea de código anterior no es difícil de usar para las personas que llaman directamente, y su método es puramente para mayor comodidad. Debería ser y es algo opcional . Y esa es la forma correcta de comportamiento de la API.


Una vez que se tengan en cuenta todas las partes relevantes y hayamos reconocido que el método es solo una conveniencia, debemos reformular su pregunta:

¿Cuál sería el caso de uso más común para sus llamantes?

Si el objetivo es hacer que el caso de uso típico de medir y escribir en la misma pizarra sea un poco más conveniente, entonces tiene mucho sentido que esté disponible Boarddirectamente en la clase:

public class Board : IMeasurer, IFileAccess
{
    // Interface methods...

    /// <summary>
    /// Convenience method to measure and immediate record measurement in
    /// default location.
    /// </summary>
    public void ReadAndSaveMeasurement()
    {
        this.SaveFile(SerializeMeasurement(this.Measure()));
    }
}

Si esto no mejora la conveniencia, entonces no me molestaría en absoluto con el método.


Al tratarse de un método de conveniencia, surge otra pregunta.

¿Debería la IFileAccessinterfaz conocer el tipo de medición y cómo serializarlo? Si es así, puede agregar un método para IFileAccess:

interface IFileAccess
{
    void SaveFile(string fileContents);
    void SaveMeasurement(IMeasurement m);
}

Ahora las personas que llaman solo hacen esto:

fileAccess.SaveFile(measurer.Measure());

que es tan corto y probablemente más claro que su método de conveniencia como se concibe en la pregunta.

jpmc26
fuente
2

El cliente consumidor no debería tener que lidiar con un par de artículos cuando un solo artículo es suficiente. En su caso, casi no lo hacen, hasta la invocación de CreateDataFile.

La posible solución que sugiere es crear una interfaz derivada combinada. Sin embargo, este enfoque requiere un único objeto que implemente ambas interfaces, lo cual es bastante restrictivo, posiblemente una abstracción permeable en el sentido de que está básicamente personalizado para una implementación particular. Considere lo complicado que sería si alguien quisiera implementar las dos interfaces en objetos separados: tendrían que proxy todos los métodos en una de las interfaces para reenviar al otro objeto. (FWIW, otra opción es simplemente fusionar las interfaces en lugar de requerir que un objeto tenga que implementar dos interfaces a través de una interfaz derivada).

Sin embargo, otro enfoque que limita o dicta menos la implementación es que IFileAccessse combina con una IMeasurercomposición, de modo que uno de ellos está vinculado y hace referencia al otro. (Esto aumenta un poco la abstracción de uno de ellos, ya que ahora también representa el emparejamiento). Luego, CreateDataFilepodría tomar solo una de las referencias, por ejemplo IFileAccess, y aún obtener la otra según sea necesario. Su implementación actual como un objeto que implementa ambas interfaces sería simplemente return this;para la referencia de composición, aquí el captador para IMeasureradentro IFileAccess.

Si el emparejamiento resulta ser falso en algún momento del desarrollo, es decir, a veces se usa un medidor diferente con el mismo acceso a archivos, entonces puede hacer este mismo emparejamiento pero en un nivel superior, lo que significa que la interfaz adicional introducida no será una interfaz derivada, sino más bien una interfaz que tiene dos captadores, emparejando un acceso a archivos y un medidor juntos a través de la composición en lugar de la derivación. Entonces, el cliente consumidor tiene un artículo con el que preocuparse mientras se mantenga el emparejamiento, y objetos individuales con los que lidiar (para componer nuevos emparejamientos) cuando sea necesario.


En otra nota, podría preguntar quién es el propietario CreateDataFile, y la pregunta es quién es este tercero. Ya tenemos algún cliente consumidor que invoca CreateDataFile, el objeto / clase propietario CreateDataFiley el IFileAccessy IMeasurer. A veces, cuando tenemos una visión más amplia del contexto, pueden aparecer organizaciones alternativas, a veces mejores. Difícil de hacer aquí ya que el contexto es incompleto, por lo que es solo un pensamiento.

Erik Eidt
fuente
0

Algunos han mencionado que eso CreateDataFileestá haciendo demasiado. Podría sugerir que, en cambio, Boardestá haciendo demasiado, ya que acceder a un archivo parece ser una preocupación separada del resto del tablero.

Sin embargo, si suponemos que esto no es un error, el mayor problema es que la interfaz debe ser definida por el cliente, en este caso CreateDataFile.

El Principio de segregación de interfaz establece que el cliente no debería tener que depender de más de una interfaz de la que necesita. Tomando prestada la frase de esta otra respuesta , esto se puede parafrasear como "una interfaz está definida por lo que el cliente necesita".

Ahora, es posible componer esta interfaz específica del cliente usando IFileAccessy IMeasurercomo sugieren otras respuestas, pero en última instancia, este cliente debe tener una interfaz a medida para ella.

Xtros
fuente
@Downvoter - ¿Qué pasa con esto es incorrecto o se puede mejorar?
Xtros