在 OO 模型中添加双向关系的最佳实践

2024-01-11

我正在努力想出一种在面向对象模型中添加双向关系的好方法。假设有一个客户可以下很多订单,也就是说客户和订单类之间存在一对多关联,需要双向遍历:对于一个特定的客户,应该可以告诉所有客户他们已经下的订单,对于订单应该可以告诉客户。

下面是一段 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);

形成单向链路而不是双向链路。

仍在学习 OOP,任何人都可以帮助解决这个问题的惯用且实用的方法,而无需诉诸“反射”或花哨的框架(无论如何都会依赖于反射)。

附:还有一个类似的问题:管理我的 java 模型中的双向关联 https://stackoverflow.com/questions/1813866/managing-bidirectional-associations-in-my-java-model,但我不认为它回答了我的请求。

附言非常感谢任何在 db4o 之上实现业务模型的实际项目源代码的链接!


这是一个very有趣的问题,对 OOP 的理论和实践具有深远的影响。首先,我将告诉您(几乎)完成您所要求的任务的快速而肮脏的方法。一般来说,我不推荐这个解决方案,但是因为没有人提到它并且(如果我没记错的话)它isMartin Fowler (UML Distilled) 的书中提到过,这可能值得一谈;你可以改变的定义设置客户方法来自:

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

to:

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

并确保Customer and Order位于同一个包中。如果不指定访问修饰符,设置客户默认为package可见性,这意味着它只能从同一包中的类访问。显然,这并不能保护您免受除Customer在同一个包内。另外,如果您决定移动,您的代码将会损坏Customer and Order在两个不同的包中。

在 Java 的常见编程实践中,包可见性在很大程度上是可以容忍的;我觉得在 C++ 社区中friend尽管修饰符具有类似的用途,但其容忍度不如 Java 中的包可见性。我实在无法理解为什么,因为friend更具选择性:基本上对于每个类,您都可以指定其他友元类和函数,这些类和函数将能够访问第一个类的私有成员。

然而,毫无疑问,无论是 Java 的包可见性还是 C++ 的包可见性,friend很好地代表了 OOP 的含义,甚至不能代表基于对象的编程的含义(OOP 基本上是 OBP 加上继承和多态性;从现在开始我将使用术语 OOP)。 OOP 的核心是存在称为objects,并且它们通过相互发送消息进行通信。对象具有内部状态,但该状态只能由对象本身改变。状态通常是结构化的即它基本上是一个集合fields例如name, age and orders。在大多数语言中,消息都是同步的,并且不会被错误地丢弃,就像邮件或 UDP 数据包一样。当你写的时候c.placeOrder(o)代表着sender,即this, 正在发送消息至c。这条消息的内容是下订单 and o.

当对象接收到消息时,它必须处理该消息。 Java、C++、C# 和许多其他语言假定对象只有在其类定义了消息时才能处理消息method具有适当的名称和形式参数列表。一个类的方法的集合称为它的界面,Java 和 C# 等语言也有适当的构造,即界面对一组方法的概念进行建模。消息的处理程序c.placeOrder(o)方法是:

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

The body方法的位置是您编写将改变对象状态的指令的地方c,如果需要的话。在这个例子中orders字段被修改。

从本质上讲,这就是 OOP 的含义。 OOP 是在模拟的背景下开发的,其中基本上有很多相互通信的黑匣子,每个黑匣子负责自己的内部状态。

大多数现代语言都完美地遵循这个方案,但前提是你限制自己private领域和公共/受保护方法。不过,也有一些问题。例如,在类的方法中Customer您可以访问私有字段,例如orders, of another Customer object.

您链接的页面上的两个答案实际上非常好,我都赞成。然而,我认为,对于 OOP 来说,拥有真正的双向关联是完全合理的,正如您所描述的。原因是要向某人发送消息,您必须有对他的引用。这就是为什么我会尝试概述问题所在,以及为什么我们 OOP 程序员有时会遇到这个问题。长话短说,realOOP 有时很乏味,并且非常类似于复杂的形式方法。但它生成的代码更容易阅读、修改和扩展,并且通常可以让您免去很多麻烦。我想把这个写下来已经有一段时间了,我认为你的问题是一个很好的借口。

