Является ли разделение большинства классов на поля данных только классами и только классы методов (если это возможно) хорошим или антишаблонным?


10

Например, класс обычно имеет члены класса и методы, например:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public void printInfo(){
        System.out.println("Name:"+this.name+",weight:"+this.weight);
    }

    public void draw(){
        //some draw code which uses this.image
    }
}

Но после прочтения принципа единой ответственности и открытого закрытого принципа, я предпочитаю разделить класс на DTO и вспомогательный класс только статическими методами, например:

public class CatData{
    public String name;
    public int weight;
    public Image image;
}

public class CatMethods{
    public static void printInfo(Cat cat){
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat){
        //some draw code which uses cat.image
    }
}

Я думаю, что это соответствует принципу Единой ответственности, потому что теперь ответственность CatData состоит в том, чтобы хранить только данные, не заботясь о методах (также для CatMethods). И это также соответствует принципу открытого закрытого типа, потому что добавление новых методов не должно изменять класс CatData.

Мой вопрос, это хорошо или анти-шаблон?


15
Делать что-либо вслепую повсюду, потому что вы узнали новую концепцию, - это всегда антишаблон
Каяман

4
Какой смысл в классах, если всегда лучше иметь данные отдельно от методов, которые их модифицируют? Это звучит как не объектно-ориентированное программирование (что, безусловно, имеет свое место). Поскольку это помечено как «объектно-ориентированное», я предполагаю, что вы хотите использовать инструменты, которые предоставляет ООП?
user1118321

4
Другой вопрос, доказывающий, что SRP настолько плохо понят, что его следует запретить. Хранение данных в нескольких открытых полях не является обязанностью .
user949300

1
@ user949300, если класс отвечает за эти поля, то перемещение их в другое место в приложении, конечно, не окажет никакого влияния. Или, иначе говоря, вы ошибаетесь: они на 100% являются ответственностью.
Дэвид Арно

2
@DavidArno и R Schmitz: Содержащие поля не считаются ответственностью для целей SRP. Если это произойдет, ООП не может быть, поскольку все будет DTO, и тогда отдельные классы будут содержать процедуры для работы с данными. Единственная ответственность принадлежит одному заинтересованному лицу . (Хотя это, кажется, немного меняется на каждой итерации принципала)
user949300

Ответы:


10

Вы показали две крайности («все частные и все (возможно, не связанные) методы в одном объекте» против «все публичное и никаких методов внутри объекта»). ИМХО хорошее ОО моделирование не из них , сладкое место где-то посередине.

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

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

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public String getInfo(){
        return "Name:"+this.name+",weight:"+this.weight;
    }
    public Image getImage(){
        return image;
    }
}

Теперь Catобъект предоставляет достаточную логику, чтобы позволить окружающему коду легко реализоваться printInfoи drawбез публичного раскрытия всех атрибутов. Подходящее место для этих двух методов, скорее всего , не является класс бога CatMethods(так как printInfoи drawэто , скорее всего , разные проблемы, поэтому я думаю , что это очень маловероятно , что они принадлежат к одному классу).

Я могу себе представить, CatDrawingControllerкоторый реализует draw(и, возможно, использует внедрение зависимости для получения объекта Canvas). Я также могу представить другой класс, который реализует некоторый вывод и использование консоли getInfo(поэтому printInfoможет устареть в этом контексте). Но для принятия разумных решений необходимо знать контекст и то, как Catкласс будет фактически использоваться.

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

Также обратите внимание, что в приведенном выше примере все еще остается место для принятия решения о (im) изменчивости. Если Catкласс не будет выставлять какие-либо сеттеры и сам Imageявляется неизменяемым, этот дизайн позволит сделать Catнеизменным (чего не будет подход DTO). Но если вы считаете, что неизменность не требуется или не помогает в вашем случае, вы также можете пойти в этом направлении.


Триггер счастливых downvoters, это становится скучно. Если у вас есть критики, пожалуйста, дайте мне знать. Я буду рад прояснить любое недоразумение, если оно у вас есть.
Док Браун

Хотя я в целом согласен с вашим ответом ... Я чувствую, что getInfo()может вводить в заблуждение человека, задающего этот вопрос. Это тонко смешивает обязанности по представлению, как это printInfo()делает, побеждая цель DrawingControllerкласса
Адриано Репетти

