1

I'd like to find out how do you guys handle the following situation: you have a class hierarchy, call it H1, with some polymorphic method that is supposed to accept an argument which type forms hierarchy H2 in the following way: the higher the class is in H1, the higher H2 argument it accepts.

+-----------+           +-----------+
|     A     |           |     A'    |
|-----------+           +-----------+
|method(A') |                 ^
+-----------+                 |
      ^                 +-----------+ 
      |                 |     B'    |
+-----------+           +-----------+
|     B     |                 ^
|-----------+                 |
|method(B') |           +-----------+ 
+-----------+           |     C'    |
      ^                 +-----------+
      |
+-----------+
|     C     |
|-----------+
|method(C') |
+-----------+

Before writing any code I mention that I have two protocol classes with methods check that have much in common, but request parameters are specific for concrete arguments -- transactions. The language is scala, but the problem is language-agnostic. At the first glance it should look like that:

trait BaseMerchantProtocol
{
  def check(transaction: BaseTransaction) = {
    // some common code...
    println(requestParams(transaction))
    // ...and here as well
  }

  protected def requestParams(transaction: BaseTransaction)
}

class SmsCommerceMerchantProtocol extends BaseMerchantProtocol
{
  override protected def requestParams(transaction: SmsCommerceTransaction) = {
    List("result specific for SmsCommerceTransaction class")
  }
}

class TelepayMerchantProtocol extends BaseMerchantProtocol
{
  override protected def requestParams(transaction: TelepayTransaction) = {
    List("result specific for TelepayTransaction class")
  }
}

But of course it does not compile as Liskov substitution principle is violated.

Let's try this one:

trait IMerchantProtocol {
  def check(transaction: SmsCommerceTransaction)
  def check(transaction: TelepayTransaction)
}

class MerchantProtocol extends IMerchantProtocol
{
  def check(transaction: SmsCommerceTransaction) = {
    doCheck(transaction)
  }

  def check(transaction: TelepayTransaction) = {
    doCheck(transaction)
  }

  private def requestParams(transaction: SmsCommerceTransaction) = {
    List("result specific for SmsCommerceTransaction class")
  }

  private def requestParams(transaction: TelepayTransaction) = {
    List("result specific for TelepayTransaction class")
  }

  private def doCheck(transaction: BaseTransaction) = {
    // some common code...
    println(requestParams(transaction))
    // ...and here as well
  }
}

But that won't compile as well -- and with the same reason: doCheck accepts BaseTransaction, but requestParamss have more strict preconditions.

The only thing that I came up with and that works is the following:

class MerchantProtocol extends IMerchantProtocol
{
  def check(transaction: SmsCommerceTransaction) = {
    doCheck(transaction, requestParams(transaction))
  }

  def check(transaction: TelepayTransaction) = {
    doCheck(transaction, requestParams(transaction))
  }

  private def requestParams(transaction: SmsCommerceTransaction) = {
    List("result specific for SmsCommerceTransaction class")
  }

  private def requestParams(transaction: TelepayTransaction) = {
    List("result specific for TelepayTransaction class")
  }

  private def doCheck(transaction: BaseTransaction, requestParams: List[String]) = {
    // some common code...
    println(requestParams)
    // ...and here as well
  }
}

But I don't like that all check methods belong to the same class. How can I split them by classes?

  • 1
    What would happen if class D would derive from B and would accept D', which derives from B' and then : B = new D(); B.method(new C'()) ? While your idea might work with list hierarchy of classes, it stops making sense when you have tree hierarchy. I think you are approaching your problem from the wrong way. – Euphoric Jul 24 '14 at 19:10
  • Are those base (A and A') and intermediate (B and B') classes actually instantiated ever, or are they abstract, and only the most derived classes (C and C') are instantiated? – dbc Jul 26 '14 at 19:14
  • @dbc All of them are concrete classes. – Zapadlo Jul 28 '14 at 6:35
  • @Euphoric I wanted to outline a problem that I encounter quite often: some objects that (seem to) form a class hierarchy do something with (or with the help of) other objects that (seem to) form a class hierarchy as well. I.e., the more specific is the handler class, the more specific class it handles. I agree that I possibly approach the problem from the wrong way. But way is better? – Zapadlo Jul 28 '14 at 6:41
  • @Zapadlo - Could a class instance of type A (BaseMerchantProtocol) be instantiated and actually process transactions of B' (SmsCommerceTransaction) and C' (TelepayTransaction) as well as A' (BaseTransaction)? – dbc Jul 28 '14 at 6:47
1

There is one piece in OOP toolset, that allows you to enforce this kind of design : constructors.

Implement handling classes, so it accepts the request class inside it's constructor. This allows you to specify more concrete version of request class, while not breaking any kind of virtual method or class polymorphism. Concrete handler will require concrete request in it's constructor, while passing it's generic form to it's predecessor.

The problem then gets shifted to how to create concrete handler for concrete type of request. You could use "downchast check" or maybe a Visitor pattern, if you want compile-time typechecking. Encapsulating it in Factory class is given. This problem I believe is the core of your concern. If you can ensure correct handler is paired with correct request, then no problems can occur. Using constructors for parameters ensures, that the handler cannot be called incorrectly outside this factory.

  • hmm, it worked -- big thanks! I was surprised it worked though. Aren't constructors just a methods LSP is applied to? Why constructors can be non-polymorphic? And concerning the first part -- about hierarchy as well -- here it is: pastebin.com/ucya1iRH (scala) and pastebin.com/fJyTRVBv (php) Did I get it right? – Zapadlo Jul 28 '14 at 10:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.