OOP - 在哪里声明属性?

My parent class has a few methods that rely on properties that can only be set by the child classes. What's the "best" approach to achieve this?

  1. Parent class has the properties set to defaults (e.g. a string is set to null string), then child class sets parent's properties in its constructor.
  2. The child class defines the properties as its own
  3. Should the parent class' methods accept the necessary values as parameters, instead?
  4. Something else???

Also - is it bad OOP that the parent methods rely on something that the child needs to provide?

I'm doing this in PHP, if that matters.

The way to handle this in PHP is the same way you handle it in any decent language -- with protected abstract methods.

http://php.net/manual/en/language.oop5.abstract.php

<?php
abstract class AbstractClass
{
    // Our abstract method only needs to define the required arguments
    abstract protected function prefixName($name);
}

class ConcreteClass extends AbstractClass
{

    // Our child class may define optional arguments not in the parent's  signature
    public function prefixName($name, $separator = ".") {
        if ($name == "Pacman") {
            $prefix = "Mr";
        } elseif ($name == "Pacwoman") {
            $prefix = "Mrs";
        } else {
            $prefix = "";
        }
        return "{$prefix}{$separator} {$name}";
    }
}

$class = new ConcreteClass;
echo $class->prefixName("Pacman"), "
";
echo $class->prefixName("Pacwoman"), "
";
?>

So... You have one class that has an algorithm and another class that is responsible to provide the data for that algorithm... When I word it this way, does it still make sense to you for one of these classes to be a parent of the other?

Your "parent class" should take an object of the "child class" probably as a parameter in a constructor and then use that object to do its work. This compositional pattern is far more reliable, reusable and understandable.

See the Strategy Pattern in the design patterns literature for more information.


In other words, instead of this:

+--------------+
|    Parent    |
+--------------+
|*getName()    |
|showGreeting()|
+-----+--------+
      ^
      |
      |
 +----+-----+
 |  Child   |
 +----------+
 |*getName()|
 +----------+

Do this:

+--------------------+    +----------+
|       Parent       |    |  Child   |
+--------------------+    +----------+
|Parent(child: Child)|--->| getName()|
|showGreeting()      |    +----------+
+--------------------+    

Based on your comment here, it sounds like you're talking about subtyping polymorphism. The basic idea behind polymorphism is that you can do something like this.

Option 1: No default. This should be used if calling this function should never happen from the parent (e.g. Shape) class

class Shape
{
    // This doesn't make sense until we know what shape we're talking about
    abstract public function getArea();
}

Then you have children classes that look like this:

class Square extends Shape
{
    public $sideLength;

    public function getArea()
    {
        return ($this->sideLength ** 2);
    }
}

class Triangle extends Shape
{
    public $base;
    public $height;

    public function getArea()
    {
        return ($this->base * $this->height * 0.5);
    }
}

etc.

Option 2: If the function should be able to be called in the parent, you would include the properties and the function itself.

class Rectangle
{
    public $length;
    public $width;

    public function getArea()
    {
        return ($this->length * $this->width);
    }
}

Then you have children classes that look like this:

class Square extends Rectangle
{
    public $length;

    public function getArea()
    {
        return ($this->length ** 2);
    }
}
etc.

In my opinion this is a very broad question and the answer entirely depends on the use case. Let me show by using some easy examples:

Your first suggestion:

abstract class Vehicle {
 String name = "SuperDriver";
 int numberOfWheels = 4;
 int maxSpeed = 20;

 public function drive(Terrain terrain) {
   echo this.name." drives with ".(this.maxSpeed - terrain->modifier)." mph in ".terrain->name;
 }

}

class Car extends Vehicle {

  public function __construct() {
    this.name="Ferrari";
    this.numberOfWheels = 4;
    this.maxSpeed = 310;
  }

}

class Bicycle extends Vehicle {
  public function __construct() {
    this.name="Mountain Bike";
    this.numberOfWheels = 2;
    this.maxSpeed = 150;
  }
}

Advantages:

