У меня есть метод FOO()
в классе A
который принимает в качестве аргументов входные данные от членов класса B
среди прочего (скажем, они два float и один 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()
?
Но что меня интересует, так это то,
связь между классом А и В.
Да, это так. A
а также B
сейчас тесно связаны.
Похоже, у вас сложилось впечатление, что общепринято, что нужно пропускать предметы, а не членов этих предметов. Я не уверен, как у вас сложилось такое впечатление, но это не так. Должны ли вы отправлять объект или членов этого объекта полностью зависит от того, что вы пытаетесь сделать.
В некоторых случаях необходимо и желательно иметь тесно связанную зависимость между двумя объектами, а в других случаях это не так. Если здесь применимо общее эмпирическое правило, я бы сказал, что в любом случае оно противоположно тому, что вы предложили:
Устраните зависимости везде, где это возможно, но больше нигде.
Нет действительно одного, который универсально прав, и другого, который универсально неправ. По сути, это вопрос, который лучше отражает ваши реальные намерения (что невозможно угадать с помощью метасинтаксических переменных).
Например, если я написал функцию «read_person_data», он, вероятно, должен взять какой-то объект «person» в качестве цели для данных, которые он собирается прочитать.
Если, с другой стороны, у меня есть функция «читать две строки и int», она, вероятно, должна принимать две строки и int, даже если (например) я использую ее для чтения имени и фамилии, и номер сотрудника (то есть, по крайней мере, часть данных человека).
Однако есть два основных момента: во-первых, функция, подобная первой, которая делает что-то осмысленное и логичное, обычно предпочтительнее, чем последняя, которая представляет собой нечто большее, чем произвольная совокупность действий, которые происходят вместе.
Во-вторых, если у вас такая ситуация, возникает вопрос: не должна ли рассматриваемая функция (хотя бы логически) быть частью вашей A
вместо того, чтобы быть чем-то отдельным, что действует на A
, Это ни в коем случае не наверняка, но вы можете просто смотреть на плохую сегментацию между этими классами.
Добро пожаловать в мир машиностроения, где все является компромиссом, и правильное решение зависит от того, как должны использоваться ваши классы и функции (и каково их значение).
Если foo()
является концептуально то, чей результат зависит от float
, int
и string
тогда правильно будет принять float
, int
и string
, Приходят ли эти значения от членов класса (может быть B
, но также может быть C
или же D
) это не важно, потому что семантически foo()
не определяется с точки зрения B
,
С другой стороны, если foo()
является концептуально то, чей результат зависит от состояния B
— например, потому что он реализует абстракцию над этим состоянием — затем заставляет его принимать объект типа B
,
Теперь также верно, что хорошая практика программирования состоит в том, чтобы позволить функциям принимать небольшое количество аргументов, если это возможно, я бы сказал, до трех без преувеличения, поэтому, если функция логически работает с несколькими значениями, вы можете сгруппировать эти значения в структуре данных и передать экземпляры этой структуры данных в foo()
,
Теперь, если вы называете эту структуру данных B
мы вернулись к исходной проблеме — но с миром семантических различий!
Следовательно, или нет foo()
должен принимать три значения или экземпляр B
в основном зависит от того, что foo()
а также B
конкретно означают в вашей программе, и как они будут использоваться — независимо от того, логически связаны их компетенции и обязанности или нет.
Учитывая этот вопрос, вы, вероятно, думаете о занятиях неправильно.
При использовании класса вас должен интересовать только его открытый интерфейс (обычно состоящий исключительно из методов), а не его члены-данные. В любом случае члены данных должны быть приватными для класса, поэтому у вас даже не будет к ним доступа.
Думайте о классах как о физических объектах, скажем, как о мяче. Вы можете посмотреть на шар и увидеть, что он красный, но вместо этого вы не можете просто установить цвет шара на синий. Вам нужно будет выполнить действие с мячом, чтобы сделать его синим, например, нарисовав его.
Вернемся к вашим классам: чтобы позволить А выполнять некоторые действия с В, А должен будет что-то знать о В (например, что шарик должен быть окрашен, чтобы изменить его цвет).
Если вы хотите, чтобы A работал с объектами, отличными от объектов из класса B, вы можете использовать наследование для извлечения интерфейса, необходимого для A, в класс I, а затем позволить классу B и некоторому другому классу C наследовать от I. A теперь может работать одинаково хорошо с обоими классами B и C.
Есть несколько причин, по которым вы хотите обойти класс вместо отдельных участников.
Если вас беспокоят зависимости, вы можете реализовать интерфейсы (например, в Java или в C ++ с использованием абстрактных классов). Таким образом, вы можете уменьшить зависимость от конкретного объекта, но при этом убедитесь, что он может обрабатывать необходимый API.
Я думаю, что это определенно зависит от того, чего именно вы хотите достичь. Нет ничего плохого в передаче нескольких членов из класса в функцию. Это действительно зависит от того, что означает «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
нужно знать все о Shape, и если мы начнем делать некоторую реорганизацию структуры данных (для хранения x
а также y
в одной переменной-члене point
), это должно измениться. Но, возможно, это всего лишь несколько небольших изменений.
С другой стороны, в DrawObject2
мы можем реорганизовать все, что нам нравится, с помощью класса shape — даже удалить все это вместе и иметь x, y, shape и color в отдельных векторах [не очень хорошая идея, но если мы думаем, что это где-то решает какую-то проблему, то мы можем сделать так].
Это в значительной степени сводится к тому, что имеет больше всего смысла. В моем примере, вероятно, имеет смысл передать Shape
Возражать DrawObject1
, Но это не обязательно так, и есть много случаев, когда вы бы этого не хотели.
Почему вы передаете объекты вместо примитивов?
Это тот тип вопросов, который я ожидал бы от программистов @ se; тем не менее ..
Мы передаем объекты вместо примитивов, потому что стремимся установить четкий контекст, выделить определенную точку зрения и выразить четкое намерение.
Выбор передачи примитивов вместо полноценных, богатых контекстных объектов снижает степень абстракции и расширяет область действия функции. Иногда это цели, но обычно их следует избегать. Низкая сплоченность — это ответственность, а не актив.
Вместо того, чтобы воздействовать на связанные вещи через полноценный, богатый объект с примитивами, мы теперь оперируем не более чем произвольными значениями, которые могут или не могут быть строго связаны друг с другом. Без определенного контекста мы не знаем, является ли комбинация примитивов допустимой в любое время, не говоря уже о текущем состоянии системы. Любая защита, которую обеспечил бы объект расширенного контекста, теряется (или дублируется в неправильном месте), когда мы выбираем примитивы в определении функции, когда мы могли бы выбрать обогащенный объект. Кроме того, любые события или сигналы, которые мы обычно генерируем, наблюдаем и воздействуем путем изменения любых значений в объекте расширенного контекста, сложнее вызывать и отслеживать в нужное время, когда они актуальны при работе с простыми примитивами.
Использование объектов поверх примитивов способствует сплоченности. Сплоченность — это цель. Вещи, которые принадлежат друг другу, остаются вместе. Уважать естественную зависимость функции от этой группировки, упорядочения и взаимодействия параметров в ясном контексте — это хорошо.
Использование объектов поверх примитивов не обязательно увеличивает вид связи, о котором мы больше всего беспокоимся. Тип связи, о котором мы должны больше всего беспокоиться, это тип связи, который возникает, когда внешние абоненты диктуют форму и последовательность обмена сообщениями, и мы далее распродаем их предписанные правила как единственный способ играть.
Вместо того, чтобы идти ва-банк, мы должны отметить четкое «сказать», когда мы это увидим. Поставщики промежуточного программного обеспечения и поставщики услуг определенно хотят, чтобы мы пошли олл-ин и плотно интегрировали их систему в нашу. Они хотят иметь твердую зависимость, тесно переплетенную с нашим собственным кодом, поэтому мы продолжаем возвращаться. Однако мы умнее этого. Если не считать умных, мы, по крайней мере, можем быть достаточно опытными, пройдя этот путь, чтобы понять, что происходит. Мы не повышаем ставку, позволяя элементам кода поставщиков вторгаться в каждый закоулок, зная, что мы не можем купить эту руку, поскольку они лежат на куче фишек, и, честно говоря, наша рука просто не так хороша. Вместо этого мы говорим, что это то, что я собираюсь сделать с вашим промежуточным программным обеспечением, и мы устанавливаем ограниченный адаптивный интерфейс, который позволяет продолжить игру, но не ставит ферму на этой единственной руке. Мы делаем это потому, что во время следующей раздачи мы можем столкнуться с другим поставщиком промежуточного программного обеспечения или поставщиком услуг.
Помимо специальной метафоры покера, идея убежать от связи, когда бы она ни представилась, будет стоить вам. Использование наиболее сложного и дорогостоящего соединения — это, пожалуй, разумная вещь, если вы намерены оставаться в игре надолго, и у вас есть склонность к тому, что вы будете играть с другими поставщиками или поставщиками или устройствами, которыми вы можете управлять слабо.
Я мог бы сказать гораздо больше о взаимосвязи объектов контекста и использования примитивов, например, в предоставлении содержательных, гибких тестов. Вместо этого я бы предложил конкретные чтения о стиле кодирования от таких авторов, как дядя Боб Мартин, но пока я их опущу.
Согласиться с первым респондентом здесь, и на ваш вопрос о «есть ли другой способ …»;
Поскольку вы передаете экземпляр класса B методу FOO1 из A, разумно предположить, что предоставляемая B функциональность не совсем уникальна, т. Е. Могут быть другие способы реализации того, что B предоставляет A (Если B — это POD без собственной логики, тогда даже не имеет смысла пытаться отделить их, так как A все равно нужно много знать о B).
Следовательно, вы можете отделить A от грязных секретов B, возвысив все, что B делает для A, до интерфейса, а затем A вместо этого включает «BsInterface.h». Это предполагает, что могут быть C, D … и другие варианты выполнения того, что B делает для A.
Если не тогда вы должны спросить себя, почему B — это класс вне A, в первую очередь …
В конце концов все сводится к одному; Это должно иметь смысл …
Я всегда переворачиваю проблему с ног на голову, когда сталкиваюсь с такими философскими дебатами (в основном с собой); как бы я использовал A-> FOO1? Имеет ли смысл в вызывающем коде иметь дело с B, и всегда ли я в конечном итоге включаю A и B вместе, так как A и B используются вместе?
то есть; если вы хотите быть разборчивыми и писать чистый код (+1 к этому), то сделайте шаг назад и примените правило «не усложняйте», но всегда позволяйте юзабилити отвергать любые соблазны, которые вам нужны, для чрезмерной разработки кода.
Ну, так или иначе, это мое мнение.