Лучшая практика для добавления двунаправленного отношения в модели OO

Я изо всех сил пытаюсь придумать хороший способ добавления двунаправленного отношения в OO-модели. Скажем, есть Клиент, который может разместить много ордеров, то есть существует связь "один-ко-многим" между классами Customer и Order, которые должны быть общими в обоих направлениях: для конкретного клиента должно быть возможно рассказать все заказы, которые они разместили, для заказа вы можете сообщить клиенту.

Вот фрагмент кода Java, хотя вопрос в основном зависит от языка:

class Customer {
 private Set orders = new HashSet<Order> ();

        public void placeOrder (Order o) {
     orders.add(o);
            o.setCustomer(this);
 }
}

class Order {
 private Customer customer;
        public void setCustomer (Customer c) {
  customer = c;
 }
}

Что меня забивает, так это то, что с учетом модели кто-то может легко позвонить:

o.setCustomer(c);

вместо правильного

c.placeOrder(o);

образует однонаправленную ссылку вместо двунаправленной.

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

P.S. Существует аналогичный вопрос: Управление двунаправленными ассоциациями в моей модели java, однако я не чувствую, что это отвечает на мою просьбу.

P.S.S. Любые ссылки на исходный код реальных проектов, реализующих бизнес-модель на вершине db4o, очень ценятся!

Ответ 1

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

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

Ответ 2

Это очень интересный вопрос, который имеет глубокие последствия для теории и практики ООП. Сначала я расскажу вам быстрый и грязный способ (почти) выполнить то, что вы просили. В общем, я не рекомендую это решение, но так как никто не упомянул его и (если память не подвела меня), оно упоминается в книге Мартина Фаулера (UML Distilled), о нем, вероятно, стоит поговорить; Вы можете изменить определение метода setCustomer из:

public void setCustomer (Customer c) {
    customer = c;
}

чтобы:

void setCustomer (Customer c) {
    customer = c;
}

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

Видимость пакетов в значительной степени допускается в обычной практике программирования на Java; Я чувствую, что в сообществе C++ модификатор друга не так терпим, как видимость пакетов в Java, несмотря на то, что он служит аналогичной цели. Я не могу понять почему, потому что друг гораздо более избирателен: в основном для каждого класса вы можете указать другие классы и функции друзей, которые будут иметь доступ к закрытым членам первого класса.

Тем не менее, нет никаких сомнений в том, что ни видимость пакета Java, ни C++ друг хорошие представители, что ООП средств, и даже не из какого объекта на основе средств программирования (ООП в основном ОБП плюс наследование и полиморфизм, я буду использовать термин ООП с этого момента). Основным аспектом ООП является то, что существуют объекты, называемые объектами, и они общаются, отправляя сообщения друг другу. Объекты имеют внутреннее состояние, но это состояние может быть изменено только самим объектом. Государство, как правило, структурировано, то есть представляет собой набор полей, таких как имя, возраст и порядок. В большинстве языков сообщения являются синхронными и не могут быть отброшены по ошибке, как почта или пакет UDP. Когда вы пишете c.placeOrder(o), это означает, что отправитель, который является этим, отправляет сообщение в c. Содержимое этого сообщения - placeOrder и o.

Когда объект получает сообщение, он должен обработать его. Java, C++, С# и многие другие языки предполагают, что объект может обрабатывать сообщение, только если его класс определяет метод с соответствующим именем и списком формальных параметров. Набор методов класса называется его интерфейсом, и языки, такие как Java и С#, также имеют соответствующую конструкцию, а именно интерфейс, для моделирования концепции набора методов. Обработчик сообщения c.placeOrder(o) - это метод:

public void placeOrder(Order o) {
    orders.add(o);
    o.setCustomer(this);
}

Тело метода - это то, где вы пишете инструкции, которые при необходимости изменят состояние объекта c. В этом примере поле заказов изменено.

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

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

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

Основная проблема с методами ООП возникает всякий раз, когда группа объектов должна изменить внутреннее состояние одновременно, в результате внешнего запроса, продиктованного бизнес-логикой. Например, когда человека нанимают, случается много вещей. 1) Сотрудник должен быть настроен так, чтобы указывать на его отдел; 2) он должен быть добавлен в список наемных работников в отделе; 3) что-то еще должно быть добавлено где-то еще, например, копия контракта (возможно, даже его сканирование), страховая информация и так далее. Первые два действия, которые я привел, являются точным примером установления (и поддержания, когда сотрудник увольняется или переводится) двунаправленной ассоциации, подобной той, которую вы описали между клиентами и заказами.

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

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

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

