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?ExternalDocumentis aDocumentis not in itself sufficient justification to makeExternalDocumenta subclass ofDocument. Consider the famous square/rectangle problem.ExternalDocumentare 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.PurposeForChange(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 aPurposeForChangeDocumentabstract 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.