'How does the separation between "domain objects" and "services" work in light of SOLID & other OOP principles?

I've been trying for some time to use the coding/design principles shown in the seemingly rather insightful, but for a time quite hard to decipher, post here:

How should a model be structured in MVC?

by tereško (it is/was hard to decipher because it seems to be heavily oriented toward a Web/Internet programming audience, and that is not a field with which I had much familiarity - most of my experience centers on desktop and mobile devices i.e. "the whole program runs on one device" paradigm). And one thing that has now come to bug me is the relationship between the Domain Objects, as he describes them, and the Services, also as he describes. I've later found that this appears to be taking ideas from a broader area called Domain-Driven Design (DDD), and while there seem to be a fair bit of resources on this topic, a lot of it is expensive books I cannot afford to pay for, so I have to try to wrack myself with more limited internet-based articles to get a feel for what all and everything that is being talked about. I'm not necessarily interested in perusing a full and rigorous "DDD" setup, but rather just the broad strokes of the pattern that tereško outlines, in a consistent way particularly in light of other "good code" principles like SOLID.

And that's the one that in particular I seem to have run into a bit of difficulty with in the following case. Suppose you have an app that functions as a diary (I've made such, but I'm actually thinking of something else here). As I get it from the above, an object called "Diary" might live in the "Domain Objects" portion of the program and do and represent just what you think it would. Likewise, in the UI layer, where we may have the "View" and "Controller" arms of MVC, or maybe "View" and "ViewModel" if we're using MVVM or whatever else is appropriate to the MV* flavor we want. The trick though is particularly the whole "interface" business suggested by SOLID - in fact, 4 of the 5 letters, all the ones after the "S"! This part seems clear enough (using a Java-style code):

Consider the "New Diary" command on the user interface. This button will send an event to an MVC Controller, which then must use a Model Service interface, presumably like this to satisfy ISP of SOLID (don't have available for calling more than you need):

public interface IDiaryCreatorService {
    void createDiary(String name);
}

Model Layer contains an implementation. This is where the trick is. Presumably, the implementor is going to call a Factory of some kind that will produce new Diary domain objects, and will be dependency-injected into the Controller:

// in Model
public class DiaryCreatorService implements IDiaryCreatorService {
     @Inject private IDiaryFactory mDiaryFactory;
     @Inject private IDiaryRetainer mDiaryRetainer; // implemented in Data layer

     public void createDiary(String name) {
          Diary diary = mDiaryFactory.create(name); // concrete Diary is exposed to service!
          mDiaryRetainer.retain(diary);
     }
}

Note the commented bit. Does this seemingly very reasonable design - note also that while I imagine that IDiaryRetainer is to be implemented as one head of a DiaryRepository down in the Data Layer, I have made DiaryCreatorService not depend on a full IDiaryRepository interface, again, because of the "I" in SOLID as applied to the caller, as this caller dose not need to know of, say, any Retrieve method it may have - still end up as "bad design" because in some sense "DiaryCreatorService" depends on, since it "sees", the concrete class Diary which is the Domain Object? Yet it seems there's no other way to make this work, because that has to get - in the "job description" of a Service as a "higher-level Domain Object", from the factory into the database or whatever backing collection the rest of the program draws on to do further operations!

Now I can figure some ways around this. One would be to simply @Inject a different interface that replaces the Factory and which also can receive a suitable receptacle. This object actually directly calls the construction on the Diary, as in a Factory, and then pushes it to the receptacle, but that also seems now kind of ugly because it feels kind of SRP-problematic.

The other way is to have the Factory interface not return a concrete class, but instead only return some kind of opaque ADiaryStub or something that Data Layer then receives through IDiaryRetainer and then downcasts as need be, but that again, seems kinda funny because of the downcast which doesn't seem like it should be necessary here.

What am I missing? How strict should one interpret or not the "knowledge hiding" aspects of the OO principles for these kind of situations?



Solution 1:[1]

What am I missing?

Possibly a couple things.

In Java, generics give you some extra flexibility for preserving decoupling and type safety. So you can write something like:

public class DiaryCreatorService<D extends IDiary> implements IDiaryCreatorService {
     private IDiaryFactory<D> mDiaryFactory;
     private IDiaryRetainer<D> mDiaryRetainer;

     public void createDiary(String name) {
          D diary = mDiaryFactory.create(name); // Only the interface is exposed!
          mDiaryRetainer.retain(diary);
     }
}

Here, the DiaryCreatorService doesn't care what kind of Diary you get, so long as both the factory and the retainer use using the same kind of diary.

(You may need to be careful with your dependency injection framework - type erasure may permit the framework to inject collaborators that aren't actually compatible.)

D here is just an identifier, so you can alternative spellings if you think it improves the design

public class DiaryCreatorService<Diary extends IDiary> implements IDiaryCreatorService {
     public void createDiary(String name) {
          Diary diary = mDiaryFactory.create(name); 
          ...
     }
...
}

What this means is that you can, if you like, define an interface for your domain model, and then any controller that consumes the interface can communicate with any implementation of the domain model.


In practice, I think you'll find that it is more common to couple controllers to "the" implementation of the domain model, rather than trying to decouple them. Take a look at onion/hexagonal/clean architecture, where coupling to implementations is expected when those couplings point toward the domain model.

My experience with trying to create a contract for the domain model (to facilitate changing the underlying implementation) is that legibility suffers quite a bit; therefore, make sure you really are going to get the return on investment before you start the work -- a bunch of fancy ceremony that gets used for only a single implementation is likely to be a bad trade.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 VoiceOfUnreason