Передать объект дважды в один и тот же метод или объединить с объединенным интерфейсом?

15

У меня есть метод, который создает файл данных после разговора с цифровой платой:

CreateDataFile(IFileAccess boardFileAccess, IMeasurer boardMeasurer)

Здесь boardFileAccessи boardMeasurerтот же экземпляр Boardобъекта, который реализует оба IFileAccessи IMeasurer. IMeasurerиспользуется в этом случае для одного метода, который установит один вывод на плате активным, чтобы сделать простое измерение. Данные этого измерения затем сохраняются локально на плате с помощью IFileAccess. Boardнаходится в отдельном проекте.

Я пришел к выводу, что CreateDataFileделать одну вещь - сделать быстрое измерение, а затем сохранить данные, и выполнение обоих в одном и том же методе является более интуитивно понятным для кого-то, кто использует этот код, чем для того, чтобы выполнить измерение и записать в файл. как отдельные вызовы методов.

Мне кажется неудобным передавать один и тот же объект методу дважды. Я рассмотрел создание локального интерфейса, IDataFileCreatorкоторый будет расширяться, IFileAccessа IMeasurerзатем иметь реализацию, содержащую Boardэкземпляр, который будет просто вызывать необходимые Boardметоды. Учитывая, что один и тот же объект платы всегда будет использоваться для измерения и записи файла, является ли плохой практикой передавать один и тот же объект методу дважды? Если да, то является ли использование локального интерфейса и реализации подходящим решением?

pavuxun
источник
2
Трудно или невозможно определить цель вашего кода по именам, которые вы используете. Интерфейс с именем IDataFileCreator, передаваемый методу с именем CreateDataFile, является ошеломляющим. Они конкурируют за ответственность за сохранение данных? Какой класс CreateDataFile метод в любом случае? Измерение не имеет ничего общего с постоянными данными, так что многое ясно. Ваш вопрос не о самой большой проблеме с вашим кодом.
Мартин Маат
Возможно ли когда-нибудь, чтобы ваш объект доступа к файлу и объект измерения были двумя разными объектами? Я бы сказал, да. Если вы измените его сейчас, вам придется изменить его обратно в версии 2, которая поддерживает проведение измерений по сети.
user253751
2
Но вот еще один вопрос - почему доступ к файлам данных и объекты измерений в первую очередь одинаковы?
user253751

Ответы:

40

Нет, это прекрасно. Это просто означает, что API перегружен в отношении вашего текущего приложения .

Но это не доказывает, что никогда не будет варианта использования, в котором источник данных и измеритель различны. Задача API - предложить программисту возможности, не все из которых будут использоваться. Вы не должны искусственно ограничивать то, что могут делать пользователи API, если это не усложняет API, так что понятность сети снижается.

Килиан Фот
источник
7

Согласитесь с ответом @ KilianFoth, что это прекрасно.

Тем не менее, если вы хотите, вы можете создать метод, который принимает один объект, который реализует оба интерфейса:

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

Нет общей причины, по которой аргументы должны быть разными объектами, и если метод требует, чтобы аргументы были разными, это было бы особым требованием, которое должен четко указывать контракт.

натуральный
источник
4

Я пришел к выводу, что CreateDataFileделать одну вещь - сделать быстрое измерение, а затем сохранить данные, и выполнение обоих в одном и том же методе является более интуитивно понятным для кого-то, кто использует этот код, чем для того, чтобы выполнить измерение и записать в файл. как отдельные вызовы методов.

Я думаю, что это ваша проблема, на самом деле. Метод не делает одну вещь. Он выполняет две отдельные операции, связанные с вводом-выводом на разные устройства , которые выгружаются на другие объекты:

  • Получить измерение
  • Сохранить этот результат в файл где-нибудь

Это две разные операции ввода / вывода. Примечательно, что первый никоим образом не изменяет файловую систему.

На самом деле, мы должны отметить, что есть промежуточный шаг:

  • Получить измерение
  • Сериализация измерения в известный формат
  • Сохраните сериализованное измерение в файл

Ваш API должен предоставлять каждый из них отдельно в некоторой форме. Откуда вы знаете, что вызывающий абонент не захочет проводить измерения, не сохраняя их где-либо? Откуда вы знаете, что они не захотят получить измерение из другого источника? Откуда вы знаете, что они не захотят хранить его где-то кроме устройства? Есть веская причина отделить операции. На голой минимум, каждая отдельная часть должна быть доступна любому абоненту. Я не должен быть принужден записывать измерение в файл, если мой вариант использования не требует его.

Например, вы можете разделить операции следующим образом.

IMeasurer есть способ получить измерение:

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

