Как фабрика узнает, какой тип объекта создавать?

Я считаю, что шаблон проектирования фабричных методов подходит для того, что я пытаюсь сделать, но я не уверен, какую ответственность (знания о подклассах, которые он создает) возложить на него. Пример использования шаблона фабричных методов в Википедии почти точно описывает ситуацию, в которой я нахожусь:

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.
        }
    }
}

Мой вопрос: как выглядит функция figureOutImageType? В этом конкретном примере я бы предположил, что он проверяет заголовок файла в InputStream, чтобы определить, в каком формате изображения находятся данные. Я хотел бы знать, знает ли сам ImageReaderFactory, как анализировать заголовки файлов и определять, является ли тип файла GIF , JPEG и т. д., или если он вызывает функцию внутри каждого класса Reader, которая позволяет ему узнать, какой это тип изображения. Что-то вроде этого, может быть:

int figureOutImageType(InputStream is)
{
    if(GifReader.isGIF(is))
        return ImageReaderFactory.GIF;
    else if(JpegReader.isJPEG(is))
        return ImageReaderFactory.JPEG;
    // etc.
}

Похоже, что наличие у фабрики знаний о том, как анализировать изображения, нарушает инкапсуляцию, и разрешение подклассам решать, какой из них должен быть создан, является частью шаблона проектирования фабричного метода. Тем не менее, также кажется, что функция figureOutImageType просто добавляет некоторый избыточный код, потому что почему бы каждому подклассу просто не выполнить проверку InputStream в функции getImageReader и пропустить случай переключения?

Раньше у меня не было опыта использования фабрик, и я надеялся получить некоторое представление от некоторых людей, которые использовали их в прошлом, о наилучшем способе решения этой проблемы. Нормально ли, чтобы фабрика знала о внутренней работе своих подклассов, или они должны нести ответственность за то, чтобы фабрика знала, что создавать, и как вы все это организуете?

Спасибо!


person Venesectrix    schedule 31.03.2009    source источник


Ответы (5)


Фабрика должна иметь некоторое представление о выборе фактического объекта для создания. Например, метод WebRequest.Create в .NET должен иметь возможность выбирать между клиентами с разными протоколами, проверяя протокольную часть файла Uri. Не нужно разбирать все подряд. Просто часть, необходимая для того, чтобы определить, какой класс будет за это отвечать (в вашем примере это, вероятно, будет просто заголовок файла).

Что касается вашего вопроса о нарушении инкапсуляции, не совсем... В большинстве случаев фабрика жестко запрограммирована и уже знает о различных типах классов и их функциях. Это уже зависит от функциональности, предлагаемой известным набором классов, поэтому вы не добавляете к нему многого. Вы также можете инкапсулировать часть обнаружения фабрики в другой вспомогательный класс, который может использоваться как фабрикой, так и подклассами (в духе принципа DRY).

person mmx    schedule 31.03.2009
comment
Спасибо за ваш вклад. Итак, вы можете порекомендовать мне использовать общий вспомогательный/служебный класс для определения типа файла из InputStream в функции getImageReader и просто включить возвращаемое перечисление? - person Venesectrix; 01.04.2009

Оба являются допустимыми вариантами в зависимости от контекста.

ЕСЛИ вы проектируете расширяемость - скажем, модель плагина для разных ImageReaders - тогда ваш класс Factory не может знать обо всех возможных ImageReaders. В этом случае вы идете по пути ImageReader.CanRead(ImageStream) — спрашивая каждого исполнителя, пока не найдете того, кто сможет его прочитать.

Имейте в виду, что иногда порядок здесь имеет значение. У вас может быть GenericImageReader, который может обрабатывать JPG, но Jpeg2000ImageReader лучше справляется с этим. Проходя по ImageReader, разработчики остановятся на том, что будет первым. Вы можете посмотреть на сортировку списка возможных ImageReaders, если это проблема.

В противном случае, если список ImageReaders конечен и находится под вашим контролем, вы можете использовать более традиционный подход Factory. В этом случае Фабрика решает, что создавать. Он уже связан ctor с конкретными реализациями ImageReader, поэтому добавление правил для каждого ImageReader не увеличивает связанность. Если логика выбора ImageReader в основном находится в самом ImageReader, то, чтобы избежать дублирования кода, вы все равно можете пойти по маршруту ImageReader.CanRead(ImageStream), но можно просто жестко запрограммировать, какие типы вы используете.

person Mark Brackett    schedule 01.04.2009
comment
Спасибо за ответ! Я на самом деле использую C++, и я считаю, что у меня есть только возможность ходить по типам ImageReader вручную. Я не знаю, как определить, какие подклассы существуют для базового класса, кроме как отслеживать их конкретно в коде (жестко закодированные или добавленные в массив и т. д.) - person Venesectrix; 01.04.2009

Для расширения вы можете внедрить некоторые из этих зависимостей, которые вы упомянули. Например, выяснить, что это за файл, или сопоставить тип файла с классом, который его обрабатывает. Внешний реестр (то есть файл свойств) будет хранить, скажем, GIF -> GifReader или лучше GIF -> GifMetadataClass. Тогда ваш код может быть общим и не иметь зависимостей от всех классов, плюс вы можете расширить его в будущем, или третьи стороны могут его расширить.

person John Ellinwood    schedule 31.03.2009
comment
Я думаю, вы говорите, что я мог бы использовать карту, чтобы связать тип файла с классом, который обрабатывает этот тип файла, верно? Как в этом случае фабрика определит, какой тип файла был основан на InputStream? Я не уверен, что вы могли бы настроить карту из InputStream в тип файла. - person Venesectrix; 01.04.2009
comment
Во входном потоке многие из этих классов изображений имеют в начале магические числа или, по крайней мере, что-то общее, через которое вы можете пройти, чтобы извлечь тип изображения. - person John Ellinwood; 01.04.2009
comment
Это правда. Я понимаю, что вы говорите о том, чтобы сделать его универсальным и расширяемым с использованием ассоциаций, что определенно следует учитывать. В моем конкретном случае это может быть того не стоит, потому что у меня создается только несколько разных объектов, и я не ожидаю, что будет добавлено намного больше. - person Venesectrix; 01.04.2009

Если это для окон, я бы попытался угадать тип контента, а затем использовать factory. На самом деле я сделал это некоторое время назад.

Вот класс для угадывания типа содержимого файла:

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";
            }
        }
    }
}
person Sergej Andrejev    schedule 31.03.2009
comment
Есть ли у них какие-либо функции поиска MIME в .Net или вызов API Windows является единственным решением, кроме хранения вашей собственной таблицы поиска. - person James; 01.04.2009
comment
Насколько мне известно, в .NET нет определения типа mime. - person Sergej Andrejev; 01.04.2009

Я бы использовал статический метод CanReadFrom (или что-то подобное) в общем интерфейсе ImageReader (не уверен, что это возможно — FIXME). Используйте отражение, чтобы захватить всех реализаторов и вызвать функцию. Если кто-то возвращает true, верните экземпляр класса.

person strager    schedule 31.03.2009