事件源-复杂聚合设计

bgibtngc  于 2021-07-08  发布在  Java
关注(0)|答案(2)|浏览(521)

我有下面的代码规范示例,是在photoshop中建模的图像。
图像带有 PhotoshopImage . 每个图像都有 Layers 它是一个对象,包含了一个图像所包含的所有层,在我的例子中,它只包含两层-第一层是实体层(示例 DefaultLayer )第二个是透明层(例如 NotifiableLayer ). 无论何时 DefaultLayer 是更新,我们也要更新 NotifiableLayer 那就是倾听变化 DefaultLayer (即在下面),这样它就可以自我更新(比如当你更新下面一层上的一些黑色像素时,那么下面一层上的透明度为50%的透明层将以灰色显示该像素)。
具体实施如下:

public class ES2 {
    public static void main(String[] args) {
        PhotoshopImage image = new PhotoshopImage();

        //draw ine black pixel at position 1,1 in layer 1 (top transparent layer)
        DrawOneBlackPixelCommand command1 = new DrawOneBlackPixelCommand(1,1,new Coordinates(1,1));
        image.drawOneBlackPixel(command1);

        //draw one black pixel at position 0,0 in layer 0 (bottom solid layer)
        //this command will also affect transparent layer 1 via callback
        DrawOneBlackPixelCommand command2 = new DrawOneBlackPixelCommand(1,0,new Coordinates(0,0));
        image.drawOneBlackPixel(command2);

        int[][] imagePixels = image.getImagePixels();

        //[2, 0]
        //[0, 1]
        System.out.println(Arrays.toString(imagePixels[0]));
        System.out.println(Arrays.toString(imagePixels[1]));
    }
}

record DrawOneBlackPixelCommand(
    int imageId,
    int layerType,
    Coordinates pixelCoordinates
){}
record Coordinates(int x, int y){}

class PhotoshopImage{
    Integer imageId = 1;
    String imageName = "someName";
    LocalDateTime dateTime = LocalDateTime.now();
    Layers layers;

    PhotoshopImage(){
        layers = new Layers();
    }

    void drawOneBlackPixel(DrawOneBlackPixelCommand command){
        if(LocalDateTime.now().isBefore(dateTime)){
            throw new DrawingPixelTimeExpiredException();
        }
        layers.drawOneBlackPixel(command.layerType(), command.pixelCoordinates());
    }

    int[][] getImagePixels(){
        return layers.getVisibleLayerPixels();
    }

    class DrawingPixelTimeExpiredException extends RuntimeException{}
}

class Layers{
    Set<NotifiableLayer> notifiableLayerObservers = new HashSet<>();
    NavigableMap<Integer, Layer> layers = new TreeMap<>();

    Layers(){
        DefaultLayer solid = new DefaultLayer();
        NotifiableLayer transparent = new NotifiableLayer();
        layers.put(0, solid);
        layers.put(1, transparent);
        notifiableLayerObservers.add(transparent);
    }

    void drawOneBlackPixel(int layerType, Coordinates pixelCoordinates){
        if(!layers.containsKey(layerType)){
            throw new LayerDoesNotExistException();
        }
        Layer change = layers.get(layerType);
        change.drawOneBlackPixel(pixelCoordinates);
        notifiableLayerObservers.forEach(l -> l.notifyLayer(change, pixelCoordinates));
    }

    public int[][] getVisibleLayerPixels() {
        return layers.lastEntry().getValue().getLayerPixels();
    }

    class LayerDoesNotExistException extends RuntimeException{}
}

interface Layer{
    void drawOneBlackPixel(Coordinates coordinates);
    int[][] getLayerPixels();
}

class DefaultLayer implements Layer{
    int[][] pixels = new int[][]{{0,0},{0,0}};

    @Override
    public void drawOneBlackPixel(Coordinates c) {
        pixels[c.x()][c.y()] = 1;
    }

    @Override
    public int[][] getLayerPixels() {
        return pixels;
    }
}

class NotifiableLayer implements Layer{
    int[][] pixels = new int[][]{{0,0},{0,0}};

    void notifyLayer(Layer changed, Coordinates c){
        //if it is not this layer, then it is layer below (solid layer)
        if(changed!=this){
            int pixelInLayerBelow = changed.getLayerPixels()[c.x()][c.y()];
            syncPixelWithLayerBelow(pixelInLayerBelow, c);
        }
    }