@AdrianoRepetti Я не вижу, что это побеждает цель DrawingController. Я согласен с этим getInfo()и printInfo()ужасные имена. Рассмотрим toString()иprintTo(Stream stream)
candied_orange

@candid это не (просто) о названии, а о том, что оно делает. Этот код только о деталях представления, и мы фактически просто абстрагировали вывод, а не обработанный контент и его логику.
Адриано Репетти

@AdrianoRepetti: честно, это всего лишь надуманный пример, чтобы соответствовать первоначальному вопросу и продемонстрировать мою точку зрения. В реальной системе будет окружающий контекст, который, конечно, позволит принимать решения о лучших методах и именах.
Док Браун

6

Поздний ответ, но я не могу удержаться.

Является ли X большинством классов в Y хорошим или антишаблоном?

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

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

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

Теперь, делая это, я создал несколько статических методов для измельчения. Мой стажер создал класс DTO, очень похожий на ваш CatData.

Когда я впервые посмотрел на DTO, это меня подкосило. Годы ущерба, нанесенного Java моему мозгу, заставили меня отшатнуться в общественных местах. Но мы работали в C #. C # не нуждается в преждевременных методах получения и установки, чтобы сохранить ваше право делать данные неизменяемыми или инкапсулированными позже. Без изменения интерфейса вы можете добавлять их в любое время. Может быть, просто так вы можете установить точку останова. Все, не говоря своим клиентам об этом. Да, C #. Бу Ява.

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

Тогда нам это было нужно в других местах. Мы обнаружили, что хотим скопировать и вставить код. 14 строк инициализации швыряются вокруг. Становилось больно. Он колебался и спросил меня об идеях.

Я неохотно спросил: "Вы считаете объект?"

Он оглянулся на своего DTO и смутил лицо в замешательстве. «Это объект».

«Я имею в виду реальный объект»

«А?»

«Позвольте мне показать вам кое-что. Вы решаете, полезно ли это»

Я выбрал новое имя и быстро набрал что-то похожее на это:

public class Cat{
    CatData(string catPage) {
        this.catPage = catPage
    }
    private readonly string catPage;

    public string name() { return chop("name prefix", "name suffix"); }
    public string weight() { return chop("weight prefix", "weight suffix"); }
    public string image() { return chop("image prefix", "image suffix"); }

    private string chop(string prefix, string suffix) {
        int start = catPage.indexOf(prefix) + prefix.Length;
        int end = catPage.indexOf(suffix);
        int length = end - start;
        return catPage.Substring(start, length);
    }
}

Это ничего не делало, статические методы еще не делали. Но теперь я поместил 14 статических методов в класс, где они могли бы быть наедине с данными, с которыми они работали.

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

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

Я говорю, что вы всегда должны придерживаться ОО-объектов, а не ОТО? Нет. DTO сияет, когда вам нужно пересечь границу, которая удерживает вас от движущихся методов. У DTO есть свое место.

Но так же и объекты OO. Узнайте, как использовать оба инструмента. Узнайте, что стоит каждый. Научитесь решать проблему, ситуацию и стажера. Догма не твой друг здесь.


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

Например, класс обычно имеет члены класса и методы, например:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public void printInfo(){
        System.out.println("Name:"+this.name+",weight:"+this.weight);
    }

    public void draw(){
        //some draw code which uses this.image
    }
}

Где твой конструктор? Это не показывает мне достаточно, чтобы знать, полезно ли это.

Но после прочтения принципа единой ответственности и открытого закрытого принципа, я предпочитаю разделить класс на DTO и вспомогательный класс только статическими методами, например:

public class CatData{
    public String name;
    public int weight;
    public Image image;
}

public class CatMethods{
    public static void printInfo(Cat cat){
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat){
        //some draw code which uses cat.image
    }
}

Я думаю, что это соответствует принципу Единой ответственности, потому что теперь ответственность CatData состоит в том, чтобы хранить только данные, не заботясь о методах (также для CatMethods).

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

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

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

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

Мой вопрос, это хорошо или анти-шаблон?

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

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

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


2

Вы наткнулись на тему дебатов, которая уже более десяти лет вызывает споры среди разработчиков. В 2003 году Мартин Фаулер придумал фразу «Модель анемичной области» (ADM), чтобы описать это разделение данных и функциональности. Он - и другие, которые с ним согласны - утверждают, что «Богатые доменные модели» (смешение данных и функциональности) - это «правильная ОО», и что подход ADM - это не шаблон OO.

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

