问题主要是设计问题(与ddd有些相关)。抱歉这个人为的例子:
假设您有(域)类代表不同类型的水果:苹果,樱桃等。现在假设你必须实施一些压榨果汁的行为。呼叫者应该能够在不知道他有哪种特定水果的情况下调用挤压。
我应该把这种行为放在哪里?
当然,可以定义一个水果界面/基类功能
Fruit#squeeze()
让所有子类实现自己的行为。 现在调用者可以简单地执行以下操作:
Fruit f = new Cherry();
f.squeeze();
但是,如果挤压不是那么简单并且涉及更复杂的行为,例如调用不同的外部服务,那么应该做些什么,对于每个水果都是不同的,例如
AppleJuicerService#squeeze(Apple a)
和
CherryJuicerService#squeeze(Cherry c)
?从域类调用服务感觉不对。
我已经读过看起来不适合的双重调度模式,因为每个子类都需要不同的服务。
我的问题是:在这里可以做些什么来获得“干净”的设计?
修改
到目前为止,感谢您的所有答案。我会尝试澄清一下这个问题。我将尝试给出另一个,希望不那么做作的例子来说明我在这里要说的问题:
考虑一个Message基类,它允许将其内容显示为String。
interface Message {
String showContent();
}
现在假设我们有不同类型的消息,如EMailMessage:
class EMailMessage implements Message {
//some specific parameters for email
private EmailAddress recipientEmail;
public String showContent() {
//here the content would be converted to string
return "the content of an EMail"
}
}
另一种类型是SMSMessage:
class SMSMessage implement SMSMessage {
//some specific parameters for SMS
private TelNumber recepientTelephoneNumber;
public String showContent() {
//here the content would be converted to string
return "the content of a SMS"
}
}
此外,假设消息被建模为实体,因此可以保存在数据库中。虽然技术上相当,但假设使用像Spring这样的依赖注入框架来注入依赖项。
与fruit示例类似,请考虑我们必须实现send()行为,将Message发送给收件人。此外,假设发送电子邮件涉及与SMS不同的逻辑。现在,问题是:应该把发送消息的逻辑放在哪里?
通常我会选择创建一个发送短信的服务,例如封装,例如SMS服务提供商的API。此外,我创建了另一个服务来封装发送电子邮件。
interface SendMessageService<T extends Message> {
void send(T message);
}
class SendEmailService extends SendMessageService<EMailMessage> {
public void send(EMailMessage message) {
//send the EMail
}
}
class SendSMSService extends SendMessageService<SMSMessage> {
public void send(SMSMessage message) {
//send the SMS
}
}
这种方法的缺点是你不能在没有确定其具体子类的情况下发送消息,即不能直接使用以下内容
List<Message> messages = //Messages of different types
SendMessageService service = //???
for (Message m : messages) {
service.send(m);
}
当然,可以根据特定的消息类型创建一个用于创建服务的工厂。但这有点意味着克隆Message的继承层次结构。有没有更好的方法来达到预期的效果?或者我错过了什么?或者以某种方式将服务注入实体会更好吗?
答案 0 :(得分:1)
您可以将工作委托给SqueezeBehavior
界面,并让每个实现定义squeeze
Fruit
或特定Fruit
的方式。这是一个原始想法(这意味着它可以改进,但作为第一步是好的):
interface SqueezeBehavior<T> {
void squeeze(T squeezeMe);
}
interface FruitSqueezeBehavior<T extends Fruit> extends SqueezeBehavior<T> {
}
class FruitSqueezer implements FruitSqueezeBehavior<Fruit> {
public void squeeze(Fruit fruit) {
System.out.println("squizing any fruit");
}
}
class AppleSqueezer implements FruitSqueezeBehavior<Apple> {
public void squeeze(Apple apple) {
System.out.println("squizing apple");
}
}
class CherrySqueezer implements FruitSqueezeBehavior<Cherry> {
public void squeeze(Cherry cherry) {
System.out.println("squizing cherry");
}
}
class FruitService {
public void foo(Fruit fruit) {
FruitSqueezeBehavior fruitSqueezer = ...
fruitSqueezer.squeeze(fruit);
}
}
答案 1 :(得分:0)
有一个定义标准行为的基类Fruit
。当您必须使用更复杂的实现时,您可以覆盖适当的方法。
class Fruit {
public void Squeeze(){
// Standard squeeze behaviour
}
}
class Apple extends Fruit {
@Override
public void Squeeze(){
// Complex squeeze behaviour
}
}
class Cherry extends Fruit {
// Nothing special, cherries are easy to squeeze
}
如果必须为特定类型定义特定实现,则始终必须在某处定义行为。如果这对于一种方法来说太多了,那么你可以调用一个更详细的类来为你做这件事。
您可以与工厂合作并执行此类操作
class FruitManipulator {
void Squeeze(Fruit f){
// Switch over fruit, create new service depending on the type
}
}
interface JuiceService<T extends Fruit> {
void Squeeze(T f);
}
class AppleJuiceService implements JuiceService<Apple> {
void Squeeze(Apple apple){
// Do your thing
}
}
并像这样使用它:
FruitManipulator service = new FruitManipulator();
service.Squeeze(new Apple());
您可能希望找到一个更好的示例:Squeeze()
类比并不容易。也许扩展一下挤压实际意味着什么?
答案 2 :(得分:0)
您可以考虑使用DomainEvents。这有助于您将域模型与外部服务分离(通常需要注入无状态bean)
interface Fruit {
void squeeze();
}
class Apple implements Fruit {
@Override
public void squeeze(){
// domain rules validations
DomainEvents.raise(new AppleSequeezedEvent(this));
}
}
class Cherry extends Fruit {
@Override
public void squeeze(){
// domain rules validations
DomainEvents.raise(new CherrySequeezedEvent(this));
}
}
class Banana extends Fruit {
@Override
public void squeeze(){
// domain rules validations
// hmm...No one cares banana...
}
}
class DomainEvents {
private static List<DomainEventHandler> handlers = new ArrayList<DomainEventHandler>();
public static void register(DomainEventHandler handler) {
this.handler.add(handler);
}
public static void raise(DomainEvent event) {
for (DomainEventHander handler: handlers) {
if (handler.subscribe(event.getClass()) {
handler.handle(event);
}
}
}
}
现在,当你测试apple时,你可以注册一些处理程序mock / stub:
@Test
public void tellsAppleIsSqueezed() throws Throwable {
DomainEventHandler stub = new FruitSqueezedEventHandlerStub(Apple.class);
DomainEvents.register(stub );
Apple apple = new Apple();
apple.squeeze();
//assert state change of apple if any before you publishing the event
assertThat(stub.getSqueezed(), sameInstance(apple));
}
您可以在自己的单元测试用例中测试真实的处理程序。
但我认为这个解决方案会增加额外的复杂性。