当一组对象由于外部请求而必须同时改变内部状态时,OOP 技术的主要问题就出现了,商业逻辑。例如,当一个人被雇用时,会发生很多事情。 1)员工必须配置为指向其部门; 2)必须将其列入本部门聘用人员名单; 3)必须在其他地方添加其他内容,例如合同副本(甚至可能是scan)、保险信息等。我引用的前两个操作正是建立(并在员工被解雇或调动时维护)双向关联的示例,就像您在客户和订单之间描述的那样。

在过程编程中Person, 部门 and Contract将是结构,以及像这样的全局程序雇佣部门内人员并签订合同与点击用户界面中的按钮相关联的操作将通过三个指针来操纵这些结构的 3 个实例。整个业务逻辑都在这个函数内部,必须考虑到every更新这三个对象的状态时可能出现的特殊情况。例如,当您单击雇用某人的按钮时,他可能已经在另一个部门工作,甚至更糟的是在同一部门工作。计算机科学家知道特殊情况不好 http://luigigobbi.com/jokesaboutmathematicians.htm。雇用一个人基本上是一个非常复杂的用例,有很多扩展 http://blog.casecomplete.com/post/Writing-Use-Case-Extensions.aspx这种情况并不经常发生,但必须考虑到这一点。

RealOOP 要求对象必须交换消息才能完成此任务。业务逻辑分为责任的几个对象。CRC卡 http://en.wikipedia.org/wiki/Class-responsibility-collaboration_card是研究 OOP 中业务逻辑的非正式工具。

要从valid从约翰失业的状态到他担任研发部门项目经理的另一个有效状态,需要经过多个无效状态,至少一个。所以有一个初始状态,一个无效状态和一个最终状态,一个人和一个部门之间至少有两条消息交换。您还可以确保该部门必须收到一条消息,以便有机会改变其内部状态,出于同样的原因,该人员也必须收到另一条消息。中间状态是无效的,因为它并不真正存在于现实世界中,或者可能存在但并不重要。但是,应用程序中的逻辑模型必须以某种方式跟踪它。

基本上这个想法是,当人力资源人员填补“新员工”时JFrame并点击“雇用”JButton,所选部门是从J组合框,这又可能是从数据库中填充的,以及一个新的Person是根据各种内部信息创建的J组件。也许创建的工作合同至少包含职位名称和工资。最后,有适当的业务逻辑将所有对象连接在一起并触发所有状态的更新。该业务逻辑由一个名为的方法触发hire类中定义的部门,它以参数 aPerson and a Contract。这一切都可能发生在动作监听器 of the 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);

我想用 OOP 的术语强调一下第 4 行发生了什么;this(在我们的例子中是动作监听器, 正在发送消息至部门,说他们必须雇用person under contract。让我们看一下这三个类的合理实现。

Contract是一个非常简单的类。

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.
    */
}

Person更有趣。

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

我定义的消息至少非常皮托式的姓名;在真实的应用程序中,您可能不想使用这样的名称,但在本示例的上下文中,它们有助于以有意义且直观的方式对对象之间的交互进行建模。

部门可以收到以下消息:

  • 正在招聘:发件人想知道某个特定人员是否正在被该部门聘用。
  • 正在被解雇:发件人想知道某个人是否正在被部门解雇。
  • hire:发件人希望部门雇用具有指定合同的人员。
  • fire:发件人希望部门解雇一名员工。
  • iResign:发件人可能是一名员工,并告诉部门他要辞职。

Person可以收到以下消息:

  • 你被录取了:部门发送此消息是为了通知该人他已被录用。
  • 你被开除了:部门发送此消息是为了通知该员工他被解雇了。
  • resign:发件人希望此人辞去目前的职位。请注意,被其他部门雇用的员工可以发送resign给自己发一条信息,以辞掉原来的工作。

田野Person.mResigning, 部门正在招聘, 部门正在被解雇是我用来编码上述内容的invalid状态:当其中任何一个为“非零”时,应用程序处于invalid状态,但正在走向有效状态。

另请注意,没有set方法;这与合作的常见做法形成鲜明对比JavaBeans http://en.wikipedia.org/wiki/JavaBeans。 JavaBean 本质上与 C 结构非常相似,因为它们往往为每个私有属性都有一个 set/get(或 boolean 的 set/is)对。然而,它们确实允许验证集合,例如您可以检查String传递给 set 方法时不为 null 且不为空,并最终引发异常。

