11

I am troubled lately about the use of abstract classes.

Sometimes an abstract class is created in advance and work as a template of how the derived classes would work. That means, more or less, that they provide some high level functionality but leaves out certain details to be implemented by derived classes. The abstract class defines the need for these details by putting in place some abstract methods. In such cases, an abstract class works like a blueprint, a high level description of the functionality or whatever you wanna call it. It cannot be used on its own, but must be specialized to define the details that have been left out of the high level implementation.

Some other times, it happen that the abstract class is created after the creation of some "derived" classes (since the parent/abstract class is not there they have not been derived, yet, but you know what I mean). In these cases, the abstract class is usually used as a place where you can put any kind of common code that current derived classes contain.

Having made the above observations, I am wondering which of these two cases should be the rule. Should any kind of details be bubbled up to the abstract class just because they currently happen to be common in all the derived classes? Should common code that is not part of a high-level functionality be there ?

Should code that may have no meaning for the abstract class itself be there just because it happens to be common for the derived classes?

Let me give an example: Abstract class A has a method a() and an abstract method aq(). The method aq(), in both derived classes AB and AC, uses a method b(). Should b() moved to A ? If yes, then in case someone looks only at A (let's pretend AB and AC are not there), the existence of b() would make no much sense! Is this a bad thing? Should someone be able to have a look in an abstract class and understand what is going on without visiting the derived classes?

To be honest, at the moment of asking this, I tend to believe that writing an abstract class that makes sense without having to look in the derived classes it is a matter of clean code and clean architecture. Ι don't really like the idea of an abstract class that acts like a dump for any kind of code happens to be common in all derived classes.

What do you think/practice ?

4
  • 7
    Why must there be a "rule?" Commented Oct 28, 2017 at 0:27
  • 1
    Writing an abstract class that makes sense without having to look in the derived classes is a matter of clean code and clean architecture. -- Why? Isn't it possible that, in the course of designing and developing an application, I discover that several classes have common functionality that can naturally be refactored into an abstract class? Must I be clairvoyant enough to always anticipate this prior to writing any code for derived classes? If I fail to be this astute, am I forbidden from performing such a refactoring? Must I toss out my code and start over? Commented Oct 28, 2017 at 0:31
  • Sorry, if I was misunderstood! I was trying to say what I feel (for the moment) as a better practice and not implying that there should be an absolute rule. Moreover, I am not implying that any code that belongs to the abstract class should be written only in advance. I was describing how, in practice, an abstract class ends up with high-level code (that acts as a template for the derived ones) as well as low-level code (that you cannot understand it's usability in you don't look in the derived classes). Commented Oct 28, 2017 at 9:34
  • @RobertHarvey for a public base class, you would be prohibited from looking at derived classes. For internal classes, it makes no difference. Commented Oct 30, 2017 at 22:22

6 Answers 6

1

It seems to me you are missing the point of object orientation, both logically and technically. You describe two scenarios: grouping common behavior of types in a base class and polymorphism. These are both legitimate applications of an abstract class. But whether you should make the class abstract or not depends on your analytical model, not the technical possibilities.

You should recognize a type that has no incarnations in the real world, yet lays the groundwork for specific types that do exist. Example: an animal. There is no such thing as an animal. It is always a dog or a cat or whatever but there is no real animal. Yet Animal frames them all.

Then you speak of levels. Levels are not part of the deal when it comes to inheritance. Nor is recognizing common data or common behavior, that is a technical approach which will likely not help. You should recognize a stereotype and then insert the base class. If there is no such stereotype, you may be better of with a couple of interfaces that are implemented by multiple classes.

The name of your abstract class together with its members should make sense. It must be independent of any derived class, both technically and logically. Like Animal could have an abstract method Eat (which would be polymorphic) and boolean abstract properties IsPet and IsLifestock, that makes sense without knowing about cats or dogs or pigs. Any dependency (technical or logical) should go one way only: from descendant to base. Base classes themselves should have no knowledge of descending classes either.

3
  • The stereotype that you mention is more or less the same thing that I mean when I talk about higher-level functionality or when someone else talks about generalized concepts being described by class higher in the hierarchy (vs more specialized concepts described by classes lower in the hierarchy). Commented Nov 2, 2017 at 0:12
  • "Base classes themselves should have no knowledge of descending classes either." I totally agree with this. A parent class (abstract or not) should contain a self-contained piece of business logic that needs not inspection of the child class implementation to make sense of it. Commented Nov 2, 2017 at 0:15
  • There is another thing about a base class knowing its subtypes. Whenever a new subtype is developed, the base class needs to be modified which is backwards and violated the open-closed principle. I recently encountered this in existing code. A base class had a static method that created instances of subtypes based on the application's configuration. The intent was a factory pattern. Doing it this way also violates SRP. With some SOLID points I used to think "duh, that's obvious" or "the language will automatically take care of that" but I found people are more creative than I could imagine. Commented Nov 2, 2017 at 5:50
4

Let me give an example: Abstract class A has a method a() and an abstract method aq(). The method aq(), in both derived classes AB and AC, uses a method b(). Should b() moved to A ? If yes, then in case someone looks only at A (let's pretend AB and AC are not there), the existence of b() would make no much sense! Is this a bad thing? Should someone be able to have a look in an abstract class and understand what is going on without visiting the derived classes?

What your asking is where to place b(), and, in some other sense a question is whether A is the best choice as the immediate super class for AB and AC.

It seems there are three choices:

  1. leave b() in both AB and AC
  2. create an intermediate class ABAC-Parent that inherits from A and that introduces b(), and is then used as the immediate super class for AB and AC
  3. put b() in A (not knowing whether another future class AD will want b() or not)

  1. suffers from not being DRY.
  2. suffers from YAGNI.
  3. so, that leaves this one.

Until another class AD that doesn't want b() presents itself, (3) seems like the right choice.

At such time as an AD presents, then we can refactor to the approach in (2) — after all it is software!

9
  • 8
    There is a 4th option. Don't put b() in any class. Make it a free function that takes all its data as arguments and have both AB and AC call it. This means you don't need to move it or create any more classes when you add AD. Commented Oct 28, 2017 at 4:11
  • 1
    @user1118321, excellent, nice thinking outside the box. Makes particular sense if b() needs no instance-long-lived state.
    – Erik Eidt
    Commented Oct 28, 2017 at 4:12
  • What troubles me in (3) is that the abstract class does not describe anymore a self-contained piece of behavior. It is bits-and-pieces of code and I cannot understand the abstract class code without going back-and-forth between this and all the derived classes. Commented Oct 28, 2017 at 11:07
  • Can you make a base/default ac that calls b instead of ac being abstract?
    – Erik Eidt
    Commented Oct 28, 2017 at 14:06
  • 3
    To me, the most important question is, WHY both AB and AC both use the same method b(). If it's just coincidence, I'd leave two similar implementations in AB and AC. But probably it is because there exists some common abstraction for AB and AC. To me, DRY is not so much a value in itself, but a hint you that you probably missed some useful abstraction. Commented Oct 28, 2017 at 17:13
2

An abstract class is not meant to be the dumping ground for various functions or data that are thrown in to the abstract class because it is convenient.

The one rule of thumb that seems to provide the most reliable and extendable object oriented approach is "Prefer composition over inheritance." An abstract class is probably best thought of as an interface specification which doesn't contain any code.

If you have some method that is a kind of library method that goes along with the abstract class, a method that is the most likely means of expressing some functionality or behavior that classes derived from an abstract class typically need then it makes sense to create a class in between the abstract class and other classes where this method is available. This new class provides a particular instantiation of the abstract class defining a particular implementation alternative or path by providing this method.

The idea of an abstract class is to have an abstract model of what the derived classes which actually implement the abstract class are supposed to provide so far as service or behavior. Inheritance is easy to over use and so many times the most useful classes are composed of various classes using a mixin pattern.

However there are always the change questions, what is going to change and how will it change and why will it change.

Inheritance can lead to brittle and fragile bodies of source (see also Inheritance: just stop using it already!).

The fragile base class problem is a fundamental architectural problem of object-oriented programming systems where base classes (superclasses) are considered "fragile" because seemingly safe modifications to a base class, when inherited by the derived classes, may cause the derived classes to malfunction. The programmer cannot determine whether a base class change is safe simply by examining in isolation the methods of the base class.

3
  • I am sure that any OOP concept can lead to problems if it is misused or overused or abused! Moreover, making the assumption that b() is a library method (e.g a pure function without other dependencies) narrows down the scope of my question. Let's avoid this. Commented Oct 31, 2017 at 0:11
  • @AlexandrosGougousis I am not assuming that b() is a pure function. It could be a functoid or a template or something else. I am using the phrase "library method" as meaning some component whether pure function, COM object, or whatever that is used for the functionality it provides to the solution. Commented Oct 31, 2017 at 0:33
  • Sorry, if I wasn't clear! I mentioned the pure function as one out of many examples (you have given more). Commented Oct 31, 2017 at 0:46
1

Your question suggests an either or approach to abstract classes. But I think that you should consider them as just another tool in your toolbox. And then the question becomes: For which jobs/problems are abstract classes the right tool?

One excellent use case is to implement the template method pattern. You put all invariant logic in the abstract class and the variant logic in its subclasses. Note, that the shared logic in itself is incomplete and non-functional. Most of the time it is about implementing an algorithm, where several steps are always the same, but at least one step varies. Put this one step as an abstract method that is called from one of the functions inside the abstract class.

Sometimes an abstract class is created in advance and work as a template of how the derived classes would work. That means, more or less, that they provide some high level functionality but leaves out certain details to be implemented by derived classes. The abstract class defines the need for these details by putting in place some abstract methods. In such cases, an abstract class works like a blueprint, a high level description of the functionality or whatever you wanna call it. It cannot be used on its own, but must be specialized to define the details that have been left out of the high level implementation.

I think your first example is essentially a description of the template method pattern (correct me if I'm wrong), so I would consider this as a perfectly valid use case of abstract classes.

Some other times, it happen that the abstract class is created after the creation of some "derived" classes (since the parent/abstract class is not there they have not been derived, yet, but you know what I mean). In these cases, the abstract class is usually used as a place where you can put any kind of common code that current derived classes contain.

For your second example, I think that using abstract classes isn't the optimal choice, because there are superior methods for dealing with shared logic and duplicated code. Let's say you have abstract class A, derived classes B and C and both derived classes share some logic in the form of method s(). To decide on the correct approach to get rid of the duplication, it is important to know, whether method s() is part of the public interface or not.

If it is not (as in your own concrete example with method b()), the case is quite simple. Just create a separate class from it, also extracting the context that is needed to perform the operation. This is a classic example of composition over inheritance. If there is little or no context needed, a simple helper function might already be enough as suggested in some comments.

If s() is part of the public interface, it becomes a bit more tricky. Under the assumption that s() has nothing to do with B and C being related to A, you shouldn't put s() inside A. So where to put it then? I would argue for declaring a separate interface I, which defines s(). Then again, you should create a separate class that contains the logic for implementing s() and both B and C depend on it.

Last, here is a link to an excellent answer of an interesting question on SO that might help you to decide when to go for an interface and when for an abstract class:

A good abstract class will reduce the amount of code that has to be rewritten because it's functionality or state can be shared.

1
  • I would agree that s() being part of the public interface make things much more tricky. I would generally avoid defining public methods calling other public methods in the same class. Commented Oct 31, 2017 at 0:38
0

First case, from your question:

In such cases, an abstract class works like a blueprint, a high level description of the functionality or whatever you wanna call it.

Second case:

Some other times ... the abstract class is usually used as a place where you can put any kind of common code that current derived classes contain.

Then, your question:

I am wondering which of these two cases should be the rule. ... Should code that may have no meaning for the abstract class itself be there just because it happens to be common for the derived classes?

IMO you have two different scenarios, therefore you cannot draw a single rule to decide what design must be applied.

For the first case, we have things like Template Method pattern already mentioned in other answers.

For the second case, you gave the example of abstract class A receiving a b() method; IMO the b() method should only be moved if it makes sense for ALL the other derived classes. In other words, if you're moving it because it's used in two places, probably this is not a good choice, because tomorrow there might be a new concrete Derived class on which b() makes no sense at all. Also, as you said, if you look at A class separately and b() also makes no sense in that context, then probably it's a hint that this is not a good design choice as well.

-1

I've had exactly the same questions some time ago!

What I came to is whenever I stumble upon this issue I find that there is some hidden concept that I haven't found. And this concept more probably than not should be expressed with some value-object. You have very abstract example, so I'm afraid I can't demonstrate what I mean using your code. But here is a case from my own practice. I had two classes that represented a way to send request to external resource and parse the response. There were similarities in how the requests were formed and how the responses were parsed. So that's how my hierarchy looked like:

abstract class AbstractProtocol
{
    /**
     * @return array Registration params to send
     */
    abstract protected function assembleRegistrationPart();

    /**
     * @return array Payment params to send
     */
    abstract protected function assemblePaymentPart();

    protected function doSend(array $data)
    {
        return
            (new HttpClient(
                [
                    'timeout' => 60,
                    'encoding' => 'utf-8',
                    'language' => 'en',
                ]
            ))
                ->send($data);
    }

    protected function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}

class ClassicProtocol extends AbstractProtocol
{
    public function send()
    {
        $registration = $this->assembleRegistrationPart();
        $payment = $this->assemblePaymentPart();
        $specificParams = $this->assembleClassicSpecificPart();

        $dataToSend =
            array_merge(
                $registration, $payment, $specificParams
            );

        $this->log($dataToSend);

        $this->doSend($dataToSend);
    }

    protected function assembleRegistrationPart()
    {
        return ['hello' => 'there'];
    }

    protected function assemblePaymentPart()
    {
        return ['pay' => 'yes'];
    }
}

Such code indicates that I simply misuse inheritance. That's how it could be refactored:

class ClassicProtocol
{
    private $request;
    private $logger;

    public function __construct(Request $request, Logger $logger, Client $client)
    {
        $this->request = $request;
        $this->client = $client;
        $this->logger = $logger;
    }

    public function send()
    {
        $this->logger->log($this->request->getData());
        $this->client->send($this->request->getData());
    }
}

$protocol =
    new ClassicProtocol(
        new PaymentRequest(
            new RegistrationData(),
            new PaymentData(),
            new ClassicSpecificData()
        ),
        new ClassicLogger(),
        new ClassicClient()
    );

class RegistrationData
{
    public function getData()
    {
        return ['hello' => 'there'];
    }
}

class PaymentData
{
    public function getData()
    {
        return ['pay' => 'yes'];
    }
}

class ClassicLogger
{
    public function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}
class ClassicClient
{
    private $properties;

    public function __construct()
    {
        $this->properties =
            [
                'timeout' => 60,
                'encoding' => 'utf-8',
                'language' => 'en',
            ];
    }
}

Since then I treat inheritance very carefully cause I was hurt many times.

Since then I came to another conclusion about inheritance. I'm strongly against inheritance based on internal structure. It breaks encapsulation, it's fragile, it's procedural after all. And when I decompose my domain the right way, inheritance simply doesn't happen very often.

1
  • @Downvoter, please do me a favor and comment what's wrong with this answer. Commented Nov 1, 2017 at 16:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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