    private void syncPixelWithLayerBelow(int pixelBelow, Coordinates c){
        pixels[c.x()][c.y()] = pixelBelow + 1;
    }

    @Override
    public void drawOneBlackPixel(Coordinates c) {
        pixels[c.x()][c.y()] = 1;
    }

    @Override
    public int[][] getLayerPixels() {
        return pixels;
    }
}

现在,它被实现为可变状态对象(也就是说,它不使用事件源)。无论我读过什么关于事件源的手册,它都只是基于一些超级简单的例子。
在我的情况下-我不知道如何创建事件 OneBlackPixelDrawnEvent (一种方法是在下面更新的答案中,但是对于es带来的好处来说它看起来太复杂了)-这应该是代码中这两个操作的结果,以及如何应用这些事件-应该在 PhotoshopImage ,还是每个层都应该负责更新其部分状态?如何将这些事件从 PhotoshopImage 聚合到 Layers 再往下走?
更新-使用事件源实现的一种方法的示例

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

public class ES2 {
    public static void main(String[] args) {
        PhotoshopImage image = new PhotoshopImage();

        //draw ine black pixel at position 1,1 in layer 1 (top transparent layer)
        DrawOneBlackPixelCommand command1 = new DrawOneBlackPixelCommand(1,1,new Coordinates(1,1));
        List<Event> events1 = image.drawOneBlackPixel(command1);

        //[OneBlackPixelDrawnEvent[layerType=1, pixelCoordinates=Coordinates[x=1, y=1], pixelValue=1]]
        System.out.println(events1);

        //draw one black pixel at position 0,0 in layer 0 (bottom solid layer)
        //this command will also affect transparent layer 1 via callback
        DrawOneBlackPixelCommand command2 = new DrawOneBlackPixelCommand(1,0,new Coordinates(0,0));
        List<Event> events2 = image.drawOneBlackPixel(command2);

        //[OneBlackPixelDrawnEvent[layerType=0, pixelCoordinates=Coordinates[x=0, y=0], pixelValue=1], LayerSyncedEvent[layerType=1, pixelCoordinates=Coordinates[x=0, y=0], pixelValue=2]]
        System.out.println(events2);

        int[][] imagePixels = image.getImagePixels();

        //[2, 0]
        //[0, 1]
        System.out.println(Arrays.toString(imagePixels[0]));
        System.out.println(Arrays.toString(imagePixels[1]));
    }
}

interface Event{}
record DrawOneBlackPixelCommand(
    int imageId,
    int layerType,
    Coordinates pixelCoordinates
){}
record Coordinates(int x, int y){}

record OneBlackPixelDrawnEvent(
        Integer layerType,
        Coordinates pixelCoordinates,
        Integer pixelValue
) implements Event{}

class PhotoshopImage{
    Integer imageId = 1;
    String imageName = "someName";
    LocalDateTime dateTime = LocalDateTime.now();
    Layers layers;

    PhotoshopImage(){
        layers = new Layers();
    }

    List<Event> drawOneBlackPixel(DrawOneBlackPixelCommand command){
        if(LocalDateTime.now().isBefore(dateTime)){
            throw new DrawingPixelTimeExpiredException();
        }
        List<Event> events = layers.drawOneBlackPixel(command.layerType(), command.pixelCoordinates());
        apply(events);  //Only here we can update state of this aggregate, so it is not updated twice
        return events;
    }

    void apply(List<Event> events){
        layers.apply(events);
    }

    int[][] getImagePixels(){
        return layers.getVisibleLayerPixels();
    }

    class DrawingPixelTimeExpiredException extends RuntimeException{}
}

class Layers{
    Map<Integer, NotifiableLayer> notifiableLayerObservers = new HashMap<>();
    NavigableMap<Integer, Layer> layers = new TreeMap<>();

    Layers(){
        DefaultLayer solid = new DefaultLayer();
        NotifiableLayer transparent = new NotifiableLayer();
        layers.put(0, solid);
        layers.put(1, transparent);
        notifiableLayerObservers.put(1, transparent);
    }