В принципе идея заключается в том, что, когда парень человеческого ресурса заполняет "новый сотрудник" JFrame и щелкает "Прокат" JButton, выбранный отдел извлекается из JComboBox, который, в свою очередь, может быть заселен из базы данных, и новый человек создан на основе информации внутри различных JComponents. Может быть, создан контракт на работу, содержащий хотя бы название должности и зарплату. Наконец, существует соответствующая бизнес-логика, которая связывает все объекты вместе и запускает обновления для всех состояний. Эта бизнес-логика запускается методом найма, определенным в классе Department, который принимает в качестве аргументов Person и Contract. Все это может произойти в ActionListener JButton.

Department department = (Department)cbDepartment.getSelectedItem();
Person person = new Person(tfFirstName.getText(), tfLastName.getText());
Contract contract = new Contract(tfPositionName.getText(), Integer.parseInt(tfSalary.getText()));
department.hire(person, contract);

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

Контракт очень простой класс.

package com.example.payroll.domain;

public class Contract {

    private String mPositionName;
    private int mSalary;

    public Contract(String positionName, int salary) {
        mPositionName = positionName;
        mSalary = salary;
    }

    public String getPositionName() {
        return mPositionName;
    }

    public int getSalary() {
        return mSalary;
    }

    /*
        Not much business logic here. You can think
        about a contract as a very simple, immutable type,
        whose state doesn't change and that can't really
        answer to any message, like a piece of paper.
    */
}

Человек намного интереснее.

package com.example.payroll.domain;

public class Person {

    private String mFirstName;
    private String mLastName;
    private Department mDepartment;
    private boolean mResigning;

    public Person(String firstName, String lastName) {
        mFirstName = firstName;
        mLastName = lastName;
        mDepartment = null;
        mResigning = false;
    }

    public String getFirstName() {
        return mFirstName;
    }

    public String getLastName() {
        return mLastName;
    }

    public Department getDepartment() {
        return mDepartment;
    }

    public boolean isResigning() {
        return mResigning;
    }

    // ========== Business logic ==========

    public void youAreHired(Department department) {
        assert(department != null);
        assert(mDepartment != department);
        assert(department.isBeingHired(this));

        if (mDepartment != null)
            resign();

        mDepartment = department;
    }

    public void youAreFired() {
        assert(mDepartment != null);
        assert(mDepartment.isBeingFired(this));

        mDepartment = null;
    }

    public void resign() {
        assert(mDepartment != null);

        mResigning = true;
        mDepartment.iResign(this);
        mDepartment = null;
        mResigning = false;
    }
}

Отдел довольно крутой.

package com.example.payroll.domain;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public class Department {

    private String mName;
    private Map<Person, Contract> mEmployees;
    private Person mBeingHired;
    private Person mBeingFired;

    public Department(String name) {
        mName = name;
        mEmployees = new HashMap<Person, Contract>();
        mBeingHired = null;
        mBeingFired = null;
    }

    public String getName() {
        return mName;
    }

    public Collection<Person> getEmployees() {
        return mEmployees.keySet();
    }

    public Contract getContract(Person employee) {
        return mEmployees.get(employee);
    }

    // ========== Business logic ==========

    public boolean isBeingHired(Person person) {
        return mBeingHired == person;
    }

    public boolean isBeingFired(Person person) {
        return mBeingFired == person;
    }

    public void hire(Person person, Contract contract) {
        assert(!mEmployees.containsKey(person));
        assert(!mEmployees.containsValue(contract));

        mBeingHired = person;
        mBeingHired.youAreHired(this);
        mEmployees.put(mBeingHired, contract);
        mBeingHired = null;
    }

    public void fire(Person person) {
        assert(mEmployees.containsKey(person));

        mBeingFired = person;
        mBeingFired.youAreFired();
        mEmployees.remove(mBeingFired);
        mBeingFired = null;
    }

    public void iResign(Person employee) {
        assert(mEmployees.containsKey(employee));
        assert(employee.isResigning());

        mEmployees.remove(employee);
    }
}

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

Отдел может получать следующие сообщения:

  • isBeingHired: отправитель хочет знать, находится ли конкретный человек в процессе приема на работу в отдел.
  • isBeingFired: отправитель хочет знать, находится ли конкретный человек в процессе увольнения из отдела.
  • найм: отправитель хочет, чтобы отдел нанял человека с указанным контрактом.
  • огонь: отправитель хочет, чтобы отдел уволил сотрудника.
  • iResign: отправитель, скорее всего, является сотрудником и сообщает отделу, что уходит в отставку.

