Patterns of Object-Oriented PHP: Composition

Posted by ashell on Mon, 01 Jul 2019 21:55:27 +0200

Opening chapter

If you pay attention to the catalogue, you will know that composition is a new beginning.
In the process of system code design, we organize code through inheritance, parent class and subclass, which correspond to the overall specification and specific requirements of the business. Therefore, we need to combine classes according to some logic, so that classes can become a collection system.
Composition patterns describe this logic - what patterns ("tools") are available when we need to associate classes through standard operations, or even format them into parent-child hierarchies.

problem

The complexity of managing a group of objects is relatively high, and it is more difficult to interpret it from the outside by theoretical way. To this end, a fictional scene is designed here:
In the previous model, we used a scenario similar to a civilized game. Now we continue to use it. Here, we want to implement a simple combat unit composition system.

Firstly, define some types of combat units:

abstract class Unit {
    abstract function bombardStrength();
}

class Archer extends Unit {
    function bombardStrength()
    {
        return 3;
    }
}

class LaserCannonUnit extends Unit {
    function bombardStrength()
    {
        return 10;
    }
}

We designed an abstract method, bombardStrength, to set up the damage of combat units, and realized two specific subclasses by inheritance: Archer, LaserCannonUnit. The complete class should naturally contain the content of mobile speed, defense and so on, but you can find that this is homogeneous, so we used the example code. Simple, omit it.

Next, we create a separate class to implement the combination of combat units (troops).

class Army {
    private $units = array();

    function addUnit( Unit $unit ) {
        array_push($this->units, $unit);
    }

    function bombradStrength() {
        $ret = 0;
        foreach ($this->units as $unit) {
            $ret += $unit->bombardStrength();
        }

        return $ret;
    }
}

The addUnit method of Army class is used for receiving units, and the total damage is calculated by bombardStrength method. But I don't think if you're interested in games, you'll be satisfied with such a crude model. Let's add something new: our army / allies split up (at present, if they're mixed together, they can't distinguish between forces).

class Army {
    private $units = array();
    private $armies = array();

    function addUnit( Unit $unit ) {
        array_push($this->units, $unit);
    }

    function addArmy( Army $army ) {
        array_push( $this->armies, $army );
    }

    function bombradStrength() {
        $ret = 0;
        foreach ($this->units as $unit) {
            $ret += $unit->bombardStrength();
        }

        foreach ( $this->armies as $army ) {
            $ret += $army->bombardStrength();
        }

        return $ret;
    }
}

So now, this Army class can not only merge the army, but also dismantle the Allied forces in one army when needed.

Finally, we look at the written classes. They all have the same method, bombardStrength, and logically have something in common, so we can integrate them into a family of classes.

Realization

Combination mode adopts single inheritance, and UML is released below.

As you can see, all military classes originate from Unit, but here's a note: Army, TroopCarrier classes are composite objects, Archer, LaserCannon classes are local objects or leaf objects.

Here we describe the class structure of the combination pattern. It is a tree structure. The combination object is a branch, which can open a considerable number of leaves. The leaf object is the smallest unit, and it can not contain other objects of the combination pattern.

Here's a question: Do local objects need to include methods like addUnit and removeUnit, which we'll discuss later for consistency?

Now we start to implement the Unit and Army classes. Looking at Army, we can find that it can save all instances of Unit-derived classes (objects), because they have the same method, they need the attack intensity of the army, so long as we call the attack intensity method, we can complete the summary.

Now, we are faced with the problem of how to implement add and remove methods, which are added to the parent class by the general combination pattern, which ensures that all derived classes share the same interface, but at the same time indicates that the system designer will tolerate redundancy.

This is the default implementation method:

class UnitException extends Exception {}

abstract class Unit {
    abstract function addUnit( Unit $unit );
    abstract function removeUnit( Unit $unit );
    abstract function bombardStrength();
}

class Archer extends Unit {
    function addUnit( Unit $unit ) {
        throw new UnitException( get_class($this) . " It belongs to the smallest unit.");
    }

    function removeUnit( Unit $unit ) {
        throw new UnitException( get_class($this) . " It belongs to the smallest unit.");
    }

    function bombardStrength()
    {
        return 3;
    }
}

class Army extends Unit {
    private $units = array();

    function addUnit( Unit $unit ) {
        if ( in_array( $unit, $this->units ,true)) {
            return;
        }
        $this->units[] = $unit;
    }

    function removeUnit( Unit $unit ) {
        $this->units = array_udiff(
            $this->units,
            array( $unit ),
            function( $a, $b ) { return ($a === $b) ? 0 : 1; }
        );
    }

    function bombardStrength() {
        $ret = 0;
        foreach ($this->units as $unit) {
            $ret += $unit->bombardStrength();
        }

        return $ret;
    }
}

We can do a few minor improvements: move the throw exception code of add and remove into the parent class:

abstract class Unit {
    function addUnit( Unit $unit ) {
        throw new UnitException( get_class($this) . " It belongs to the smallest unit.");
    }

    function removeUnit( Unit $unit ) {
        throw new UnitException( get_class($this) . " It belongs to the smallest unit.");
    }