    List<Event> drawOneBlackPixel(int layerType, Coordinates pixelCoordinates){
        if(!layers.containsKey(layerType)){
            throw new LayerDoesNotExistException();
        }
        Layer change = layers.get(layerType);
        OneBlackPixelDrawnEvent event = change.drawOneBlackPixel(pixelCoordinates);
        //Here, I have to add layerType, since it is a missing info on event!
        OneBlackPixelDrawnEvent updatedEvent = new OneBlackPixelDrawnEvent(layerType, event.pixelCoordinates(), event.pixelValue());
        List<LayerSyncedEvent> syncedEvents = notifiableLayerObservers.entrySet().stream()
                .map(en ->
                    en.getValue()
                            .notifyLayer(change, updatedEvent)
                            //Here we have to re-pack event, since it is missing some info that can be
                            //filled only on this level
                            .map(e -> new LayerSyncedEvent(en.getKey(), e.pixelCoordinates(), e.pixelValue()))
                )
                .flatMap(Optional::stream)
                .collect(Collectors.toList());
        List<Event> results = new ArrayList<>();
        results.add(updatedEvent);
        results.addAll(syncedEvents);
        //apply(results); we still cannot apply here, since applying in aggregate root would apply twice!
        return results;
    }

    public void apply(List<Event> events){
        for(Event e : events){
            if(e instanceof LayerSyncedEvent ev){
                layers.get(ev.layerType()).apply(ev);
            }
            if(e instanceof OneBlackPixelDrawnEvent ev){
                layers.get(ev.layerType()).apply(ev);
            }
        }
    }

    public int[][] getVisibleLayerPixels() {
        return layers.lastEntry().getValue().getLayerPixels();
    }

    class LayerDoesNotExistException extends RuntimeException{}
}

interface Layer{
    OneBlackPixelDrawnEvent drawOneBlackPixel(Coordinates coordinates);
    int[][] getLayerPixels();
    <T extends Event> void apply(T e);
}

class DefaultLayer implements Layer{
    int[][] pixels = new int[][]{{0,0},{0,0}};

    @Override
    public OneBlackPixelDrawnEvent drawOneBlackPixel(Coordinates c) {
        OneBlackPixelDrawnEvent event = new OneBlackPixelDrawnEvent(null, c, 1);
        //apply(event); ! Since applying in aggregate root - cannot apply here!
        return event;
    }

    @Override
    public int[][] getLayerPixels() {
        return pixels;
    }

    @Override
    public <T extends Event> void apply(T e) {
        if(e instanceof OneBlackPixelDrawnEvent ev){
            Coordinates c = ev.pixelCoordinates();
            pixels[c.x()][c.y()] = ev.pixelValue();
        }
    }
}

record LayerSyncedEvent(
        Integer layerType,
        Coordinates pixelCoordinates,
        Integer pixelValue
) implements Event{}

class NotifiableLayer implements Layer{
    int[][] pixels = new int[][]{{0,0},{0,0}};

    Optional<LayerSyncedEvent> notifyLayer(Layer changed, OneBlackPixelDrawnEvent event){
        //if it is not this layer, then it is layer below (solid layer)
        if(changed!=this){
            Coordinates c = event.pixelCoordinates();
            //Since layer is not updated anymore in-place, we have to take changes from event!
            //int pixelInLayerBelow = changed.getLayerPixels()[c.x()][c.y()];
            int pixelInLayerBelow = event.pixelValue();
            return Optional.of(syncPixelWithLayerBelow(pixelInLayerBelow, c));
        }
        return Optional.empty();
    }

    private LayerSyncedEvent syncPixelWithLayerBelow(int pixelBelow, Coordinates c){
        LayerSyncedEvent event = new LayerSyncedEvent(null, c, pixelBelow + 1);
        //apply(event); ! Since applying in aggregate root - cannot apply here!
        return event;
    }

    @Override
    public OneBlackPixelDrawnEvent drawOneBlackPixel(Coordinates c) {
        OneBlackPixelDrawnEvent event = new OneBlackPixelDrawnEvent(null, c, 1);
        //apply(event); ! Since applying in aggregate root - cannot apply here!
        return event;
    }

    @Override
    public int[][] getLayerPixels() {
        return pixels;
    }