我在不到一个小时的时间内写了这个小库。然后我编写了一个驱动程序,它在第一次运行时与 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();
    }
}

但事实上它有效并不是一件很酷的事情。最酷的是,只有招聘或解雇部门有权发送你被录取了 and 你被开除了给被雇用或解雇的人的消息;同样,只有辞职员工才能发送iResign向其部门发送消息,并且仅向该部门发送消息;发送自的任何其他非法消息main会触发一个断言。在真实的程序中,您将使用异常而不是断言。

这一切是不是太过分了?诚然,这个例子有点极端。但我觉得这就是OOP的精髓。对象必须合作实现某个目标,即改变全局状态根据预定的业务逻辑来控制应用程序,在本例中hiring, firing and resign。有些程序员认为业务问题不适合 OOP,但我不同意;业务问题基本上是工作流程,它们本身是非常简单的任务,但涉及很多参与者(即objects),通过消息进行通信。继承、多态性和所有模式都是受欢迎的扩展,但它们不是面向对象过程的基础。特别是,基于参考的关联通常优于实现继承.

请注意,通过使用静态分析、按合同设计和自动定理证明器,您将能够验证您的程序对于任何可能的输入是否正确,without运行它。 OOP 是使您能够以这种方式思考的抽象框架。它不一定比过程式编程更紧凑,也不会自动导致代码重用。但我坚持认为它更容易阅读、修改和扩展;我们来看看这个方法:

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

我们还可以通过以下方式扩展应用程序你被录取了一个布尔函数,返回true仅当旧部门同意新招聘时。显然我们可能需要改变其他东西,就我而言,我做了人.辞职一个布尔函数,这又可能需要部门.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;
    }

现在,现任员工对是否可以调动拥有最终决定权。当前部门可以将确定这一点的责任委托给一个Strategy http://en.wikipedia.org/wiki/Strategy_pattern反过来,这可能会考虑员工参与的项目、项目的截止日期和各种合同限制。

从本质上讲,向客户添加订单确实是业务逻辑的一部分。如果需要双向关联,并且反射不是一种选择,并且对此提出的解决方案和链接的问题都不是令人满意的,我认为唯一的解决方案是这样的。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在 OO 模型中添加双向关系的最佳实践 的相关文章