Независимо от того, на какой стороне забора вы сидите (я сижу очень твердо на стороне «Кстати, Мартин Фаулер говорит о куче старых вещей»), ваше использование статических методов для printInfoи drawпочти повсеместно осуждается. Статические методы трудно подделать при написании модульных тестов. Поэтому, если они имеют побочные эффекты (такие как печать или рисование на каком-либо экране или другом устройстве), они не должны быть статичными или должны иметь выходное местоположение, передаваемое в качестве параметра.

Таким образом, вы можете иметь интерфейс:

public interface CatMethods {
    void printInfo(Cat cat);
    void draw(Cat cat);
}

И реализация, которая внедряется в остальную часть вашей системы во время выполнения (с другими реализациями, используемыми для тестирования):

internal class CatMethodsForScreen implements CatMethods {
    public void printInfo(Cat cat) {
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public void draw(Cat cat) {
        //some draw code which uses cat.image
    }
}

Или добавьте дополнительные параметры, чтобы удалить побочные эффекты от этих методов:

public static class CatMethods {
    public static void printInfo(Cat cat, OutputHandler output) {
        output.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat, Canvas canvas) {
        //some draw code which uses cat.image and draws it on canvas
    }
}

Но почему вы пытаетесь включить OO-язык, такой как Java, в функциональный язык, вместо того, чтобы использовать функциональный язык с самого начала? Это что-то вроде «я ненавижу Java, но все используют его, поэтому я должен, но, по крайней мере, я собираюсь написать это по-своему». Если только вы не утверждаете, что парадигма ОО устарела и каким-то образом устарела. Я подписываюсь на школу мысли «Если ты хочешь Х, ты знаешь, где ее взять».
Каяман

@Kayaman, « Я подписываюсь на« Если вы хотите X, вы знаете, где взять это «школа мысли ». Я не. Я считаю это отношение близоруким и слегка оскорбительным. Я не использую Java, но я использую C # и игнорирую некоторые «настоящие OO» функции. Меня не волнует наследование, объекты с состоянием и тому подобное, и я пишу много статических методов и запечатанных (окончательных) классов. Размеры инструментов и сообщества вокруг C # не имеют себе равных. Для F # это намного беднее. Поэтому я подписываюсь на школу мысли «Если вы хотите X, попросите добавить ее в ваш текущий инструментарий».
Дэвид Арно

1
Я бы сказал, что игнорирование аспектов языка сильно отличается от попыток смешивать и сопоставлять функции других языков. Я также не пытаюсь написать «настоящий ОО», поскольку попытка просто следовать правилам (или принципам) в неточной науке, такой как программирование, не работает. Конечно, вам нужно знать правила, чтобы вы могли их нарушать. Слепое использование функций может плохо сказаться на развитии языка. Без обид, но ИМХО часть сцены FP, похоже, считает, что «FP - величайшая вещь после нарезанного хлеба, почему люди не поймут этого». Я бы никогда не сказал (или не подумал), что OO или Java - это «лучший».
Каяман

1
@Kayaman, но что значит «пытаться смешивать и сопоставлять функции из других языков»? Вы утверждаете, что функциональные возможности не должны быть добавлены в Java, потому что «Java - это ОО-язык»? Если так, то это, на мой взгляд, недальновидный аргумент. Пусть язык развивается с учетом потребностей разработчиков. Я считаю, что заставлять их переключаться на другой язык - неправильный подход. Конечно, некоторые «защитники FP» говорят о том, что FP - лучшая вещь в мире. И некоторые «сторонники ОО» говорят о том, что ОО «выиграл битву, потому что она явно лучше». Игнорировать их обоих.
Дэвид Арно,

1
Я не против ФП. Я использую это в Java, где это полезно. Но если программист говорит «Я хочу это», это как клиент, который говорит «Я хочу это». Программисты не являются языковыми дизайнерами, а клиенты - не программистами. Я проголосовал за ваш ответ, поскольку вы говорите, что «решение» ОП осуждается. Комментарий в вопросе более лаконичен, то есть он заново изобрел процедурное программирование на ОО-языке. Общая идея имеет смысл, как вы показали. Неанемичная модель предметной области не означает, что данные и поведение являются исключительными, и сторонние средства визуализации или докладчики будут запрещены.
Каяман

0

DTOs - объекты транспорта данных

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

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


-1

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

На мой взгляд, принцип « Сделай самое простое, что могло бы работать» должен победить по умолчанию, если у тебя нет веских причин усложнять дизайн. Так что в вашем конкретном примере я бы выбрал первый дизайн, который является более традиционным объектно-ориентированным подходом с данными и функциями вместе в одном классе.

Вот пара вопросов, которые вы можете задать себе по поводу приложения:

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

Ответ «да» на любой из этих вопросов может привести к более сложному дизайну с отдельными DTO и функциональными классами.


-1

Это хороший шаблон?

Вы, вероятно, получите здесь разные ответы, потому что люди следуют разным школам мысли. Итак, прежде чем я начну: этот ответ соответствует объектно-ориентированному программированию, как он описан Робертом К. Мартином в книге «Чистый код».


"Правда" ООП

Насколько я знаю, есть 2 интерпретации ООП. Для большинства людей это сводится к тому, что «объект - это класс».

Тем не менее, я нашел другую школу мысли более полезной: «Объект - это то, что что-то делает». Если вы работаете с классами, каждый класс будет одним из двух: « держатель данных » или объект . Держатели данных хранят данные (дух), объекты делают вещи. Это не значит, что у владельца данных не может быть методов! Подумайте о list: А на listсамом деле ничего не делает , но у него есть методы, чтобы обеспечить способ хранения данных .

Держатели данных

Вы Catяркий пример держателя данных. Конечно, вне вашей программы кошка - это что-то, что делает что-то , но внутри вашей программы a Cat- это String name, int weightи Image image.

То, что держатели данных ничего не делают, также не означает, что они не содержат бизнес-логики! Если у вашего Catкласса есть конструктор, требующий name, weightи imageвы успешно инкапсулировали бизнес-правило, согласно которому у каждого кота есть такие. Если вы можете изменить weightпосле того, как Catкласс был создан, это инкапсулированное другое бизнес-правило. Это очень простые вещи, но это просто означает, что очень важно не понимать их неправильно - и таким образом вы гарантируете, что это невозможно сделать неправильно.

Объекты

Объекты что-то делают. Если вы хотите очистить * объекты, мы можем изменить это на «Объекты делают одно ».

Печать информации о кошке - это работа объекта. Например, вы можете вызвать этот объект " CatInfoLogger" с помощью открытого метода Log(Cat). Это все, что нам нужно знать извне: этот объект регистрирует информацию о кошке, и для этого ему нужен Cat.

Внутри этот объект будет иметь ссылки на все, что ему нужно для выполнения своей единственной обязанности - регистрировать кошек. В этом случае это только ссылка на Systemfor System.out.println, но в большинстве случаев это будут частные ссылки на другие объекты. Если логика форматирования вывода перед печатью становится слишком сложной, CatInfoLoggerможно просто получить ссылку на новый объект CatInfoFormatter. Если позже, каждый журнал должен быть также записан в файл, вы можете добавить CatToFileLoggerобъект, который делает это.

Держатели данных против объектов - сводка

Теперь, зачем ты все это делаешь? Потому что теперь изменение класса меняет только одну вещь ( способ регистрации кошки в примере). Если вы меняете объект, вы меняете только способ выполнения определенного действия; если вы меняете владельца данных, вы изменяете только то, какие данные хранятся и / или каким образом они хранятся.

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

Overkill?

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

Однако любое «серьезное» (= больше, чем сценарий или 2) программное обеспечение, вероятно, достаточно велико, чтобы получить выгоду от такой структуры.


Ответ

Является ли разделение большинства классов на DTO и вспомогательные классы [...] хорошим или противодействующим?

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

Однако разделение данных и объектов рассматривается как хороший способ сохранить ваш код в чистоте *. Иногда вам понадобится только плоский «анемичный» DTO и все. В других случаях вам нужны методы для обеспечения соблюдения способа хранения данных. Только не связывайте функциональность вашей программы с данными.


* Это «Чисто» с заглавной буквой C, как название книги, потому что - помните первый абзац - речь идет об одной школе мысли, в то время как есть и другие.

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