    abstract function bombardStrength();
}

class Archer extends Unit {
    function bombardStrength()
    {
        return 3;
    }
}

Benefits of Combination Model

  • Flexibility: All classes in the composite schema share the same parent type, so it is easy to add new composite or local objects to the design without requiring extensive code modification.

  • Simple: Using combination mode, client code only needs to design simple interfaces. For the client, calling the required interface is enough, and there will be no "call without interface" situation. At least, it will also feedback an exception.

  • Implicit arrival: Objects are organized in a tree structure, and each composite object keeps references to subobjects. Therefore, a small operation in a part of the book may have a great impact, but it is unknown. For example, we move an army (a) under one army to Army 2, which is actually the army (a). All the individual troops in the army.

  • Display Arrival: The tree structure can be easily traversed, and the information containing the object can be quickly obtained by iterating the tree structure.

Finally, let's make a Small Test.

// Create a number
$myArmy = new Army();
// Adding soldiers
$myArmy->addUnit( new Archer() );
$myArmy->addUnit( new Archer() );
$myArmy->addUnit( new Archer() );

// Create a number
$subArmy = new Army();
// Adding soldiers
$subArmy->addUnit( new Archer() );
$subArmy->addUnit( new Archer() );

$myArmy->addUnit( $subArmy );

echo "MyArmy The total damage is:" . $myArmy->bombardStrength(); // The total damage done by MyArmy is 15.

Effect

Let me explain why methods like addUnit have to appear in local classes, because we want to keep Unit transparent - when clients make any access, they know that there must be addUnit or other methods in the target class, without any doubt.

Now, we parse the Unit class into an abstract subclass, CompositeUnit, and move the method that the composite object has onto it, adding a monitoring mechanism: getComposite.

Now, we have solved the "redundant method", but every time we call, we have to confirm whether it is a composite object through getComposite, and according to this logic, we can write a test code.

Complete code:

class UnitException extends Exception {}

abstract class Unit {
    function getComposite() {
        return null;
    }

    abstract function bombardStrength();
}

abstract class CompositeUnit extends Unit {
    private $units = array();

    function getComposite() {
        return $this;
    }

    protected function units() {
        return $this->units;
    }

    function addUnit( Unit $unit ) {
        if ( in_array( $unit, $this->units ,true)) {
            return;
        }
        $this->units[] = $unit;
    }

    function removeUnit( Unit $unit ) {
        $this->units = array_udiff(
            $this->units,
            array( $unit ),
            function( $a, $b ) { return ($a === $b) ? 0 : 1; }
        );
    }
}

class UnitScript {
    static function joinExisting( Unit $newUnit, Unit $occupyingUnit ) {
        if ( !is_null( $comp = $occupyingUnit->getComposite() ) ) {
            $comp->addUnit( $newUnit );
        } else {
            $comp = new Army();
            $comp->addUnit( $occupyingUnit );
            $comp->addUnit( $newUnit );
        }

        return $comp;
    }
}

When we need to implement personalized business logic in a subclass, one of the drawbacks of the composite pattern is emerging: the premise of simplification is that all classes inherit the same base class, and the advantage of simplification is sometimes at the expense of reducing object security. In order to compensate for the loss of security, we need to do type checking until one day you will find that we have done too much checking work - and even begun to significantly affect code efficiency.

class TroopCarrier {
    function addUnit(Unit $unit) {
        if ($unit instanceof Cavalry) {
            throw new UnitException("Horses cannot be placed on ships");
            super::addUnit($unit);
        }
    }

    function bombardStrength() {
        return 0;
    }
}

The advantages of the combination mode are being offset by more and more special objects. Only when most local objects are interchangeable, the combination mode is most suitable.

Another worrying problem is that if you have played with the top commander or swept thousands of troops, you will understand the seriousness of the problem. When you have thousands of combat units, and these units themselves belong to different times, you will bring huge amount of money every time you calculate the value of an army. Big military spending, even system crashes.

I'm sure we can all think of a solution to save a cache in a parent or top-level object. But in fact, unless you use very high-precision floating-point numbers, you should be careful about the validity of the cache (especially in languages like JS, where I've neglected its numerical exchange in order to make a series of game-valued caches). Calculate the error.

Finally, we need to pay attention to object persistence: 1. Although the combination mode is an elegant mode, it can not easily store itself in the relational database, you need to save the whole structure in the database through multiple expensive queries; 2. We can solve 1. problem by giving ID, but still can. We need to rebuild the parent-child reference relationship after we get the object, which makes it a little confusing.

Summary

If you want to "operate as you like an object", then the combination mode is what you need.

However, composition patterns depend on the simplicity of components, and as we introduce complex rules, code becomes more and more difficult to maintain.

Additional: Composite schemas are not well preserved in relational databases, but are well suited for persistence using XML.

(persistence = preservation)
(Parent = superclass, because English is SuperClass, plus, you may like the concepts of direct inheritance and indirect inheritance)

Little doubts

I find that foreigners often use practical applications to teach, especially very interesting applications such as games. I wonder whether this is the tradition of foreign teaching or my mistaken understanding.

Topics: PHP Database Mobile xml