Ваш тип измерения может быть просто чем-то простым, например, stringили decimal. Я не настаиваю на том, что вам нужен интерфейс или класс для него, но это делает пример здесь более общим.

IFileAccess есть метод для сохранения файлов:

interface IFileAccess
{
    void SaveFile(string fileContents);
}

Тогда вам нужен способ сериализации измерения. Встраивайте это в класс или интерфейс, представляющий измерение, или используйте служебный метод:

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

Пока не ясно, выделена ли у вас эта операция сериализации.

Такое разделение улучшает ваш API. Это позволяет вызывающему абоненту решать, что ему нужно и когда, а не заставлять ваши предвзятые представления о том, что выполнять ввод / вывод. Вызывающие должны иметь контроль для выполнения любой действительной операции, независимо от того, считаете ли вы ее полезной или нет.

Если у вас есть отдельные реализации для каждой операции, ваш CreateDataFileметод становится просто сокращением для

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

Примечательно, что ваш метод добавляет очень мало пользы после того, как вы все это сделали. Вышеуказанная строка кода не является сложной для непосредственного использования вашими абонентами, и ваш метод предназначен исключительно для удобства. Это должно быть и является чем-то необязательным . И это правильный способ поведения API.


После того, как все соответствующие части будут учтены, и мы признаем, что метод просто удобен, нам нужно перефразировать ваш вопрос:

Что будет наиболее распространенным вариантом использования для ваших абонентов?

Если весь смысл состоит в том, чтобы сделать типичный вариант использования измерения и записи на одну и ту же доску немного более удобным, то имеет смысл просто сделать его доступным Boardнепосредственно в классе:

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()));
    }
}

Если это не улучшит удобство, то я не стал бы беспокоиться о методе вообще.


Этот удобный метод поднимает еще один вопрос.

Должен ли IFileAccessинтерфейс знать о типе измерения и как его сериализовать? Если это так, вы можете добавить метод к IFileAccess:

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

Теперь звонящие просто делают это:

fileAccess.SaveFile(measurer.Measure());

который так же короток и, вероятно, более понятен, чем ваш удобный метод, как задумано в этом вопросе.

jpmc26
источник
2

Клиент-потребитель не должен иметь дело с парой предметов, когда достаточно одного предмета. В вашем случае они почти нет, до вызова CreateDataFile.

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

Тем не менее, другой подход, который менее ограничивает / диктует реализацию, заключается в том, что IFileAccessон связан с IMeasurerсоставом in, так что один из них связан с другим и ссылается на него. (Это несколько увеличивает абстракцию одного из них, поскольку теперь оно также представляет сопряжение.) Тогда CreateDataFileможно взять, скажем IFileAccess, только одну из ссылок , и при этом получить другую при необходимости. Ваша текущая реализация как объект , который реализует оба интерфейса будет просто return this;для справки композиции, здесь геттер для IMeasurerв IFileAccess.

Если в какой-то момент разработки спаривание оказывается ложным, то есть иногда используется другой измеритель с тем же доступом к файлу, вы можете выполнить такое же спаривание, но на более высоком уровне, то есть введенный дополнительный интерфейс будет не быть производным интерфейсом, а скорее интерфейсом с двумя получателями, соединяющими доступ к файлу и измеритель вместе с помощью композиции, а не деривации. В этом случае клиент-потребитель имеет один элемент, с которым ему нужно иметь дело до тех пор, пока сохраняется сопряжение, и отдельные объекты, с которыми нужно иметь дело (для создания новых пар), когда это необходимо.


С другой стороны, я мог бы спросить, кому принадлежит CreateDataFile, и вопрос идет к тому, кто эта третья сторона. У нас уже есть некоторый клиент-потребитель, который вызывает CreateDataFileобъект-класс CreateDataFile, владеющий им , и IFileAccessи IMeasurer. Иногда, когда мы более широко рассматриваем контекст, могут появиться альтернативные, иногда лучшие организации. Трудно сделать здесь, так как контекст неполный, так что просто пища для размышлений.

Эрик Эйдт
источник
0

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

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

Принцип разделения интерфейса гласит, что клиент не должен зависеть от большего количества интерфейса, чем ему нужно. Заимствуя фразу из этого другого ответа , это можно перефразировать как «интерфейс определяется тем, что нужно клиенту».

Теперь можно составить этот специфичный для клиента интерфейс с использованием IFileAccessи, IMeasurerкак предполагают другие ответы, но, в конечном счете, этот клиент должен иметь специально разработанный для него интерфейс.

Xtros
источник
@ Downvoter - Что по этому поводу неверно или может быть улучшено?
Xtros