You even could instantiate Vehicle if you do not declare it abstract and it could even drive.

Disadvantages:

  • A non-concrete Vehicle does not make a lot of sense
  • Anybody how extends the class has to know that all the properties must be overwritten
  • hard to extend and hard to maintain
  • maybe others ...

Your second suggestion:

abstract class Vehicle {

 public function drive(Terrain terrain) {
   echo this.name." drives with ".(this.maxSpeed - terrain->modifier)." mph in ".terrain->name;
 }

}

class Car extends Vehicle {

    this.name="Ferrari";
    this.numberOfWheels = 4;
    this.maxSpeed = 310;

}

class Bicycle extends Vehicle {

    this.name="Mountain Bike";
    this.numberOfWheels = 2;
    this.maxSpeed = 150;
}

I can not see any advantage. If the parent class needs those properties you should definitly make them required. If the parent class does not need any of those properties then this is child class territory which you should not care about.

Your third suggestion:

abstract class Vehicle {

 protected function drive(Terrain terrain, String name, int maxSpeed) {
   echo name." drives with ".(maxSpeed - terrain->modifier)." mph in ".terrain->name;
 }

 abstract public function drive(Terrain terrain);

}

class Car extends Vehicle {
    this.name="Ferrari";
    this.numberOfWheels = 4;
    this.maxSpeed = 310;

  function drive(Terrain terrain) {
   parent::drive(terrain,this.name,this.maxSpeed);
  }
}

class Bicycle extends Vehicle {
    this.name="Mountain Bike";
    this.numberOfWheels = 2;
    this.maxSpeed = 150;

  function drive(Terrain terrain) {
   parent::drive(terrain,this.name,this.maxSpeed);
  }
}

Advantages:

  • You have some flexibility now
  • You can extend the drive method

Disadvantages:

  • Basically you declare two different methods with the same name in order to achieve what you want so this is a workaround
  • not very flexible

This is how you could force unchangeable properties:

abstract class Vehicle {
 String name;
 int numberOfWheels;
 int maxSpeed;

 public function __construct() {

 }

 protected abstract function getName();
 protected abstract function getNumberOfWheels();
 protected abstract function getMaxSpeed();

 public function drive(Terrain terrain) {
   echo this.getName()." drives with ".(this.getMaxSpeed() - terrain->modifier)." mph in ".terrain->name;
 }

}

class Car extends Vehicle {

  protected function getName() { return "Ferrari"; }
  protected function getNumberOfWheels() { return 4; }
  protected function getMaxSpeed() { return 310; }

}

class Bicycle extends Vehicle {
  protected function getName() { return "Mountain Bike"; }
  protected function getNumberOfWheels() { return 2; }
  protected function getMaxSpeed() { return 150; }
}

If you do it this way nobody can change the properties from outside, only subclasses.

You also could use a protected constructor in the superclass:

abstract class Vehicle {
 String name;
 int numberOfWheels;
 int maxSpeed;

 protected function __construct($name,$numberOfWheels,$maxSpeed) {
   this.name = name;
   this.numberOfWheels = numberOfWheels;
   this.maxSpeed = maxSpeed;
 }

 public function drive(Terrain terrain) {
   echo this.name." drives with ".(this.maxSpeed - terrain->modifier)." mph in ".terrain->name;
 }

}

class Car extends Vehicle {
   // or pass the arguments here, if you want an outside user to set em    
   public function __construct() {
       parent::__construct("Ferrari",4,310);
   }

}

Why this works:

Note: Parent constructors are not called implicitly if the child class defines a constructor. In order to run a parent constructor, a call to parent::__construct() within the child constructor is required. If the child does not define a constructor then it may be inherited from the parent class just like a normal class method (if it was not declared as private).

If you have public properties which just should be initialized simply use constructor arguments.

Your second question: Is it bad OOP that the parent methods rely on something that the child needs to provide?

In my opinion no, not necessarily. Otherwise there would be only interfaces and no abstract classes or e.g. Traits (PHP). Mechanisms like this avoid duplicate code.