skip to Main Content

I have following code specification sample that is modelling image inside Photoshop.

Image is given with PhotoshopImage. Every image has Layers which is an object that holds all layers that an image is made of and in my case it contains only two layers – first is solid layer (instance of DefaultLayer) and second one is transparent layer (instance of NotifiableLayer). Whenever DefaultLayer is updated, we have also to update NotifiableLayer that is listening to changes on DefaultLayer (that is below), so that it can update itself (like when you update some black pixel on layer below, then transparent layer with 50% opacity that is on top of that lower layer will show that pixel in gray color).

Implementation of this is given as:

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;
    }
}

Now, this is implemented as mutable state objects (that is – it is not using event sourcing). Whatever manual about event sourcing that I read, it is based only on some super-simple examples.

In my case – I do not know how to create events OneBlackPixelDrawnEvent (one way is in updated answer below, but it looks too complex for the benefits ES brings) – that should be result of these 2 operations in the code, and how to apply those events – should it be applied in PhotoshopImage, or should each layer be in charge of updating part of its state? How to forward those events from PhotoshopImage aggregate to Layers and further down?

UPDATE – Example of one way to implement using event sourcing

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();
        }
    }
}

I just updated example here with one way to implement Aggregate root, with methods that return events. I guess this is one possible implementation – but look how much complex this is now; even this simple example – increased 2x in complexity. Am I doing something wrong, or is this just not that easy to do in event-sourced system?

2

Answers


  1. Event Sourcing is based on the assumption that the system records events happening on Aggregate roots. In your case, when a Layer is updated, the Image containing it will append an event to an internal collection. Something like LayerUpdated… although it’s considered good practise to give meaningful names to events.

    When all the operations (aka Commands) have been executed, the system starts persisting those events and for each one, it will also broadcast a notification.

    Now you could either have each NotifiableLayer listening to specific notifications, or you can have a separate service who can do that and update all the NotifiableLayer instances accordingly. I would go for the service: I don’t really like the idea of Domain Entities listening to notifications.

    Login or Signup to reply.
  2. Although debatable, I’d argue whether "photoshopping" is a domain you’d want to implement with paradigms like DDD, CQRS and Event Sourcing in mind. As to some extend mentioned by VoicOfUnreason, sometimes the work doesn’t out way the benefits; you might just have picked such a domain for which it isn’t feasible.

    Regardless, let me try to give some guidance to your questions and the snippets you’ve shared. First thing I’d like to emphasize on, is returning the List<Event> objects from your command handlers. Although reasonable in home grown DDD/CQRS/ES systems, this is not something you’d do with an Axon Framework based application (which I assume you are using through the axon tag).

    Command handlers should only share whether an operation was successful, unsuccessful, or the identifier of newly created entities. That’s it.

    Another pointer worth sharing, is the placement of the Command Handler. You have currently designed it to start at the PhotoshopImage. However commands can perfectly well be targeted towards an exact entity within the Aggregate. From a definition stance this is also fine, as:

    The Aggregate is a group of associated objects which in regard to data changes act as a single unit. There is a single reference to the Aggregate, called the Aggregate Root. Lastly, consistency rules apply within the boundaries of the Aggregate.

    So, the entire Aggregate consists (in your sample) out of a PhotoshopImage and a list of Layer entities. The PhotoshopImage is your Aggregate Root in this case. Taking the "single reference" argument, this means commands would always flow through the Aggregate Root, which thus is the PhotoshopImage. This however doesn’t make the PhotoshopImage entity the object in charge of deciding upon handling the command.

    From the looks of the implementation, and if I follow your description correctly, there is a necessity to handle the operation in the root to delegate the operations to all layers. This would indeed opt for the command handler as it is situated right now.

    It is upon the publication of events where you can greatly simplify things. Note though that I am basing myself on Axon Framework in this case, which I am assuming is fair since the axon tag is being used. Right now, it’s the PhotoshopImage which publishes the events. I would have each of your Layers publish it’s own OneBlackPixelDrawnEvent instead. When you would be using Event Sourcing, the publication and handling of such an event within the boundary of the aggregate will have precedence over furthering the command handling operation.

    Thus, there would be no need whatsoever to invoke notifiableLayerObservers in your sample to correctly notify all Layers. This should simply be part of the CQRS/DDD/ES framework you are using, which Axon Framework would thus do for you out of the box. Simply mark a method as an @EventSourcingHandler and Axon Framework will not to invoke all event sourcing handlers for a given event, regardless of whether they reside in the Aggregate Root or any of the entities.
    Following that route, you can adjust the right portions of state per entity when they are handling the (in your scenario) OneBlackPixelDrawnEvent.

    As stated earlier, I am assuming you are using a framework like Axon in this case. Or otherwise that you have the correctly implementation layering in place to achieve the same. Having such a set up will allow you to get away with all the custom routing information you are currently doing in your Command Handling functions.

    Last note, I am making assumption on a Domain I am not familiar with. If anything this hurts when using the above mentioned approach, make sure to place a comment so we can discuss it further. In the meantime, I hope this helps you out!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search