随机推荐

  • kworker 线程的起源

    在我新安装的使用内核 3 2 的系统上 我看到一个 kworker 线程不断消耗 CPU 我想知道内核 模块的哪一部分创建了这个工作队列 如何跟踪名为 kworker 0 3 的 kworker 线程到其在内核空间中的起源 我尝试查看 sy
  • 如何在 Rust 的闭包内重用外部作用域的值?

    我正在尝试添加一个网络服务器 hyper 在一个小的 Rust 程序中 我遇到了移动问题 main rs Suppose this is something meaningful and used in multiple places in
  • 在 JavaScript 中使用匿名函数调用删除事件侦听器

    我正在尝试删除创建的 span 元素的事件侦听器 其中调用的函数位于闭包内 我尝试了各种方法 但似乎都不起作用 var MyClass function MyClass prototype addSpan function el var s
  • 在 Android 上将音频路由到蓝牙耳机(非 A2DP)

    我有一个非 A2DP 单耳 BT 耳机 Plantronics 510 想将其与我的 Android HTC Magic 一起使用来收听低质量音频 例如播客 有声读物 经过多次谷歌搜索后 我发现只有电话音频可以路由到非 A2DP BT 耳机
  • 节点如何运行交互脚本( python、shell 等..)

    我有一个 python 交互脚本 如下例所示 python sync email py please input your email email protected cdn cgi l email protection validatin
  • Python 生成所有独特的排列并且没有排序的重复[已修复,我的意思是寻找组合]

    编辑 我混淆了排列和组合之间的区别 编辑问题只是为了保留它 因为我无法删除它 虽然我意识到了自己的错误 我已经浏览了这个问题一段时间 但找不到适合我正在寻找的解决方案 基本概念是如果我运行一个函数gen permutations 1 2 生
  • mongodb 查询:$size 和 $gt 返回始终为 0

    我想知道如何才能在 mongodb 查询中启用数组大小 的不等式 例如 我有以下两个查询 gt db userStats count sessions size 1 1381 gt db userStats count sessions s
  • 如何获取列表中项目的 ModelState 键

    Problem 我有一个用户可以编辑的字段列表 提交模型后 我想检查这些项目是否有效 我无法使用数据符号 因为每个字段都有不同的验证过程 直到运行时我才会知道 如果验证失败我使用ModelState AddModelError string
  • 将 Moqs It.IsAny 作为方法参数传递

    首先是我的开发环境的一些信息 Net框架4 5 Moq 4 10 Autofac 4 2 NUnit 3 11 我尝试模拟一个需要一些字符串参数的函数 我想使用It IsAny
  • 如何在node.js+express中捕获“响应结束”事件?

    我想编写一个快速中间件函数 它在响应的 结束 事件 如果存在 上设置一个侦听器 目的是根据最终处理程序决定发送的 http 响应代码进行清理 例如记录数据库事务的响应代码和回滚 提交 即 我希望此清理对于最终调用者是透明的 我想在快递中做类
  • 在 TypeScript 中定义自定义 jQuery UI 小部件

    我们目前正在考虑将 JavaScript 项目转换为 TypeScript 我们的应用程序严重依赖定制开发的 jQuery UI 小部件 在我们当前的代码库中 我们使用深度复制机制来继承小部件定义 例如 允许我们声明一个通用的TableWi
  • Scrapy 和 robots.txt 的尊重

    我昨天发现Scrapy默认尊重robots txt文件 ROBOTSTXT OBEY True 如果我请求一个 URLscrapy shell url 如果我有回应 是否意味着url不受robots txt保护 根据文档 只有当您使用创建项
  • 如何在悬停在anchorEl 和“popover”上时继续显示“popover”?

    在这个例子中material ui https material ui com utils popover mouse over interaction https material ui com utils popover mouse o
  • 在 Hibernate 中保留 Joda DateTime 而不是 Java Date

    我的实体当前包含 java Date 属性 我开始经常使用 Joda Time 进行日期操作和计算 这意味着我必须不断地将日期转换为 Joda DateTime 对象 然后再转换回来 所以我想知道 是否有任何理由我不应该只更改实体来存储 J
  • 在TeX中制作一个牢不可破的块[关闭]

    Closed 这个问题是无关 help closed questions 目前不接受答案 我想在 TeX 中做如下的事情 begin nobreak Text here will not split over pages it will r
  • QueryOver 上的析取始终引用根实体

    我试图在 X 个实现包含日期信息的特定接口的实体上使用析取来添加一定数量的 OR 条件 我的问题是 当生成 SQL 时 所有析取条件都指向 QueryOver 的根实体 我创建了一个通用方法来添加我的条件 public static Que
  • 如何在 React 中的组件外部访问 Redux 存储

    我从 Redux 开始 我总是在带有 connect 和 mapStateToProps 的组件中使用它 但现在我想每隔 x 次使用 setInterval 调用我的 API 以检查服务器是否有未存储在 Redux 存储中的新数据 并替换它
  • Keycloak 将新注册的用户保存到应用程序数据库中

    在我的应用程序中 我有一些实体关系 例如用户和组织 我想我会使用Keycloak 这样我就不必实现自定义注册 登录 密码重置 授权和身份验证 问题是 当用户注册到 Keycloak 时 我的应用程序的内部数据库中没有新的用户实体 有没有办法
  • 访问 Jboss7 或 Wildfly 上的多个 Web 应用程序

    我知道我们可以在 JBoss 7 或 Wildfly 上部署多个 Web 应用程序 但是我们如何使用不同的端口访问不同的Web应用程序呢 我们在哪里为 Web 应用程序设置该端口 例如 application1 可在 x x x x 808
  • 在 OO 模型中添加双向关系的最佳实践

    我正在努力想出一种在面向对象模型中添加双向关系的好方法 假设有一个客户可以下很多订单 也就是说客户和订单类之间存在一对多关联 需要双向遍历 对于一个特定的客户 应该可以告诉所有客户他们已经下的订单 对于订单应该可以告诉客户 下面是一段 Ja