I have Document and ExternalDocument classes in my system, where ExternalDocument extends Document. The main distinction is that ExternalDocument holds onto externalDocumentId and externalEventId data in order to correlate with the external system.
Documents may be overwrote calling document.overwrite(a, b, c). When overwriting external documents I want to track the externalEventId that triggered the change and this is where the design falls apart.
According to the LSP I shouldn't strengthen preconditions in document.overwrite. I could implement an document.externalOverwrite operation and throw an exception when document.overwrite is called directly, but that stills violates the LSP.
The language I use doesn't support generics so I can't go for Document<T> either where T defines the override contract parameter.
I could solve the problem by not inheriting from Document at all and use composition instead, but it feels weird given ExternalDocument still is a Document specialization.
Any guidance?
EDIT:
Just to give a little more context, local documents can be overwrote by a local/user process. External documents are a reflection of documents existing in an external system. I want to communicate the fact that we do not have authority over external documents. The state of those documents is updated in response to remote system events and I want to be able to correlate every state change with a corresponding externalEventId.
Note that some local document operations remain valid on the external ones though, like assigning the document, etc. I'm also trying to keep the business logic within the model as much as possible to avoid an anemic domain model.
After thinking a little more about it I think I may have conflated both "overwrite" operations as one although overwriting a local document & external document are actually distinct processes. I think we could make a parallel between this and having multiple kinds of locks: they can all be opened, but all in very different ways that would be hard to generalize.
Therefore, so far the most logical route seems to be splitting the current concrete Document into Document (abstract base class) and LocalDocument. The overwrite operation would be implemented on both LocalDocument and ExternalDocument. Both implementations could leverage an internal overwrite implementation living in the Document abstract class for parts of the process that are similar.
Obviously clients would have to know what type of document they are dealing with in order to process an overwrite.
Any new suggestions in light of those precisions?
overwrite? – Alexander Oct 08 '20 at 19:07ExternalDocumentis aDocumentis not in itself sufficient justification to makeExternalDocumenta subclass ofDocument. Consider the famous square/rectangle problem. – Alexander Oct 08 '20 at 19:08ExternalDocumentare triggered by receiving an event from a remote system in which case I want to track theIDof the event that triggered the change along with the new state. BothoverwriteandexternalOverwriteare probably distinct operations. However,externalDocument.overwriteshouldn't normally take place as we do not have authority over those documents. – plalx Oct 08 '20 at 19:13PurposeForChange(or something like that) interface, which is an input tooverwrite. In the general document case, it can beNoReasonobject (until you have a business requirement to track why those happened, too), and in theExternalDocument, it can be a different object that contains the id of the external event that triggered the change. Unfortunately without covariant types, you don't have a way of saying thatExternalDocument.overwriteexcepts aExternalDocumentChangeReasoninstead of aPurposeForChange– Alexander Oct 08 '20 at 19:24Documentabstract class withStandardDocumentandExternalDocument. The abtract class wouldn't expose any operation but I could still use it in the repository's contract. However, service classes must now cast to explicit document types, but they kinda need to anyway since we deal with those types differently. – plalx Oct 08 '20 at 19:27Documentinstance as an argument for composition. In your case, you should give up the inheritance altogether. – Andy Oct 08 '20 at 19:37overwriteandexternalOverwriteare conceptually two distinct processes I think removing the operation from the base class seems to make the most sense in the end, no? All the solutions require the client to know what process it's fulfilling anyway, whether areasonis used or generic types could be used. – plalx Oct 08 '20 at 19:59ExternalDocumentdoesn't behave as aDocument, then it's not a specialization of aDocument. Now, it is possible that you've defined the behavior of theDocumenttoo narrowly - that's worth considering too. – Filip Milovanović Oct 08 '20 at 20:27overwriteon the specializations only? – plalx Oct 08 '20 at 20:59overwritemethod as immutable private fields); a derivative command could similarly store anexternalEventId, and all could have a common interface (e.g.execute(). – Filip Milovanović Oct 08 '20 at 21:45ExternalDocumentcontains aDocumentinstance, which is accessible through a getter and on which operations may be executed (alternatively you can introduce proxy methods, something like this: https://pastebin.com/ssn92bxS). – Andy Oct 09 '20 at 06:13AbstractDocumentthat contains all those operations and implement the 1% on specializations? I'm all for composition over inheritance, but delegating all operations clearly is a smell IMO. – plalx Oct 09 '20 at 11:36ExternalDocument, then the information is there to use whenever overwritten. Otherwise there's not enough information here to understand the issue. – Erik Eidt Oct 09 '20 at 12:02externalEventId. Some local document operations remain valid on the external ones though, like adding notes, assigning the document, etc. I'm also trying to keep the logic business logic within the model. – plalx Oct 09 '20 at 15:15