在Java中继承和封装集合类

5
假设我有以下类型的数据:
class Customer {
  String id; // unique
  OtherCustData someOtherData;
}

class Service {
  String url; // unique
  OtherServiceData someOtherData;
}

class LastConnection {
  Date date;
  OtherConnData someOtherData; // like request or response
}

现在我需要记住每个客户端连接到每个服务的时间。
我会建立以下结构:
Map<Customer, Map<Service, LastConnection>> lastConnections;

或者,为了能够通过ID进行搜索而不必编写所有的equal()和hashCode():
Map<String, Map<String, LastConnection>> lastConnections;

现在我可以通过以下方式访问LastConnection数据:

LastConnection connection = lastConnections.get(custId).get(srvUrl);

所有这些看起来很丑,尤其是我必须将它作为参数传递给许多期望LastConnections映射的映射的方法,因此我考虑创建自己的类,它应该看起来像这样:

class CustomerConnections extends HashMap<String, LastConnection> {
}

class AllConnections extends HashMap<String, CustomerConnections> {
    public LastConnection get(String custId, String srvUrl) {
        return get(custId).get(srvUrl);
    }
}

好的,我已经了解到继承是有害的,那么让我们尝试组合:

class CustomerConnections {
    Map<String, LastConnection> customerConnections;
    LastConnection get(String srvUrl) { 
        return customerConnections.get(srvUrl);
    }
    ... // all other needed operations;
}

class AllConnections {
    Map<String, CustomerConnections> allConnections;
    public LastConnection get(String custId, String srvUrl) {
        return get(custId).get(srvUrl);
    }
    public CustomerConnection get(String custId) {
        return allConnections.get(custId);
    }
    ... // all other needed operations;
}

问题在于我不确定在尊重SOLID原则和所有最佳实践方面,哪种方法是最好的。创建除了扩展已经存在的集合之外什么都不做的类似乎是在增加不必要的实体,但会使我的代码更清晰(特别是当有下一级 - 比如按月份分类的所有连接的映射)。有任何建议吗?

6个回答

5
创建仅扩展已存在的集合的类似乘法计算不必要实体。我会更改为封装。您隐藏了存储信息的详细信息。您的类的客户端无需知道您如何为他们提供客户连接历史记录。这样做是个好主意,因为您可以更改基础模型,而无需使api的客户端更改其代码。
这很棒,也是做此操作的好理由。YourClass.getCustomerConnection(cId)比yourCollection.get(id)。get(id)。getConnection()更清晰。即使您是该人,您也需要使使用此代码的人们的生活更轻松。
很好,那么你正在计划并使你的代码可扩展。这是良好的OO实践。在我看来,您自己得出的结论就是我要做的事情。

1

我认为你还应该考虑集合的使用方式和数据获取方式:

  • 如果它们只是简单的结果集(例如要在页面上显示),那么使用标准集合似乎是合理的 - 然后您可以使用许多标准库来操作它们。
  • 另一方面,如果它们是可变的,并且需要持久化任何更改(例如),则将它们封装在自己的类中可能更可取(然后可以将更改写入数据库等)。

您如何获取和持久化数据?如果数据存储在数据库中,则可以使用SQL按客户端和/或服务和/或月份选择LastConnections,而不必自己维护数据结构(并仅返回连接列表或映射或连接计数)。或者也许您不想为每个请求运行查询,因此需要将整个数据结构保存在内存中。

封装通常是一件好事,特别是它可以帮助您遵守Demeter法则 - 也许您可以将要对集合执行的某些操作推回到AllConnections类(实际上是DAO)。这通常有助于单元测试。

此外,为什么在这里扩展HashMap被认为是邪恶的呢?考虑到你只想添加一个微不足道的辅助方法。在你的AllConnections代码中,AllConnections将始终像HashMap一样行事-它是多态可替换的。当然,你可能会将自己锁定在使用HashMap(而不是TreeMap等)上,但这可能并不重要,因为它具有与Map相同的公共方法。然而,你是否真的想这样做取决于你打算如何使用集合-我只是认为你不应该自动排除实现继承总是不好的(通常确实如此!)。
class AllConnections extends HashMap<String, CustomerConnections> {
    public LastConnection get(String custId, String srvUrl) {
        return get(custId).get(srvUrl);
    }
}

