Есть ли причина использовать классы «старых данных»?

43

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

class Bottle {
   int height;
   int diameter;
   Cap capType;

   getters/setters, maybe a constructor
}

Мое понимание ОО состоит в том, что классы являются структурами для данных и методами работы с этими данными. Это, кажется, исключает объекты этого типа. Для меня они не более чем structsи своего рода поражение цели ОО. Я не думаю, что это обязательно зло, хотя это может быть запахом кода.

Есть ли такой случай, когда такие объекты были бы необходимы? Если это часто используется, делает ли это подозрительным дизайн?

Майкл К
источник
1
Это не совсем отвечает на ваш вопрос, но, тем не менее, кажется уместным: stackoverflow.com/questions/36701/struct-like-objects-in-java
Адам Лир
2
Я думаю, что вы ищете термин POD (Plain Old Data).
Гаурав
9
Это типичный пример структурированного программирования. Не обязательно плохо, просто не объектно-ориентированный.
Конрад Рудольф
1
не должно ли это быть переполнением стека?
Муад Диб
7
@ Муад'Диб: технически это является о программистах. Вашему компилятору все равно, если вы используете простые старые структуры данных. Вероятно, вашему процессору это нравится (в смысле «я люблю запах данных, только что из кэша»). Это люди, которые зацикливаются на том, "делает ли это мою методологию менее чистой?" вопросов.
Shog9

Ответы:

67

Определенно, не зло и не кодовый запах в моей голове. Контейнеры данных являются действительными гражданами ОО. Иногда вы хотите инкапсулировать связанную информацию вместе. Намного лучше иметь такой метод, как

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

чем

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

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

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

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

Адам Лир
источник
25
Неа. DoStuffWithдолжен быть метод из к Bottleклассу в ООП (и , скорее всего , должны быть неизменны, тоже). То, что вы написали выше, не является хорошим шаблоном в языке OO (если вы не взаимодействуете с устаревшим API). Это является , однако, действительный дизайн в среде без ОО.
Конрад Рудольф
11
@Javier: Тогда Java тоже не лучший ответ. Акцент Java на OO огромен, язык в принципе ничего хорошего не дает.
Конрад Рудольф
11
@JohnL: я, конечно, упрощал. Но в целом объекты в ОО инкапсулируют состояние , а не данные. Это хорошее, но важное различие. Смысл ООП состоит в том, чтобы точно не хранить много данных. Это отправка сообщений между государствами, которые создают новое состояние. Я не вижу, как вы можете отправлять сообщения на объект или без него. (И это оригинальное определение ООП, поэтому я не считаю его ошибочным.)
Конрад Рудольф,
13
@Konrad Rudolph: Вот почему я явно сделал комментарий внутри метода. Я согласен, что методы, которые влияют на экземпляры Bottle, должны идти в этом классе. Но если другой объект должен изменить свое состояние на основе информации в Bottle, то я думаю, что мой дизайн будет вполне корректным.
Адам Лир
10
@ Конрад, я не согласен с тем, что doStuffWithBottle должен идти в классе бутылок. Почему бутылка должна знать, как делать вещи с собой? doStuffWithBottle указывает, что что-то еще будет делать с бутылкой. Если бы в бутылке было это, это было бы крепким сцеплением. Однако, если бы у класса Bottle был метод isFull (), это было бы совершенно уместно.
Неми
25

Классы данных действительны в некоторых случаях. DTO - один хороший пример, упомянутый Анной Лир. В общем, вы должны рассматривать их как семя класса, методы которого еще не выросли. И если вы сталкиваетесь со многими из них в старом коде, относитесь к ним как к сильному запаху кода. Они часто используются старыми программистами на C / C ++, которые никогда не переходили на ОО-программирование и являются признаком процедурного программирования. Постоянное использование геттеров и сеттеров (или, что еще хуже, прямого доступа к частным лицам) может привести к проблемам до того, как вы об этом узнаете. Рассмотрим пример внешнего метода, для которого требуется информация Bottle.

Вот Bottleкласс данных):

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Здесь мы дали Bottleнекоторое поведение):

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Первый метод нарушает принцип «скажи-не-спрашивай», и, оставаясь Bottleтупым, мы позволяем неявным знаниям о бутылках, таких как то, что заставляет одного хрупкого ( Cap) делиться , в логику, которая находится вне Bottleкласса. Вы должны быть в тонусе, чтобы предотвратить такого рода «утечки», когда вы обычно полагаетесь на добытчиков.

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

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

