我有一个应用程序,试图遵循清洁的架构,我需要做一些缓存无效,但我不知道这应该在哪一层做。
为了这个例子,假设我有一个OrderInteractor
,它有两个用例:GetOrderHistory()
和SendOrder(Order)
。
第一个用例使用OrderHistoryRepository
,第二个用例使用OrderSenderRepository
。这些存储库是具有多个实现的接口(MockOrderHistoryRepository
和InternetOrderHistoryRepository
)。OrderInteractor
仅通过接口与这些存储库交互,以便隐藏真正的实现。
mock
版本非常虚设,但是历史存储库的internet
版本将一些数据保存在缓存中,以便执行得更好。
现在,我想实现以下内容:当订单发送成功时,我想使历史的缓存无效,但我不知道应该在哪里执行实际的缓存无效操作。
我的第一个猜测是向OrderHistoryRepository
添加一个InvalidateCache()
,并在交互器内的SendOrder()
方法的末尾使用此方法。在InternetOrderHistoryRepository
中,我只需实现缓存无效,就可以了。但是我将被迫在MockOrderHistoryRepository
中实际实现该方法,这样就向外部暴露了这样一个事实,即某些缓存管理是由存储库执行的。我认为OrderInteractor
不应该知道此缓存管理,因为它是OrderHistoryRepository
的Internet
版本的实现细节。
我的第二个猜测是,当InternetOrdersenderRepository
知道订单已成功发送时,它将在InternetOrderHistoryRepository
中执行缓存无效操作,但它将强制该存储库知道InternetOrderHistoryRepository
,以便获取此回购用于缓存管理的缓存密钥。并且我不希望我的OrdersenderRepository
与OrderHistoryRepository
有依赖关系。
最后,我的第三个猜测是有某种CacheInvalidator
(无论名称如何)接口,当存储库被模仿时使用Dummy
实现,当Interactor
使用Internet
存储库时使用Real
实现。此CacheInvalidator
将被注入到Interactor
,所选实现将由构建存储库和CacheInvalidator
的工厂
提供。这意味着我将有一个MockedOrderHistoryRepositoryFactory
-用于构建MockedOrderHistoryRepository
和DummyCacheInvalidator
-以及一个InternetOrderHistoryRepository
-用于构建InternetOrderHistoryRepository
和RealCacheInvalidator
。但是在这里,我也不知道这个CacheInvalidator
应该由SendOrder()
末尾的interactor
使用,还是直接由InternetOrdersenderRepository
使用(尽管我认为后者更好,因为交互器可能也不知道隐藏着一些缓存管理)。
您更喜欢的架构方式是什么?
非常感谢。皮埃尔
您的第二个猜测是正确的,因为缓存是持久性机制的一个细节。例如。如果存储库是基于文件的,存储库缓存可能不是问题(例如本地ssd)。
交互者(用例)应该根本不知道缓存。这将使测试变得更容易,因为您不需要真正的缓存或mock来进行测试。
我的第二个猜测是,当InternetOrdersenderRepository
知道订单已成功发送时,它将在InternetOrderHistoryRepository
中执行缓存无效操作,但它将强制该存储库知道InternetOrderHistoryRepository
,以便获取此回购用于缓存管理的缓存密钥。
您的缓存密钥似乎是多个order属性的组合,因此您需要在某个地方封装缓存密钥创建逻辑以便重用。
在这种情况下,您有以下选项:
两个接口的一个实现
您可以创建一个实现InternetOrdersenderRepository
和InternetOrderHistoryRepository
接口的类。在这种情况下,您可以将缓存密钥生成逻辑提取到私有方法中并重用它。
使用实用工具类创建缓存密钥
简单地提取一个实用程序类中的缓存密钥创建逻辑,并在两个存储库中使用它。
创建缓存密钥类
缓存键只是一个任意对象,因为缓存只能检查键是否存在,这意味着使用每个对象都有的equals
方法。但是为了更好地保证类型安全,大多数缓存对键使用泛型类型,这样您就可以定义一个泛型类型。
因此,您可以将缓存,密钥,逻辑和验证放在自己的类中。这样做的好处是,您可以轻松地测试该逻辑。
public class OrderCacheKey {
private Integer orderId;
private int version;
public OrderCacheKey(Integer orderId, int version) {
this.orderId = Objects.requireNonNull(orderId);
if (version < 0) {
throw new IllegalArgumentException("version must be a positive integer");
}
this.version = version;
}
public OrderCacheKey(Order order) {
this(order.getId(), order.getVersion());
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
OrderCacheKey other = (OrderCacheKey) obj;
if (!Objects.equals(orderId, other.orderId))
return false;
return Objects.equals(version, other.version);
}
public int hashCode() {
int result = 1;
result = 31 * result + Objects.hashCode(orderId);
result = 31 * result + Objects.hashCode(version);
return result;
}
}
您可以使用此类作为缓存的键类型:Cache
。然后可以在两个存储库实现中使用OrderCacheKey
类。
引入订单缓存接口来隐藏缓存细节
您可以应用接口隔离原则,将完整的缓存细节隐藏在一个简单的接口后面。这将使您的单元测试更容易,因为您必须更少地模拟。
public interface OrderCache {
public void add(Order order);
public Order get(Integer orderId, int version);
public void remove(Order order);
public void removeByKey(Integer orderId, int version);
}
然后,您可以在两个存储库实现中使用OrderCache
,还可以将接口隔离与上面的缓存键类结合起来。
如何申请