0

I'm designing a music information system. I have a couple of entities that are connected to each other.
Below is part of the domain code.

class Album {
    
    private Set<Track> tracks;
    private boolean published;
    
    public Set<Track> getTracks() {
        return this.tracks;
    }
    public boolean isPublished() {
        return this.published;
    }

    public void publish() {
        System.out.println("Album.publish() called");
        this.published = true;
        this.tracks.forEach(track -> track.publish());
    }

}

class Track {

    private boolean published;
    private Album album;

    public boolean isPublished() {
        return this.published;
    }
    public Album getAlbum() {
        return this.album;
    }

    public void publish() {
        // if track is single (this.album == null), set published to true
        // if track is part of an album and the album is NOT published, return;
        // if track is part of an album and the album is published, set published to true
        if(this.album != null && !this.album.isPublished())
            return;
        this.published = true;
    }

}

Track is an independent entity. It can be a single track (I.e. without an Album). So the album attribute is actually needed.
One domain rule is that when an album is archived (i.e. not published), its tracks cannot be published neither and if an album is published, any of its tracks can either be published or archived.
The problem is that when an album is published (e.g. album1.publish()), its tracks' publish() method is called as well. But track1.publish() checks if the album is published based on the copy it already has (which is not published).
How can I solve the problem?

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Amirhosein Al
  • 470
  • 6
  • 18
  • If `Album` is an *entity*, there shouldn't be "copies"—an entity has a unique identity, and all references should point to the same object. – erickson Aug 06 '21 at 18:07
  • Correct. How should I implement it in Java? As far as I know Java is always pass by value. @erickson – Amirhosein Al Aug 06 '21 at 18:09
  • https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value good read on pass-by-value vs. pass-by-ref – Taylor Aug 06 '21 at 18:11
  • Yes, pass a reference to the parent `Album` to the `Track` (by value, as all references are passed in Java). One way to enforce data integrity is to remove any API that directly sets `Album` for `Track`, offering instead only an `addTrack(Track)` method on `Album` which updates the collection of tracks *and* sets the `Album` via package-private access. – erickson Aug 06 '21 at 18:12

1 Answers1

0

If you split domain model entities by behaviour, you can get rid of described limitations

Let's have some interfaces for such entities:

interface AlbumId{
   String asString();
   AlbumId Absent = () -> "NO ALBUM AT ALL";
}

interface Publication{
   void publish() throws Exception;
   void archive() throws Exception;
   boolean published();
}

interface Track{
    TrackId id(); 
    AlbumId albumId(); //smart type (as DDD suggest), therefore, no more nulls
}

Now you may enforce rules by creating class that will get you a list of tracks you can publish:

public class TracksReadyToPublishOf implements Supplier<Map<TrackId, TrackPublication>>{ 
    //this class may access to cache and have dosens of other optimizations
    public TracksReadyToPublishOf(AlbumId id){...}
    @Override public get(){...} 
}

Then you can reuse your code to check your rules anywhere:

public class TrackPublication implements Publication {
    private final Track track;
    private final Supplier<Map<TrackId, TrackPublication>> allowedTracks;

    //easy for unit testing
    public SmartTrackPublication(Track track, Supplier<Map<TrackId, TrackPublication>> allowedTracks){
        this.track = track;
        this.allowedTracks = allowedTracks;
    }

    public SmartTrackPublication(Track track){
        this(track, new TracksReadyToPublishOf(track.albumId());
    }

    @Override
    public publish() throws AlbumArchivedException{
        if(this.albumId != AlbumId.Absent){
            if(!this.allowedTracks.get().containsKey(this.track.id())){
                throw new AlbumArchivedException();
            }
        }
        this.allowedTracks.get().get(this.id()).publish();
    }

}

And for album publishing:

public class AlbumPublication implements Publication{

   private final AlbumId id;
   private final Producer<Map<TrackId, TrackPublication>> tracks
   
   private AlbumWithTracks(AlbumId id, Producer<Map<TrackId, TrackPublication>> tracks){
       this.id = id;
       this.tracks = tracks;
   }

   public AlbumWithTracks(AlbumId id){
       this(id, new TracksReadyToPublishOf(id))
   }
   ...
   @Override publish() throws Exception{
      //code for publishing album
         
      for(TrackPublication t : Arrays.asList(
          this.tracks.get() 
      )){
          t.publish(); //track can publish anyway if it presents in list above
      }
   }
}
rocket-3
  • 26
  • 4