Майк Э
источник
1
Не могу поверить, что у этого ответа нет голосов (ну, у вас есть один сейчас). Это может быть простой пример, но когда ОО достаточно злоупотреблен, вы получаете классы обслуживания, которые превращаются в кошмары, содержащие массу функций, которые должны были быть инкапсулированы в классы.
Alb
"старыми программистами C / C ++, которые никогда не делали переход (sic) к OO"? Программисты на C ++ обычно довольно симпатичны, так как это язык OO, т.
Е.
7

Если это то, что вам нужно, это нормально, но, пожалуйста, пожалуйста, сделайте это как

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

вместо чего-то вроде

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

Пожалуйста.

compman
источник
14
Единственная проблема с этим заключается в том, что нет проверки. Любая логика относительно того, что такое действительная Бутылка, должна быть в классе Бутылок. Однако, используя предложенную вами реализацию, у меня может быть бутылка с отрицательной высотой и диаметром - нет способа обеспечить выполнение каких-либо бизнес-правил для объекта без проверки каждый раз, когда объект используется. Используя второй метод, я могу убедиться, что если у меня есть объект Bottle, он был, есть и всегда будет действительным объектом Bottle в соответствии с моим контрактом.
Томас Оуэнс
6
Это одна из областей, где .NET имеет небольшое преимущество над свойствами, поскольку вы можете добавить средство доступа к свойствам с логикой проверки, с тем же синтаксисом, как если бы вы обращались к полю. Вы также можете определить свойство, где классы могут получить значение свойства, но не установить его.
JohnL
3
@ user9521 Если вы уверены, что ваш код не вызовет фатальной ошибки с «плохими» значениями, то перейдите к своему методу. Однако, если вам нужна дополнительная проверка или возможность использовать отложенную загрузку или другие проверки при чтении или записи данных, используйте явные методы получения и установки. Лично я склонен держать свои переменные в секрете и использовать методы получения и установки для согласованности. Таким образом, все мои переменные обрабатываются одинаково, независимо от проверки и / или других «продвинутых» методов.
Джонатан
2
Преимущество использования конструктора заключается в том, что он делает класс неизменным. Это важно, если вы хотите написать многопоточный код.
Fortyrunner
7
Я бы сделал поля окончательными, когда это возможно. ИМХО, я бы предпочел, чтобы поля были окончательными по умолчанию и имели ключевое слово для изменяемых полей. например, вар
Питер Лори
6

Как сказала Анна, определенно не зло. Конечно, вы можете помещать операции (методы) в классы, но только если хотите . Вам не нужно .

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

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

  2. Решите проблему 1, добавив методы уведомлений , чтобы при изменении части A она пыталась распространить необходимые изменения на части B и C. Это основная причина, по которой рекомендуется использовать методы доступа get-and-set. Поскольку это рекомендуемая практика, она, по-видимому, оправдывает проблему 1, вызывая еще больше проблемы 1 и больше решения 2. Это приводит не только к ошибкам из-за неполной реализации уведомлений, но и к проблеме снижения производительности уведомлений о сбегах. Это не бесконечные вычисления, а просто очень длинные.

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

Вот что я пытаюсь сделать:

  1. Сохраняйте данные как можно более нормализованными, чтобы при внесении изменений в данные было сделано как можно меньше кодовых точек, чтобы минимизировать вероятность входа в несогласованное состояние.

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

Майк Данлавей
источник
3

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

  • довольно легко создать несколько тестов и убедиться, что данные согласованы.
  • он содержит все виды поведения, которые включают эту информацию, поэтому время отслеживания ошибок данных сокращается
  • При их использовании метод args должен быть легким.
  • При использовании ORM эти классы обеспечивают гибкость и согласованность. Добавление сложного атрибута, который рассчитывается на основе простой информации, уже имеющейся в классе, приводит к написанию одного простого метода. Это гораздо более гибко и продуктивно, чем необходимость проверять вашу базу данных и проверять, исправлены ли все базы данных с новой модификацией.

Подводя итог, по моему опыту, они обычно более полезны, чем раздражают.

guiman
источник
3

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

AttackingHobo
источник
3

Согласитесь с Анной Лир,

Определенно, не зло и не кодовый запах в моей голове. Контейнеры данных являются действительными гражданами ОО. Иногда вы хотите инкапсулировать связанную информацию вместе. Намного лучше иметь такой метод, как ...

Иногда люди забывают прочитать Соглашения о кодировании Java 1999 года, в которых очень ясно сказано, что этот вид программирования идеально подходит. На самом деле, если вы избегаете этого, ваш код будет пахнуть! (слишком много получателей / установщиков)

Из Java Code Conventions 1999: один пример соответствующих общедоступных переменных экземпляра - это случай, когда класс по сути является структурой данных без какого-либо поведения. Другими словами, если бы вы использовали структуру вместо класса (если Java поддерживает структуру), то целесообразно сделать переменные экземпляра класса общедоступными. http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177

