Объектно-ориентированное программирование: разделение данных и поведения

Недавно мы обсудили разделение данных и поведения в классах. Концепция разделения данных и поведения реализуется путем размещения модели домена и ее поведения в отдельных классах.
Однако я не убежден в предполагаемых преимуществах такого подхода. Хотя это, возможно, было придумано "великим" (я думаю, что это Мартин Фаулер, хотя я не уверен). Здесь я представляю простой пример. Предположим, у меня есть класс Person, содержащий данные для Person и его методы (поведение).

class Person
{
    string Name;
    DateTime BirthDate;

    //constructor
    Person(string Name, DateTime BirthDate)
    {
        this.Name = Name;
        this.BirthDate = BirthDate;
    }

    int GetAge()
    {
        return Today - BirthDate; //for illustration only
    }

}

Теперь разделите поведение и данные на отдельные классы.

class Person
{
    string Name;
    DateTime BirthDate;

    //constructor
    Person(string Name, DateTime BirthDate)
    {
        this.Name = Name;
        this.BirthDate = BirthDate;
    }
}

class PersonService
{
    Person personObject;

    //constructor
    PersonService(string Name, DateTime BirthDate)
    {
        this.personObject = new Person(Name, BirthDate);
    }

    //overloaded constructor
    PersonService(Person personObject)
    {
        this.personObject = personObject;
    }

    int GetAge()
    {
        return personObject.Today - personObject.BirthDate; //for illustration only
    }
}

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

class Employee: Person
{
    Double Salary;

    Employee(string Name, DateTime BirthDate, Double Salary): base(Name, BirthDate)
    {
        this.Salary = Salary;       
    }

}

class EmployeeService: PersonService
{
    Employee employeeObject;

    //constructor
    EmployeeService(string Name, DateTime BirthDate, Double Salary)
    {
        this.employeeObject = new Employee(Name, BirthDate, Salary);
    }

    //overloaded constructor
    EmployeeService(Employee employeeObject)
    {
        this.employeeObject = employeeObject;
    }
}

Обратите внимание, что даже если мы отделим поведение в отдельном классе, нам по-прежнему нужен объект класса Data для методов класса Behavior. Таким образом, наш класс Behavior содержит как данные, так и поведение, хотя мы имеем данные в форме модельного объекта.
Вы можете сказать, что вы можете добавить некоторые интерфейсы в микс, чтобы мы могли иметь IPersonService и IEmployeeService. Но я думаю, что введение интерфейсов для каждого класса и наследование с интерфейсов не выглядит нормально.

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

Ответ 1

Фактически, Мартин Фаулер говорит, что в модели домена данные и поведение следует комбинировать. Посмотрите AnemicDomainModel.

Ответ 2

Я согласен, разделение по мере того, как вы реализовывали, громоздко. Но есть и другие варианты. Как насчет объекта ageCalculator, который имеет метод getAge (person p)? Или person.getAge(IAgeCalculator calc). Или еще лучше calc.getAge(IAgeble a)

Есть несколько преимуществ, которые возникают от разделения этих проблем. Предполагая, что вы намеревались вернуться к своей реализации, лет, что, если человеку/ребенку всего 3 месяца? Вы возвращаете 0? +0,25? Выбросить исключение? Что, если я хочу возраст собаки? Возраст в десятилетия или часы? Что, если я хочу возраст на определенную дату? Что, если человек мертв? Что делать, если я хочу использовать марсианскую орбиту в течение года? Или ивритский каландр?

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

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

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

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

Ответ 3

Как ни странно, ООП часто описывается как объединение данных и поведения.

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

Различные уровни приложения могут иметь более процедурный изгиб, который поддается модели обслуживания, как вы показали, но обычно это будет только на самом краю системы. И даже в этом случае это внутренне реализуется традиционным дизайном объектов (данные + поведение). Обычно это просто головная боль.

Ответ 4

Возраст интриги к человеку (любому человеку). Поэтому он должен быть частью объекта Person.

hasExperienceWithThe40mmRocketLauncher() не является неотъемлемым для человека, но, возможно, для интерфейса MilitaryService, который может либо расширить, либо объединить объект Person. Поэтому он не должен быть частью объекта Person.

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

В принципе, если вы видите, что добавляете к вашему базовому объекту такие вещи, как "hasServedInMilitary", у вас проблемы. Затем вы будете выполнять множество утверждений, например if (p.hasServedInMilitary()) blablabla. Это действительно логично то же самое, что делать instanceOf() все время и указывает, что Person и "Person, кто видел военную службу", действительно две разные вещи и должны быть как-то отключены.

Сделав шаг назад, ООП о сокращении числа операторов if и switch и вместо этого позволяет различным объектам обрабатывать вещи в соответствии с их конкретными реализациями абстрактных методов/интерфейсов. Разделение данных и поведения способствует этому, но нет никаких оснований считать его крайними и отделить все данные от всего поведения.

Ответ 5

Я понимаю, что я опаздываю на год, чтобы ответить на это, но в любом случае... lol

Я отделил Behaviors раньше, но не так, как вы показали.

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

Если бы я делал игру, например, некоторые виды поведения, доступные для объектов, могли бы быть способностью ходить, летать, прыгать и т.д.

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

Для IWalkable у вас может быть...

CannotWalk: IWalkableBehavior

Ограниченный доступ: IWalkableBehavior

UnlimitedWalking: IWalkableBehavior

Аналогичный шаблон для IFlyableBehavior и IJumpableBehavior.

Эти конкретные классы будут реализовывать поведение для CannotWalk, LimitedWalking и UnlimitedWalking.

В ваших конкретных классах для объектов (таких как враг) у вас будет локальный экземпляр этих Поведений. Например:

IWalkableBehavior _walking = new CannotWalk();

Другие могут использовать новый ограниченный доступ() или новый UnlimitedWalking();

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

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

Я не могу объяснить это очень четко, но в основном я имею в виду, это смотреть на это наоборот. Вместо того, чтобы инкапсулировать ваш объект (то, что вы вызываете Data здесь) в класс Behavior, инкапсулируйте Поведение в объект с помощью интерфейсов, и это дает вам "свободную связь", позволяющую уточнить поведение, а также легко расширить каждую "поведенческую базу", (Walking, Flying, Jumping и т.д.) С новыми реализациями, но ваши объекты сами не знают разницы. Они просто имеют поведение Walking, даже если это поведение определено как CannotWalk.