    @Override
    public <T extends Event> void apply(T e) {
        if(e instanceof LayerSyncedEvent ev){
            Coordinates c = ev.pixelCoordinates();
            pixels[c.x()][c.y()] = ev.pixelValue();
        }
        if(e instanceof OneBlackPixelDrawnEvent ev){
            Coordinates c = ev.pixelCoordinates();
            pixels[c.x()][c.y()] = ev.pixelValue();
        }
    }
}

我刚刚用一种实现聚合根的方法更新了这个示例,方法返回事件。我想这是一个可能的实现-但看看现在有多复杂;即使是这个简单的例子,复杂性也增加了2倍。是我做错了什么,还是这在事件源系统中不是那么容易做到?

9rygscc1

9rygscc11#

尽管有争议,但我还是会争论“photoshopping”是否是一个你想用ddd、cqrs和事件源这样的范例来实现的领域。在某种程度上,VoicounReason提到,有时工作并没有带来好处;您可能只是选择了这样一个不可行的域。
不管怎样,让我试着给你的问题和你分享的片段一些指导。我想强调的第一件事是归还 List<Event> 命令处理程序中的对象。虽然在国内开发的ddd/cqrs/es系统中是合理的,但这并不是使用基于axon框架的应用程序(我假设您是通过 axon 标签)。
命令处理程序应该只共享操作是成功的、不成功的还是新创建实体的标识符。就这样。
另一个值得分享的指针是命令处理程序的位置。您当前已将其设计为从 PhotoshopImage . 但是,命令完全可以针对聚合中的确切实体。从定义的立场来看,这也很好,因为:
聚合是一组与数据更改相关联的对象,它们作为单个单元。有一个对聚合的引用,称为聚合根。最后,一致性规则适用于聚合的边界。
因此,整个聚合(在您的示例中)由 PhotoshopImage 以及一份 Layer 实体。这个 PhotoshopImage 是本例中的聚合根。以“single reference”参数为例,这意味着命令将始终流经聚合根,因此是 PhotoshopImage . 然而,这并不意味着 PhotoshopImage 负责决定处理命令的对象。
从实现的外观来看,如果我正确地遵循您的描述,则有必要在根目录中处理操作,以便将操作委派给所有层。这确实会选择命令处理程序,因为它现在就位于这里。
它是在事件的出版上,在那里你可以大大简化事情。请注意,虽然我是基于轴突框架在本例中,我认为这是公平的,因为 axon 正在使用标签。现在,是 PhotoshopImage 发布事件的。我会让你的每一层都发布它自己的 OneBlackPixelDrawnEvent 相反。当您将使用事件源时,在聚合边界内发布和处理此类事件将优先于进一步执行命令处理操作。
因此,没有必要援引任何东西 notifiableLayerObservers 以正确通知所有层。这应该只是您正在使用的cqrs/ddd/es框架的一部分,因此axon框架可以为您提供现成的帮助。只需将方法标记为 @EventSourcingHandler axon框架不会调用给定事件的所有事件源处理程序,不管它们是驻留在聚合根中还是任何实体中。按照该路线,您可以在每个实体处理(在您的场景中)时调整状态的正确部分 OneBlackPixelDrawnEvent .
如前所述,我假设您在本例中使用的是类似axon的框架。或者,您有正确的实现分层来实现相同的功能。有了这样的设置,您就可以轻松处理当前在命令处理函数中执行的所有自定义路由信息。
最后一点,我在假设一个我不熟悉的领域。如果在使用上述方法时遇到任何问题,请务必发表评论,以便我们进一步讨论。同时,我希望这能帮到你!

stszievb

stszievb2#

事件源基于这样的假设:系统记录发生在聚合根上的事件。在您的情况下,更新层时,包含该层的图像将向内部集合附加事件。像这样的 LayerUpdated ... 尽管给事件起一个有意义的名字被认为是一种很好的做法。
当所有的操作 Commands )一旦执行,系统将开始持久化这些事件,并且对于每一个事件,它还将广播一个通知。
现在你可以各有各的 NotifiableLayer 侦听特定的通知,或者您可以让一个单独的服务来执行此操作并更新所有 NotifiableLayer 相应的示例。我会选择这项服务:我不太喜欢域实体监听通知的想法。

相关问题