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


15

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

CreateDataFile(IFileAccess boardFileAccess, IMeasurer boardMeasurer)

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

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

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


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

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


2

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

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

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

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


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


0

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

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

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

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


@ Downvoter - Что по этому поводу неверно или может быть улучшено?
Xtros
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.