design patterns - ¿Cómo sabe una fábrica qué tipo de objeto crear?
design-patterns factory (5)
Creo que el patrón de diseño del método de fábrica es apropiado para lo que estoy tratando de hacer, pero no estoy seguro de cuánta responsabilidad (conocimiento de las subclases que crea) para darle. El ejemplo de usar el patrón de método de fábrica en Wikipedia describe la situación en la que estoy casi exactamente:
public class ImageReaderFactory
{
public static ImageReader getImageReader( InputStream is )
{
int imageType = figureOutImageType( is );
switch( imageType )
{
case ImageReaderFactory.GIF:
return new GifReader( is );
case ImageReaderFactory.JPEG:
return new JpegReader( is );
// etc.
}
}
}
Mi pregunta es, ¿qué aspecto tiene la función figureOutImageType
? En este ejemplo específico, asumo que comprueba un encabezado de archivo en InputStream
para determinar en qué formato de imagen están los datos. Me gustaría saber si el propio ImageReaderFactory
sabe cómo analizar los encabezados de archivo y determinar si el tipo de archivo es GIF , JPEG, etc., o si llama a una función dentro de cada clase de Reader
que le permite saber qué tipo de imagen es. Algo como esto, tal vez:
int figureOutImageType(InputStream is)
{
if(GifReader.isGIF(is))
return ImageReaderFactory.GIF;
else if(JpegReader.isJPEG(is))
return ImageReaderFactory.JPEG;
// etc.
}
Parece que el hecho de que la fábrica sepa cómo analizar las imágenes rompe la encapsulación, y dejar que las subclases decidan cuál debe crearse es parte del patrón de diseño del método de fábrica. Sin embargo, también parece que la función figureOutImageType
solo está agregando un código redundante, porque ¿por qué no hacer que cada subclase realice su comprobación en el InputStream
en la función getImageReader
y omita la caja del interruptor?
No he tenido experiencia en el uso de fábricas antes, y esperaba obtener información de algunas personas que las han usado en el pasado sobre la mejor manera de manejar este problema. ¿Es correcto que la fábrica conozca el funcionamiento interno de sus subclases, o debería ser responsable de dejar que la fábrica sepa qué crear y cómo organizarlo todo?
¡Gracias!
Ambas son opciones válidas dependiendo del contexto.
Si está diseñando para la extensibilidad, diga un modelo de complemento para diferentes ImageReaders, entonces su clase Factory no podrá conocer todos los posibles ImageReaders. En ese caso, vaya a la ruta ImageReader.CanRead(ImageStream)
, y pregunte a cada implementador hasta que encuentre uno que pueda leerlo.
Cuidado, que a veces el ordenamiento importa aquí. Puede tener un GenericImageReader que puede manejar JPG, pero un Jpeg2000ImageReader que es mejor en eso. Caminando, los implementadores de ImageReader se detendrán en lo que sea primero. Es posible que desee ver la clasificación de la lista de posibles ImageReaders si eso es un problema.
De lo contrario, si la lista de ImageReaders es finita y bajo su control, entonces puede usar el enfoque de fábrica más tradicional. En ese caso, la Fábrica decide qué crear. Ya está acoplado a las implementaciones concretas de ImageReader por el ctor, por lo que agregar reglas para cada ImageReader no aumenta el acoplamiento. Si la lógica para elegir un ImageReader se encuentra principalmente en el mismo ImageReader, entonces para evitar la duplicación de código, aún puede ir a la ruta ImageReader.CanRead(ImageStream)
, pero podría ser difícil determinar el tipo que camina.
La fábrica debe tener alguna idea sobre la elección del objeto real para crear. Por ejemplo, el método WebRequest.Create
en .NET, debería poder elegir entre los diferentes clientes de protocolo al verificar la parte del protocolo de Uri
. No necesita analizar todo el asunto. Solo la parte necesaria para distinguir qué clase será responsable de ella (en su ejemplo, probablemente será solo el encabezado del archivo).
Con respecto a su pregunta sobre romper la encapsulación, no realmente ... La mayoría de las veces, la fábrica está codificada y ya conoce los diferentes tipos de clases y sus características. Ya depende de la funcionalidad ofrecida por un conjunto conocido de clases, por lo que no le está agregando mucho. También puede encapsular la parte de detección de la fábrica en otra clase auxiliar que puede ser utilizada tanto por la fábrica como por las subclases (en el principio de DRY).
Para la extensibilidad, puede externalizar algunas de estas dependencias que menciona. Como averiguar qué tipo de archivo es o asignar el tipo de archivo a una clase que lo maneja. Un registro externo (es decir, un archivo de propiedades) almacenaría, por ejemplo, GIF -> GifReader, o mejor GIF -> GifMetadataClass. Entonces su código podría ser genérico y no tener dependencias en todas las clases, además de que podría extenderlo en el futuro, o terceros podrían extenderlo.
Si esto fuera para Windows, intentaría adivinar el tipo de contenido y luego utilizaría la fábrica. De hecho lo hice hace algún tiempo.
Aquí hay una clase para adivinar el tipo de contenido de un archivo:
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Nexum.Abor.Common
{
/// <summary>
/// This will work only on windows
/// </summary>
public class MimeTypeFinder
{
[DllImport(@"urlmon.dll", CharSet = CharSet.Auto)]
private extern static UInt32 FindMimeFromData(
UInt32 pBC,
[MarshalAs(UnmanagedType.LPStr)] String pwzUrl,
[MarshalAs(UnmanagedType.LPArray)] byte[] pBuffer,
UInt32 cbSize,
[MarshalAs(UnmanagedType.LPStr)]String pwzMimeProposed,
UInt32 dwMimeFlags,
out UInt32 ppwzMimeOut,
UInt32 dwReserverd
);
public string getMimeFromFile(string filename)
{
if (!File.Exists(filename))
throw new FileNotFoundException(filename + " not found");
var buffer = new byte[256];
using (var fs = new FileStream(filename, FileMode.Open))
{
if (fs.Length >= 256)
fs.Read(buffer, 0, 256);
else
fs.Read(buffer, 0, (int)fs.Length);
}
try
{
UInt32 mimetype;
FindMimeFromData(0, null, buffer, 256, null, 0, out mimetype, 0);
var mimeTypePtr = new IntPtr(mimetype);
var mime = Marshal.PtrToStringUni(mimeTypePtr);
Marshal.FreeCoTaskMem(mimeTypePtr);
return mime;
}
catch (Exception)
{
return "unknown/unknown";
}
}
}
}
Tendría un método CanReadFrom
estático (o algo así) en la interfaz común de ImageReader
(no estoy seguro si esto es posible - FIXME). Usa la reflexión para agarrar a todos los implementadores y llamar a la función. Si uno devuelve verdadero, devuelve una instancia de la clase.