Почему мы передаем объектам, а не членам объекта, функции?

У меня есть метод FOO() в классе A, который в качестве аргументов принимает среди членов данных класса B среди прочего (скажем, это два поплавка и один int). Как я понимаю это, лучше всего реализовать это с помощью чего-то вроде:

A->FOO1(B, other_data_x)

а не

A->FOO2(B.member1, B.member2, B.member3, other_data_x).

Я собираю одно, но не единственное, преимущество этого в том, что он оставляет детали, из которых члены B имеют доступ к FOO1(), и поэтому помогает скрыть детали реализации.

Но меня интересует, действительно ли это вводит дополнительную связь между классами A и B. Класс A в первом случае должен знать, что класс B существует (через что-то вроде include class_B_header.h), и если члены B изменяются или перемещаются в другой класс или класс B, полностью исключаются, вы должны соответствующим образом изменить A и FOO1(). Напротив, в последнем подходе FOO2() не имеет значения, существует ли класс B, на самом деле все, о чем он заботится, состоит в том, что он снабжен двумя поплавками (которые в этом случае состоят из B.member1 и B.member2) и int (B.member3). В последнем примере также есть связь, но эта связь обрабатывается везде, где FOO2() получает вызов или какой-либо класс вызывает вызов FOO2(), а не в определении A или B.

Я предполагаю, что вторая часть этого вопроса: есть ли хороший способ разделить A и B дальше, когда мы хотим реализовать такое решение, как FOO1()?

Ответ 1

Но меня интересует, действительно ли это вводит дополнительные связь между классами A и B.

Да, да. A и B теперь плотно связаны.

У вас, похоже, создается впечатление, что принято считать, что нужно передавать объекты, а не члены этих объектов. Я не уверен, как у вас сложилось такое впечатление, но это не так. Если вы должны отправить объект или члены этого объекта, полностью зависит от того, что вы пытаетесь сделать.

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

Устранить зависимости везде, где это возможно, но нигде.

Ответ 2

На самом деле нет такого универсального права, а другого - универсального. Это в основном вопрос о том, что лучше отражает ваше реальное намерение (чего невозможно угадать с помощью метасинтатических переменных).

Например, если я написал функцию "read_person_data", вероятно, в качестве цели для данных, которые он должен читать, должен быть выбран какой-то "персональный" объект.

Если, с другой стороны, у меня есть функция "read two stringings and int", она должна, вероятно, взять две строки и int, даже если (например), я использую ее для чтения первого и фамилию и номер сотрудника (т.е., по крайней мере, часть данных человека).

Две основные точки: во-первых, функция, подобная первой, которая делает что-то значимое и логичное, обычно предпочтительнее одного, такого как последнее, что немного больше, чем любая совокупность действий, которые происходят вместе.

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

Ответ 3

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

Если foo() является концептуально чем-то, результат которого зависит от a float, a int и a string, тогда он прав для принятия a float, a int и a string. Являются ли эти значения членами класса (может быть B, но также может быть C или D), это не имеет значения, поскольку семантически foo() не определяется в терминах B.

С другой стороны, если foo() концептуально является чем-то, результат которого зависит от состояния B - например, потому что он реализует абстракцию над этим состоянием - тогда сделайте его принятым объектом типа B.

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

Теперь, если вы называете эту структуру данных B, мы вернемся к исходной проблеме - но с миром семантической разницы!

Следовательно, должен ли или не быть foo() принимать три значения или экземпляр B в основном зависит от того, что foo() и B конкретно означает в вашей программе и как они будут использоваться - независимо от того, компетенции и обязанности логически связаны или нет.

Ответ 4

Учитывая этот вопрос, вы, вероятно, думаете о неправильном пути.

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

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

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

Если вы хотите иметь работу A с объектами, отличными от объектов класса B, вы можете использовать наследование для извлечения интерфейса, необходимого A в класс I, а затем пусть класс B и некоторый другой класс C наследуются от I. A может теперь работают одинаково хорошо с обоими классами B и C.

Ответ 5

Существует несколько причин, по которым вы хотите передать класс вместо отдельных членов.

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

Если вы обеспокоены зависимостями aybout, вы можете реализовать интерфейсы (например, в Java или на С++ с использованием абстрактных классов). Таким образом, вы можете уменьшить зависимость от конкретного объекта, но убедиться, что он может обрабатывать требуемый API.

Ответ 6

Я думаю, что это определенно зависит от того, чего вы хотите достичь. Нет ничего плохого в передаче нескольких членов класса из функции. Это действительно зависит от того, что означает "FOO1" - делает ли FOO1 на B с помощью other_data_x более четко, что вы хотите сделать.

Если взять пример - вместо произвольных имен A, B, FOO и т.д. мы создаем "реальные" имена, чтобы понять смысл:

enum Shapetype
{
   Circle, Square
};

enum ShapeColour
{
   Red, Green, Blue
}

