从相同基类的域类调用不同的服务

时间:2013-09-25 20:50:06

标签: java oop domain-driven-design

问题主要是设计问题(与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的继承层次结构。有没有更好的方法来达到预期的效果?或者我错过了什么?或者以某种方式将服务注入实体会更好吗?

3 个答案:

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

您可以在自己的单元测试用例中测试真实的处理程序。

但我认为这个解决方案会增加额外的复杂性。