1

我会创建一个专用对象来存储这些信息。你正在创建的是一个管理器对象,而不是一个简单的集合。

不会让它派生自Map或其他众所周知的集合类,因为你存储这些信息的语义可能会在未来发生变化。

相反,实现一个将客户和其连接绑定在一起的类,在该类内部使用适当的集合类(你可以随意更改,而不影响接口和代码的其余部分)

你的客户/连接管理器类不仅仅是一个简单的容器。它可以存储元数据(例如,这种关系是何时建立的)。如果需要,它可以根据客户信息执行连接搜索。它可以处理重复项,而不是基础集合类如何处理等等。你可以轻松地插入调试/日志记录/性能监控,以便轻松了解发生了什么。


0
为什么要在对象之外维护对象之间的关系?
我建议采用以下方式:
public class Customer 
{
  public List<LastConnection> getConnectionHistory() 
  {
    ... 
  }

  public List<LastConnection> getConnectionHistory(Service service) 
  {
    ...
  }

  public List<LastConnection> getConnectionHistory(Date since) 
  {
    ...
  }
}

public class LastConnection
{
  public Date getConnectionTime() 
  {
    ...
  }

  public Service getService()
  {
    ...
  }
}

你可以将 List<Customer> 传递给使用这些信息的方法。

但是我的Customer类需要了解LastConnection类。我可能会在各个包之间产生大量的循环。再见,可重用性...假设我的代码中有四个包:Core、LastConnections、AddressBook和TransactionHistory(只是示例)。如果我让mypackage.core.Customer知道(并管理)LastConnections、AddressBook和TransactionHistory,那么就会造成混乱。我更希望LastConnection知道Customer,AddressBook知道Customer,以此类推,而不是相反。这样实现下一个功能时就不会改变现有的代码。 - Jakub
通过“packages”我假设你是指“jars”?在这种情况下,我会在使用这些关系的应用程序中定义域类(如上所述)。域类可以专门化或封装打包的类,我的倾向是封装,因为你可以隐藏对你的应用程序不相关的打包类的属性/特征。当涉及到“值对象”,但应用程序不应看到“值对象”公开的设置器时,我通常使用这种方法。另一个好处是,如果打包的类发生变化,你的应用程序是隔离的。 - Nick Holt

0
为什么不在简单的Map中使用custId+"#"+srvurl作为键呢?
或者使用一个包含两个ID并实现hashCode()和equals()的Tuple或Pair类作为键 - 这是最干净的面向对象解决方案。

使用custId+"#"+srvUrl作为键将无法快速获取所有给定客户的连接。但是感谢Tuple库的想法,我会研究一下它。 - Jakub
好的,那不是你原来问题描述的一部分;在这种情况下,Map of Maps 可能是最佳选择。 - Michael Borgwardt

-1
你可以创建一个实现了 Map<K,V> 接口的类,并在内部委托给一个容器 map:
class CustomerConnections implements Map<String,LastConnection> {
    private Map<String, LastConnection> customerConnections;

    @Override
    public LastConnection get(Object srvUrl) { 
        return customerConnections.get(srvUrl);
    }
    // all other needed operations;
}

这种方法的好处是你可以传递一个具有标准、明确定义的契约的Map,但避免了扩展库类,这通常是不受欢迎的。

编辑:如下所指出,这种方法的缺点是需要实现许多仅仅委托给底层集合的方法。


2
除了那个无辜的“所有其他必要操作”注释之外,还隐藏着大量样板委托。 - Michael Borgwardt
1
虽然您只需要执行一次此操作。DelegatingMap<K,V>实现了Map<K,V>,可用作所有此类扩展的基类。 - paulcm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接