Лучшая практика для добавления двунаправленного отношения в модели ОО - PullRequest
12 голосов
/ 21 октября 2010

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

Вот фрагмент кода 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);

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

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

PS Есть похожий вопрос: Управление двунаправленными ассоциациями в моей модели Java , но я не чувствую, что это отвечает моиммольба.

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

Ответы [ 5 ]

5 голосов
/ 21 октября 2010

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

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

4 голосов
/ 10 февраля 2013

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

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

на:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

По сути, идея заключается в том, что, когда специалист по персоналу заполняет "Новый сотрудник" JFrame и нажимает "Нанимать" JButton , выбранный отдел извлекается из JComboBox , который, в свою очередь, может быть заполнен из базы данных, и создается новый Person на основе информации внутри различных JComponents . Может быть, создан контракт на работу, содержащий хотя бы название должности и зарплату. Наконец, существует соответствующая бизнес-логика, которая связывает все объекты вместе и запускает обновления для всех состояний. Эта бизнес-логика запускается методом под названием hire , определенным в классе 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, в терминах ООП; this (в нашем случае это 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);
    }
}

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

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

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

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

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

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

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

Я написал эту маленькую библиотеку менее чем за час. Затем я написал программу-драйвер, и она работала корректно с переключателем 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();
    }
}

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

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

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

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

        if (mDepartment != null)
            resign();

        mDepartment = department;
    }

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

    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;
    }

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

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

0 голосов
/ 08 июня 2019

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

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; }
}
0 голосов
/ 21 октября 2010

Если вы поддерживаете двунаправленные отношения в 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)
    }
}

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

0 голосов
/ 21 октября 2010

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

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

...