При правильном использовании POD (простые старые структуры данных) лучше, чем POJO, точно так же, как POJO часто лучше, чем EJB.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures

Ричард Фабер
источник
3

Структуры имеют свое место, даже в Java. Вы должны использовать их, только если выполняются следующие две вещи:

  • Вам просто нужно объединить данные, которые не имеют никакого поведения, например, передать в качестве параметра
  • Неважно, какие значения имеют эти агрегированные данные

Если это так, то вы должны сделать поля общедоступными и пропустить методы получения / установки. Геттеры и сеттеры в любом случае неуклюжи, а Java глупа из-за отсутствия свойств, подобных полезному языку. Так как ваш структурный объект не должен иметь никаких методов, открытые поля имеют больше смысла.

Однако, если один из них не подходит, вы имеете дело с реальным классом. Это означает, что все поля должны быть приватными. (Если вам абсолютно необходимо поле в более доступной области, используйте метод получения / установки.)

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

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

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

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

Вместо этого должно быть

double volume = bottle.calculateVolumeAsCylinder();

Если бы вы изменили высоту и диаметр, это была бы та же самая бутылка? Возможно нет. Это должно быть окончательным. В порядке ли отрицательное значение для диаметра? Должна ли ваша бутылка быть выше, чем она широкая? Может ли Cap быть нулевым? Нет? Как вы это проверяете? Предположим, клиент либо глуп, либо злой. ( Невозможно определить разницу. ) Вам необходимо проверить эти значения.

Вот как может выглядеть ваш новый класс Bottle:

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}
Ева
источник
0

ИМХО, часто не хватает таких классов в сильно объектно-ориентированных системах. Мне нужно тщательно квалифицировать это.

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

Но во многих случаях такие поля данных будут иметь очень узкую область действия. Очень простой пример - закрытый Nodeкласс структуры данных. Часто он может значительно упростить код, уменьшая количество происходящих взаимодействий между объектами, если они Nodeмогут просто состоять из необработанных данных. Это служит расцепления механизма , так как альтернативного варианта может потребоваться двунаправленную связь с, скажем, Tree->Nodeи Node->Treeв отличие от просто Tree->Node Data.

Более сложный пример - системы с компонентами сущностей, которые часто используются в игровых движках. В этих случаях компоненты часто являются просто необработанными данными и классами, подобными тем, которые вы показали. Однако их область действия / видимость имеет тенденцию быть ограниченной, поскольку обычно есть только одна или две системы, которые могут получить доступ к этому конкретному типу компонента. В результате вы все еще склонны считать, что поддерживать инварианты в этих системах довольно легко, и, кроме того, такие системы имеют очень мало object->objectвзаимодействий, что позволяет очень легко понять, что происходит на уровне птичьего полета.

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

введите описание изображения здесь

... в отличие от этого:

введите описание изображения здесь

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


источник
-1

В мире ООП существует даже много странных существ. Однажды я создал класс, который является абстрактным и ничего не содержит, он просто является общим родителем двух других классов ... это класс Port:

SceneMember 
Message extends SceneMember
Port extends SceneMember
InputPort extends Port
OutputPort extends Port

Таким образом, SceneMember является базовым классом, он выполняет всю работу, которая требуется для появления на сцене: добавление, удаление, получение-установка идентификатора и т. Д. Сообщение - это соединение между портами, оно имеет свою собственную жизнь. InputPort и OutputPort содержат собственные функции ввода / вывода.

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

Может быть, это антипаттерн , но мне это нравится.

(Это похоже на слово «мат», которое используется как для «жены», так и для «мужа». Вы никогда не используете слово «мат» для конкретного человека, потому что вы знаете его пол, и это не имеет значения если он / она женат или нет, то вместо этого вы используете «кого-то», но оно все еще существует и используется в редких, абстрактных ситуациях, например, в тексте закона.)

ern0
источник
1
Почему ваши порты должны расширять SceneMember? почему бы не создать классы портов для работы на SceneMembers?
Майкл К
1
Так почему бы не использовать стандартный шаблон интерфейса маркера? В основном идентичен вашему пустому абстрактному базовому классу, но это гораздо более распространенная идиома.
TMN
1
@Michael: только по теоретическим причинам я зарезервировал порт для будущего использования, но это будущее еще не наступило, и, возможно, никогда не будет. Я не понял, что у них нет общего, в отличие от их имен. Я заплачу компенсацию за каждого, кто понес какие-либо потери, понесенные этим пустым классом ...
ern0
1
@TMN: у типов SceneMember (производных) есть метод getMemberType (), есть несколько случаев, когда Scene сканируется на предмет подмножества объектов SceneMember.
ern0
4
Это не отвечает на вопрос.
Ева