Jump to content

Composition over inheritance examples


NotionCommotion

Recommended Posts

I've definitely personally experienced improper use of inheritance which resulted in a complicated and inflexible mess.  I am not saying that inheritance is evil or shouldn't be used, but just when misused, yeah, maybe "evil" is the right word.

So, I've looked for opportunities to change the behavior of some of my classes by injecting different objects, and have found a few which has greatly simplified matters.  But I still struggling to identify more opportunities.  Most of the examples I read seem to be all the same and describe injecting an area calculator in a shape or a database interface in a mapper or some other simple example.  While this is great, often it is not some defined "thing" I need to inject but a collection of behaviors which I can't seem to define a name for.  For instance, we might have Admin extends User extends Person which seems good on the surface, but then I have a need for an Admin who isn't a user and things get wonkie.  So, instead I could inject a Person in an Admin, but I've been told that injecting some subset class into a class (i.e. inject a barchart into a chart) isn't right either.

So, where am I going with this?  I guess I need just more real examples how it is done right.  Any recommendations where I might find some (ideally in PHP)?  Also, how would you handle the Admin/Person example without using Inheritance?

Thanks

 

Link to comment
Share on other sites

Admin extends User extends Person means the admin is a user. If the admin is never a user then this isn't appropriate. But if an admin normally/often/generally is then that's fine, and a non-user admin could be something like a service account. But it's worth thinking more about whether it's a user or not. "User" doesn't necessarily have to be a person so I'm not sure User extends Person is right. Or really even what the purpose of a Person class would be.

About the chart thing. Injecting a BarChart into a Chart didn't make sense to me because a BarChart "is a" Chart, and that means it should use inheritance. Inheritance is very much so an "is a" relationship, and if you can describe two classes using that term then inheritance is probably the way to go. What would make sense if if you injected a "BarChartRenderer" into a Chart: the renderer takes care of dealing with how bar charts work, and the Chart is basically just a source of data.

...except that's not quite right either. If Chart is a data source then it should not care about rendering, so better would be the reverse of putting a Chart into BarChartRenderer. Which wouldn't really be injection so much as it would be simply one class using another. Normally.

Composition is about "is made from" in that some component is a part of how the object works. A sort of functional dependency. A Chart is not "made from" a BarChart so that's not composition. A BarChartRenderer is not "made from" a Chart so that's not composition either. A reasonable example of this would be having a User class be made from a Person: the User represents an entity in the system that's capable of actions, while a Person is surely a human who the User is acting on behalf of. Your code would deal primarily with a User, and that class would do what it needs to do with the Person behavior.

Composition is one step away from dependency injection, and most of the time those two can be converted to and from inheritance without much thinking involved.

<?php

namespace Inherited {

	// an admin is a user and gets all the behavior of a user
	class Admin extends User {
		public function deleteResource(Resource $r) { ... }
	}

	// a user is a person and gets all the behavior of a person
	class User extends Person {
		public function createResource(Resource $r) { ... }
	}

	class Person {
		public function useResource(Resource $r) { ... }
	}

}

namespace Composed {

	interface IAdmin extends IUser {
		public function deleteResource(Resource $r);
	}

	// admin is made from a user and delegates some behavior to it
	class Admin implements IAdmin {
		public function deleteResource(Resource $r) { ... }
		public function createResource(Resource $r) { (new User)->createResource($r); }
		public function useResource(Resource $r) { (new User)->useResource($r); }
	}

	interface IUser extends IPerson {
		public function createResource(Resource $r);
	}

	// user is made from a person and delegates some behavior to it
	class User implements IUser {
		public function createResource(Resource $r) { ... }
		public function useResource(Resource $r) { (new Person)->useResource($r); }
	}

	inteface IPerson {
		public function useResource(Resource $r);
	}

	class Person implements IPerson {
		public function useResource(Resource $r) { ... }
	}

}

namespace DependencyInjected {

	interface IAdmin extends IUser {
		public function deleteResource(Resource $r);
	}

	// admin must be given a user to depend on
	class Admin implements IAdmin {
		private $_user;
		public function __construct(User $u) { $this->_user = $u; }
		public function deleteResource(Resource $r) { ... }
		public function createResource(Resource $r) { $this->_user->createResource($r); }
		public function useResource(Resource $r) { $this->_user->useResource($r); }
	}

	interface IUser extends IPerson {
		public function createResource(Resource $r);
	}