Человек может получать следующие сообщения:

  • youAreHired: департамент отправляет это сообщение, чтобы проинформировать человека о его приеме на работу.
  • youAreFired: департамент отправляет это сообщение, чтобы сообщить сотруднику, что он уволен.
  • подать в отставку: отправитель хочет, чтобы человек подал в отставку с его нынешней должности. Обратите внимание, что сотрудник, нанятый другим отделом, может отправить себе сообщение об отставке, чтобы уйти со старой работы.

Поля Person.mResigning, Department.isBeingHired, Department.isBeingFired - это то, что я использую для кодирования вышеупомянутых недопустимых состояний: когда любое из них "ненулевое", приложение находится в недопустимом состоянии, но находится на пути к действительный.

Также обратите внимание, что нет установленных методов; это контрастирует с обычной практикой работы с JavaBeans. JavaBeans по сути очень похожи на структуры C, потому что они имеют тенденцию иметь пару set/get (или set/is for boolean) для каждого частного свойства. Однако они допускают проверку набора, например, вы можете проверить, что строка, передаваемая в метод набора, не является нулевой и не пустой, и в конечном итоге выдает исключение.

Я написал эту маленькую библиотеку менее чем за час. Затем я написал программу-драйвер, и она работала правильно с переключателем JVM -ea (включение утверждений) при самом первом запуске.

package com.example.payroll;

import com.example.payroll.domain.*;

public class App {

    private static Department resAndDev;
    private static Department production;
    private static Department[] departments;

    static {
        resAndDev = new Department("Research & Development");
        production = new Department("Production");
        departments = new Department[] {resAndDev, production};
    }

    public static void main(String[] args) {

        Person person = new Person("John", "Smith");

        printEmployees();
        resAndDev.hire(person, new Contract("Project Manager", 3270));
        printEmployees();
        production.hire(person, new Contract("Quality Control Analyst", 3680));
        printEmployees();
        production.fire(person);
        printEmployees();
    }

    private static void printEmployees() {

        for (Department department : departments) {
            System.out.println(String.format("Department: %s", department.getName()));

            for (Person employee : department.getEmployees()) {
                Contract contract = department.getContract(employee);

                System.out.println(String.format("  %s. %s, %s. Salary: EUR %d", contract.getPositionName(), employee.getFirstName(), employee.getLastName(), contract.getSalary()));
            }
        }

        System.out.println();
    }
}

Тот факт, что это сработало, не самая крутая вещь; круто то, что только отдел найма или увольнения уполномочен отправлять вам сообщенияAreHired и youAreFired тому, кого нанимают или увольняют; аналогичным образом, только увольняющийся сотрудник может отправить сообщение iResign в свой отдел и только в этот отдел; любое другое незаконное сообщение, отправленное с main, вызовет утверждение. В реальной программе вы будете использовать исключения вместо утверждений.

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

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

    public void youAreHired(Department department) {
        assert(department != null);
        assert(mDepartment != department);
        assert(department.isBeingHired(this));

        if (mDepartment != null)
            resign();

        mDepartment = department;
    }

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

    public void youAreHired(Department department) {
        assert(department != null);
        assert(mDepartment == null);
        assert(department.isBeingHired(this));

        mDepartment = department;
    }

Мы также можем расширить приложение, сделав youAreHired булевой функцией, которая возвращает true, только если старый отдел в порядке с новым наймом. Очевидно, что нам может потребоваться изменить что-то еще, в моем случае я сделал Person.resign логической функцией, которая, в свою очередь, может потребовать, чтобы Department.iResign был логической функцией:

    public boolean youAreHired(Department department) {
        assert(department != null);
        assert(mDepartment != department);
        assert(department.isBeingHired(this));

        if (mDepartment != null)
            if (!resign())
                    return false;

        mDepartment = department;

        return true;
    }

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

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

Ответ 3

Нет единого ответа. Это действительно зависит от участвующих классов. В вашем случае вы, очевидно, не хотите давать людям возможность делать что-то недействительное, чтобы я избавился от Order.SetCustomer.

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

Ответ 4

Если вы поддерживаете двунаправленную связь в Customer.placeOrder(Order), почему бы вам не сделать то же самое в Order.setCustomer(Customer)?

class Order {
    private Customer customer;
    public void setCustomer (Customer c) {
        customer = c;
        c.getOrders().add(this);
        // ... or Customer.placeOrder(this)
    }
}

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

Ответ 5

Я думаю, что лучший способ в этом случае - передать ответственность за проводку другому классу:

class OrderManager {
    void placeOrder(Customer c, Order o){
        c.addOrder(o);
        o.setCustomer(c);
    }
}

class Customer {
    private Set<Order> orders = new LinkedHashSet<Order>();
    void addOrder(Order o){ orders.add(o); }
}

class Order {
    private Customer customer;
    void setCustomer(Customer c){ this.customer=c; }
}