class Shape 
{ 
 public:
   Shape(ShapeType type, int x, int y, ShapeColour c) :
         x(x), y(y), type(type), colour(c) {}
   ShapeType type;
   int x, y;
   ShapeColour colour;
   ...
};


class Renderer
{
   ... 
   DrawObject1(const Shape &s, float magnification); 
   DrawObject2(ShapeType s, int x, int y, ShapeColour c, float magnification); 
};


int main()
{
   Renderer r(...);
   Shape c(Circle, 10, 10, Red);
   Shape s(Square, 20, 20, Green);

   r.DrawObject1(c, 1.0);
   r.DrawObject1(s, 2.0);
   // ---- or ---
   r.DrawObject2(c.type, c.x, c.y, c.colour, 1.0);
   r.DrawObject2(s.type, s.x, s.y, s.colour, 1.0);
};

[Да, это до сих пор довольно глупый пример, но имеет смысл обсуждать предмет, тогда объекты имеют настоящие имена]

DrawObject1 должно знать все о форме, и если мы начнем реорганизацию структуры данных (чтобы сохранить x и y в одной переменной-члене под названием point), она должна измениться. Но это, вероятно, всего несколько небольших изменений.

С другой стороны, в DrawObject2 мы можем реорганизовать все, что нам нравится, с классом формы - даже удалить все вместе и иметь x, y, форму и цвет в отдельных векторах [не особенно хорошая идея, но если мы думаем, что где-то решаем какую-то проблему, тогда мы можем это сделать].

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

Ответ 7

Почему вы передаете объекты вместо примитивов?

Это вопрос, который я бы ожидал от программистов @se; тем не менее..

Мы передаем объекты вместо примитивов, потому что мы стремимся установить четкий контекст, отличить определенную точку зрения и выразить четкое намерение.

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

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

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

Использование объектов над примитивами не обязательно увеличивает тип связи, о которой мы больше всего беспокоимся. Мы должны больше беспокоиться о том, какая связь возникает, когда внешние абоненты диктуют форму и последовательность обмена сообщениями, и мы далее продаем их предписанные правила как единственный способ играть.

Вместо того, чтобы все входить, мы должны заметить ясное "рассказать", когда увидим. Поставщики промежуточного ПО и поставщики услуг определенно хотят, чтобы мы все входили и плотно интегрировали свою систему в нашу. Они хотят крепкой зависимости, тесно переплетаются с нашим собственным кодом, поэтому мы продолжаем возвращаться. Однако мы умнее этого. Если не быть умным, мы, по крайней мере, можем быть достаточно опытными, идя по этой дороге, чтобы узнать, что произойдет. Мы не принимаем участие в торгах, позволяя элементам кода продавцов вторгаться в каждый уголок и трещину, зная, что мы не можем купить руку, так как они сидят на куче фишек, и, честно говоря, наша рука не так хороша. Вместо этого мы говорим, что это то, что я собираюсь делать с вашим промежуточным программным обеспечением, и мы устанавливаем ограниченный адаптивный интерфейс, который позволяет продолжить игру, но не ставит ферму на эту руку. Мы делаем это, потому что в следующей руке мы можем столкнуться с другим поставщиком промежуточного программного обеспечения или поставщиком услуг.

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

Я могу сказать гораздо больше о связи объектов контекста и использования примитивов, например, при предоставлении значимых гибких тестов. Вместо этого я бы предложил конкретные чтения о стиле кодирования у таких авторов, как дядя Боб Мартин, но пока я их не буду опускать.

Ответ 8

Соглашаясь с первым респондентом здесь, и на ваш вопрос о "есть ли другой способ...";

Поскольку вы передаете экземпляр класса B в метод FOO1 группы A, разумно предположить, что функциональность B не является для него полностью уникальной, то есть могут быть другие способы реализации любых B (если B - это POD без собственной логики, тогда даже не имеет смысла пытаться отделить их, поскольку A все равно должен знать много о B).

Следовательно, вы можете отделить A от B грязных секретов, подняв все, что B делает для A в интерфейс, а затем A включает в себя "BsInterface.h". Это предполагает, что могут быть C, D... и другие варианты выполнения того, что B делает для A. Если нет, тогда вы должны спросить себя, почему B - это класс вне А, в первую очередь...

В конце концов все сводится к одной вещи; Это должно иметь смысл...

Я всегда переворачиваю проблему на голове, когда сталкиваюсь с такими философскими дебатами (в основном, с самим собой); как я буду использовать A- > FOO1? Имеет ли смысл в вызывающем коде иметь дело с B и всегда ли я включаю A и B вместе в любом случае из-за того, что A и B используются вместе?

то есть; если вы хотите быть придирчивым и писать чистый код (+1 к этому), то сделайте шаг назад и примените правило "держать это просто", но всегда позволяйте юзабилити отменять любые соблазны, которые вам нужно для разработки вашего кода.

Ну, это мое мнение.