	// user must be given a person to depend on
	class User implements IUser {
		private $_person;
		public function __construct(Person $p) { $this->_person = $p; }
		public function createResource(Resource $r) { ... }
		public function useResource(Resource $r) { $this->_person->useResource($r); }
	}

	inteface IPerson {
		public function useResource(Resource $r);
	}

	class Person implements IPerson {
		public function useResource(Resource $r) { ... }
	}

}

?>
<?php

namespace Inherited {

	/* this makes sense */

	// a bar chart is a type of chart
	class BarChart extends Chart {
		public function getBarWidth() { ... }
		public function setBarWidth($width) { ... }
		public function render() { ... }
	}

	// a chart forms the base for how types of charts work
	abstract class Chart {
		public function getTitle() { ... }
		public function setTitle($title) { ... }
		public abstract function render();
	}

}

namespace Composed {

	/* this is bad */

	// a chart is made from a barchart. it would have stuff for other chart types too
	// class Chart {
	//	private $_config;
	//	public function __construct($config) { $this->_config = $config; }
	//	public function getTitle() { ... }
	//	public function setTitle($title) { ... }
	//	public function getBarWidth() { return BarChart::getBarWidth($this->_config); }
	//	public function setBarWidth($width) { $this->_config = BarChart::setBarWidth($this->_config, $width); }
	//	public function renderAsBarChart() { (new BarChart($this->_config))->render(); }
	// }

	// a bar chart has bar chart knowledge
	// class BarChart {
	//	private $_config;
	//	public function __construct($config) { $this->_config = $config; }
	//	public static function getBarWidth($config) { ... }
	//	public static function setBarWidth($config, $width) { ... }
	//	public function render() { ... }
	// }

}

namespace DependencyInjected {

	/* more reasonable than composition, and a different approach to the problem than inheritance */

	interface IChartRenderer {
		public function render(Chart $chart);
	}

	// a chart has chart data
	class Chart {
		private $_config;
		private $_renderer;
		public function __construct($config, IChartRenderer $renderer) { $this->_config = $config; $this->_renderer = $renderer; }
		public function getConfig($key, $default = null) { return $this->_config[$key] ?? $default; }
		public function getTitle() { ... }
		public function setTitle($title) { ... }
		public function render() { $this->_renderer->render($this); }
	}

	// a bar chart renderer knows how to render charts into the bar style
	class BarChartRenderer implements IChartRenderer {
		public function render(Chart $chart) { ... }
	}

}

?>

 

I wrote a lot so I'm going to stop there, but I don't think I said everything I was intending to say.

Link to comment
Share on other sites

So I'm working with React and they strongly suggest composition over inheritance. I still don't understand why.

Here's composition like they say (slightly rewritten):

class Dialog extends React.Component {
    render() {
        return (
            <FancyBorder color="blue">
                <h1 className="Dialog-title">
                    {this.props.title}
                </h1>
                <p className="Dialog-message">
                    {this.props.message}
                </p>
            </FancyBorder>
        );
    }
}

class WelcomeDialog extends React.Component {
    render() {
        return <Dialog title="Welcome" message="Thank you for visiting our spacecraft!" />;
    }
}

This is inheritance:

class WelcomeDialog extends Dialog {
    constructor() {
        super({title: "Welcome", message: "Thank you for visiting our spacecraft!"});
    }
}

 

Link to comment
Share on other sites

Admin seems more like a privilege/role, so I would make that a field of User.

Then your User and Person. I get what you are saying, and I have this in my system. I would have a Person object with no reference to a User. And let the User reference Person. Thus decoupling my application from the auth layer.

<?php

class User {
  const ROLE_SYSTEM = 'role_system';
  const ROLE_ADMIN = 'role_admin';

  /**
   * @var string[]
   */
  private $roles = [ ];

  /**
   * @var Person|null
   */
  private $person;
}

For system accounts, the Person reference would be NULL as it's not required and it's role would be ROLE_SYSTEM.

Link to comment
Share on other sites

I like your "is a" and "made from" tests.  For a simple applicable, inheritance is simple and concise, but the class size and complexity will increase as functionality is added and there is the potential that functional requirements will diverge.  At that time, I will ask myself what abstract concept is it made from and break it out.  I am sure it will take time and practice, but think it is a good start.  I even pinned a sticky note (something I never do) stating these two tests so I don't fall out of the habit of asking myself them.
 

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.