SOLID principles
Single responsibility
★★★★★
Open/closed
wat
Liskov substitution
★★★★
Interface segregation
★
Dependency inversion
★★★
If you don't recognise these.....then reading up on them can be your homework.
The title of this talk is a reference to the SOLID principles, which are suggested ways of how to write good code.
Apologies in advance
I normally try to make talks start off easy, before reaching the difficult stuff.
I think I may have failed.
J.B. Rainsberger - Integrated Tests Are A Scam
https://vimeo.com/80533536
I normally try to make talks start off easy, before reaching the difficult stuff.
If I completely lose you right from the start, apologies that is my fault. When I was doing a run through yesterday I realised that there's a good chance that the beginning might be a bit confusing, which would make the rest of the talk really hard to follow.
The ideas in this talk may be easier to understand after watching this talk about integrated tests.
We'll totally get onto Solid structure....
....but first something a bit different.
The true nature of reality
Understanding the most fundamental parts of programming.
@MrDanack
I think people sometimes misunderstand the most fundamental parts of programming.
Who here has studied any quantum physics?
So my education background is physics/chemistry
Solids, liquids, gases
Molecules and atoms
Fundamental particles, quarks and leptons
Fundamental forces, gravitational, electromagnetic, strong nuclear, and weak nuclear
At school you learnt about solids liquids and gases.
Then at secondary school you learn about molecules and atoms
Then at university you start learning about fundamental particles,
And then later years at university you learn about fundamental forces, and how particles don't really exist, And that everything is really just vibrations in a quantum lattice.
So my education background is physics/chemistry
Solids, liquids, gases
Molecules and atoms
Fundamental particles, quarks and leptons
Fundamental forces, gravitational, electromagnetic, strong nuclear, and weak nuclear
So it's almost 20 years ago to the day, that I was sitting in the lecture hall on a Monday morning at 9am, at the start of a 2 hour lecture about quantum physics, I realised a couple of things:
One - whoever scheduled a 2 hour quantum physics lecture for Monday morning is not a nice person.
Two - quantum physics is a bit hard - perhaps I'll go off and make video games for a living.
Three - and most importantly - most of the time, thinking about stuff at a really low level isn't actually that useful. Instead thinking at higher levels of abstraction is a much more productive thing to do.
Normal ways of thinking about code
What language to use
Frameworks vs Individual libraries
Procedural vs Object oriented
Functional vs Stateful
Those aren't the fundamental aspects of programming
Those aren't the fundamental things.
They're nice, and most of the time they are what you should be thinking about.
Fundamental programming forces
Can be unit tested
vs
Can't be unit tested
Semantically meaningful types
vs
Basic types e.g. strings, ints
By unit tested, I really mean tested in isolation.
This talk is mostly about the difference between unit-testable code and code that can't be tested, but the difference between typed code and 'untyped' code is also important.
So left me give you some examples of what the heck I mean.
So this is a representation of an application that does something like send notifications to Twitter.
You have a bootstrap layer, that calls some controller code, that calls some services, which then calls either a database, or some external API that's on the internet.
On the left we have the bootstrap and then index.php
In the middle we have the actual application code,
On the right hand side we have code that calls services
Left - bootstrap layer
function getTwitterApiKey()
{
return getenv('twitter_api_key');
}
// Hard-coded to specific class.
// and so not testable
$app = new App();
In the bootstrap layer you have things like this, that read the api key from the environment variable.
This is not testable. It relies on the environment variables being set up in particular way. So it is fundamentally not testable in isolation.
This true of all config loading no matter how you do it. Wherever the config is stored - even if it's stored as code, it's still just a chunk of data that needs to exit
Right - external services
DB
External api
File system
Anything that talks to a DB, an external API, or a file system can't be unit-tested.
You can test these things - just not with unit-tests. Either regression tests, or integration tests can (and should be) done.
But you can't test code that interacts with those things in isolation.
Right side - is typeless
function sendMessageToUser(
TwitterAPI $twitterApi,
Message $message,
User $user
) {
$text = "@".$user->getName()." ".$message->getText()
$twitterApi->sendMessage($text);
}
class TwitterAPI
{
function sendMessage(string $text);
...
}
As I mentioned - Typed vs un-typed is the other fundamental 'force' in programming.....but it's not really the subject of this talk
You really want to use strong, semantically meaningful types across as much of your application as possible.
Pushing semantically weak types to
Middle - your lovely app code
class SendMessageController
{
function __construct(TwitterApi $twitter, VariableMap $varMap)
{
$message = $varMap->getMessage();
$twitter->send($message);
}
}
This can be tested completely.
If you push all of the untestable code to the edges, you can write all of your application code in a way that allows it to be completely covered by unit-tests.
This is not easy.....if you don't have the right tools
On the left, the bootstrap layer is completely untestable.
In the middle you have your lovely application code, which you can write unit tests for until you reach 100% coverage.
On the right you have external services that can't be unit tested.
Having two different modes is a problem
Unstructured - testable + un-testable code gets mixed up, leading to a messy application
Encapsulating external services with interfaces can be a bit of pain
Dependency injection to the rescue
https://github.com/rdlowrey/auryn
Auryn is an auto-wiring recursive dependency injector.
aka it does DI really well.
Dependency injection is awesome.
Ever since I started programming, I've always tried to get better each year. However for me a lot of techniques that people say you should follow, are just too painful to use in practice. It turns out, that for a large part it was just because I wasn't using powerful enough tools.
I think a decent analogy is that trying to write SOLID code without using a Dependency Injection Container, is like trying to do some DIY, but instead of a screwdriver, you use a knife from the kitchen drawer.
Theoretically - it could just about work.
In Practice - it will be just such a pain that you're going to abandon the project half-way through.
So......'auto-wiring recursive dependency injector' sounds like a scary phrase. It's not really - I'll break down what Auryn does.
Auryn runs code for you
function foo()
{
echo 'Hello world';
}
$injector->execute('foo');
// Output is 'Hello world'
Can pass it any callable.
Basically you give it any callable it will run it for you.
This is a simple example, that doesn't have any parameters aka dependencies....
Defining raw params
class FileWriter implements Writer {
function __construct($path ) { ... }
function write($string) { ... }
}
$fileWriterParams = array(
':path' => '/var/coolapp',
);
$injector->define('FileWriter', $fileWriterParams);
// Auryn can also just make objects
$fileWriter = $injector->make(FileWriter::class);
Auto-wiring means it looks at param types
function writeHelloWorld(FileWriter $fileWriter)
{
$fileWriter->write("Hello world");
}
$fileWriterParams = array(
':path' => '/var/coolapp',
);
$injector->define('FileWriter', $fileWriterParams);
$injector->execute('writeHelloWorld');
I <3 types. And you should too.
So when there is a parameter that needs to be injected, Auryn goes away makes that parameter, and then passes it to the thing that is being executed.
The biggest problem with a lot of other DI libraries is that they use service names rather than types. This makes setting up the config be a complete nightmare.
I wish I'd saved it - there was a bug reported to bugs.php.net where someone had a 15,000 line config file for their Symfony app. And they were wondering why their app was being a bit slow.
Auryn just uses types, which means that it automatically understands what your types are, without having to setup 15,000 line config files or using abominations like annotations.
Sharing is caring - aka
function writeHelloWorld(FileWriter $fileWriter)
{
$fileWriter->write("Hello world");
}
$fileWriterParams = array(
':path' => '/var/coolapp',
);
$injector->define('FileWriter', $fileWriterParams);
$injector->share(FileWriter::class); // This line is new
$injector->execute('writeHelloWorld'); // FileWriter is created
$injector->execute('writeHelloWorld'); // FileWriter is reused
For some objects you only ever want to have one of them. For this example we only ever want to have one filewriter.
So we tell Auryn to share it as a dependency across all objects that need a file writer.
So the first time we execute writeHelloWorld a FileWriter is created, but then after that, the first object is re-used.
You can also share objects
$object = new Foo();
// Fiddle with $object state here
$injector->share($object); // This line is new
Also, instead of telling Auryn to share an object after it's been created, you can just tell Auryn "Here is an object". If you ever need to use an object of that type, use this one.
It's really useful when you need to hack around some legacy code that is doing some horrible stuff to initialize objects.
Aliasing interfaces to classes
class StubWriter implements Writer {
function write(string $string) { ... }
}
function foo(Writer $fw)
{
$fw->write("foo did something");
}
$injector->alias('Writer', 'StubWriter');
$injector->execute('foo');
Aliasing an interface to a class allows you to say which specific implementation gets created.
In this example, I'm aliasing the interface 'writer' to an implementation, 'StubWriter' - so that when Auryn sees that the function has a dependency on a 'writer' it actually creates a 'StubWriter' instead.
So far, so 'meh'
Most DICs can do that sort of thing.
The key thing that makes Auryn be incredibly powerful is how it allows you to set delegate functions for creating objects, and all of the object instantiation is done recursively.
Delegation is awesome
function createTwitterApiConfig()
{
return new TwitterApiConfig(
getenv('twitter_api_key'),
getenv('twitter_timout'),
);
}
$injector->delegate('TwitterApiConfig', 'createTwitterApiConfig');
Recursion is awesome
class TwitterApi {
function __construct(TwitterApiConfig $tac)
{
...
}
}
function sendMessage(TwitterApi $twitterApi)
{
...
}
$injector->delegate('TwitterApiConfig', 'createTwitterApiConfig');
$injector->execute('sendMessage');
Auryn resolves dependencies recursively. In this example, it goes to execute 'sendMessage', sees that it has a dependency on TwitterApi, so goes off to make that.
Auryn then sees that TwitterApi has a dependency on TwitterApiConfig, so goes off to create that, before being able to create the TwitterApi object.
The recursion is cool as it means that you don't need to composite anything together yourself. You just tell Auryn how to create each fundamental object once, and it then does the hard work of putting stuff together for you.
End result of using DI
Testable + typed that makes up most of your app, can be written cleanly.
Non-testable + untyped bootstrap code, completely separated away.
Injector provides the binding between these two different regions.
I have been using this pattern for a couple of years....I thought it was going to be really easy to explain how awesome it is.
Although I hope I've explained what it does, it's only when you actually start coding like this, you really get to see how powerful it is.
Bootstrapping your application becomes really easy. Writing application code in a way that easily testable becomes much easier.
Limitations of using an injector
There's two problems that don't occur in 'normal' programming, when using an injector:
More than one of a type.
Unknowable dependencies.
More than one of a type
// Move data that is more than 24 months old to archive
function archiveData(PDO $live, PDO $archive)
{
// ...
}
So we've got some code, that moves data from the live DB to an archive database.
Injector can't tell the two parameters apart.
// Don't execute this directly
function archiveData(PDO $live, PDO $archive)
{
// ...
}
// Instead excute a wrapped version.
function archiveDataWrapped(PDOFactory $pdoFactory)
{
$live = $pdoFactory->create('live');
$archive = $pdoFactory->create('archive');
archiveData($live, $archive);
}
Simplest way to work around this, is to wrap the code.
Context objects
class ArchiveContext {
public $current;
public $archive;
}
http://accu.org/index.php/journals/246
A more powerful solution is to use context objects. These allow for 'contexts' to be re-used across your application, instead of having to have hacks everywhere.
"The ability to bind the lifecycle and interactions of stateful components to well-defined but extensible lifecycle contexts."
function createArchiveContext(DataStorageFactory $dsFactory) {
return new ArchiveContext(
$dsFactory->create('current');
$dsFactory->create('archive');
);
}
$injector->delegate('ArchiveContext', 'createArchiveContext');
// Want to move data from current to archive
function archiveData(ArchiveContext $archiveContext)
{
$current = $archiveContext->current;
$archive = $archiveContext->archive;
...
}
Sometimes can't know dependencies
function sendMessage(UserPrefs $userPrefs, ...)
{
$service = $userPrefs->getMessageService();
if ($service == 'twitter') {
// Need a twitter Api
}
if ($service == 'pager_duty') {
// Need a pager duty Api
}
}
This seems to be a problem that Symfony, Zend frameworks just don't want to recognise.
I'm not entirely sure why, but I suspect it's just that they don't want people to know that there are limitations to those frameworks.
Symfony does abuse event handlers to solve this problem....some apps have 1600 event listeners registered, just in case they are needed.
function sendTwitterMessage(TwitterApi $twitterApi) {
...
}
function sendPagerDutyMessage(PagerDuty $pagerDuty) {
}
function sendMessage(UserPrefs $userPrefs)
{
$service = $userPrefs->getMessageService();
if ($service == 'twitter') {
return new Exec('sendTwitterMessage');
}
if ($service == 'pager_duty') {
return new Exec('sendPagerDutyMessage');
}
}
This seems to be a problem that Symfony, Zend frameworks just don't want to recognise. I'm not entirely sure why, but I suspect it's just that they don't want people to know that there are limitations to those frameworks.
Basically the only solution that doesn't involve nasty hacks is to allow multiple levels of execution. I use this in my own 'framework'.
Here, the 'sendMessage' routine doesn't actually send the message, it just looks up which callable to run next, and then tells the framework - Hey please run this next.
Sometimes you do want to use a service locator
$injector->share($injector);
function createFoo(Injector $injector, Config $config)
{
$apiVersion = $config->getApiVersion();
if ($apiVersion == 1) {
return $injector->make('ApiV1');
}
return $injector->make('ApiV2');
}
And finally, sometimes doing dependency injection properly is just too much effort, and you think to yourself, what can one little service locator hurt.
Fin
Push untestable code to the edges of your application.
Push untyped code to the edges of your application.
Use Auryn it is awesome.
CLI app - https://github.com/Danack/console
HTTP app - https://github.com/Danack/tier
Watch https://vimeo.com/80533536
https://joind.in/talk/88a21
“For years there has been a theory that millions of monkeys typing at random on millions of typewriters would reproduce the entire works of Shakespeare. The Internet has proven this theory to be untrue.”
Object-oriented programming is an exceptionally bad idea which could only have originated in California — Edsger W. Dijkstra