1

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?

plalx
  • 399

5 Answers5

1

Indeed, LSP says that everywhere you use a Document you should be able to use an ExternalDocument. This is why you cannot strengthen the precondition. As a consequence, externalDocument.overwrite(a, b, c) should be valid whenever document.overwrite(a, b, c). If overwrite means just not to change the externalDocumentId everything is fine (I could overwrite an external document with the content of an internal document that would replace the content of the external document without changing its identity).

So far so good. But you seem to introduce and additional responsibility to track the origin on the top of external documents. This is no longer about extending the concept of Document: It's adding new responsibilities. Objectively, it seems to me that tracking change events could also make sense for internal documents.

For this reason, your ExternalDocument appears to be only in part an extension. The best way to improve the design is tho ensure separation of concerns between the real extension and the additional responsibilities:

  • limit the ExternalDocument to a Document extension that is LSP compliant;
  • use the decorator pattern to add to a Document or an ExternalDocument the responsibility of tracking change events.
Christophe
  • 81,699
0

One way to solve this using inheritance is to create an abstract base class BaseDocument that has all the functionality your current Document class has.

Both Document and ExternalDocument can now implement their own overwrite method.

Edit: I deleted this answer because the overwrite method is not part of the base class. Therefor it’s not possible to ‘program to an interface’. I undeleted this answer to allow others to leave comments for the OP why this approach is not ideal.

Rik D
  • 4,975
0

Are you absolutely sure all local documents can be overwritten?

They might be read-only, be it due to access-control or whatever. In which case, I would expect some error-indication.

Thus, an ExternalDocument objecting when you try would not violate the base contract at all, even if it does so unconditionally.

Deduplicator
  • 9,209
0

There are two different situations: One, you use inheritance so that an "ExternalDocument" can be used wherever a "Document" can be used. In that situation it should have compatible behaviour. A caller should have no need to know whether they have a Document or an ExternalDocument.

There are other situations where a caller knows nothing about "Document". They are not interested one bit how ExternalDocument is implemented, including that it is a subclass of "Document". It stands on its own. You will never ever be using an ExternalDocument in a place where a different subclass of Document can be used. That's rare. If that's the case, and you don't ever substitute one for the other, then there are no violations.

gnasher729
  • 49,096
0

If you want to add extra parameters to a method without changing the method signature, inject a context object into the constructor. ie

ExternalDoc
   ExternalDoc(IContext context)

public override Overwrite(a,b,c) { base.Overwrite(a,b,c) this.trackChange(this.context.ExternalEventId, "Overwritten") }

Prior to calling overwrite, you set the ExternalEventId in the context, which you keep a reference to after passing it into the ExternalDoc constructor

Ewan
  • 83,178