Cohesion
Coupling
Cohesion - what classes belong to a package
Coupling - relations between packages
You can only reuse the amount of code that you can actually release
“Before software can be reusable it first has to be usable.” – Ralph Johnson
Classes that are used together are packaged together
{
"require": {
"php": "^7.3",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"symfony/yaml": "^3.4.29 || ^4.0 || ^5.0",
"symfony/console": "^3.4.29 || ^4.0 || ^5.0",
"symfony/filesystem": "^3.4.29 || ^4.0 || ^5.0",
"symfony/finder": "^3.4.29 || ^4.0 || ^5.0",
"symfony/process": "^3.4.29 || ^4.0 || ^5.0",
"composer/xdebug-handler": "^1.3.3",
"justinrainbow/json-schema": "^5.2",
"nikic/php-parser": "^4.2.2",
"sanmai/pipeline": "^3.1",
"sebastian/diff": "^3.0.2 || ^4.0",
"seld/jsonlint": "^1.7"
}
}
infection/core
{
"require": {
"php": "^7.3",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
- "symfony/yaml": "^3.4.29 || ^4.0 || ^5.0",
"symfony/console": "^3.4.29 || ^4.0 || ^5.0",
"symfony/filesystem": "^3.4.29 || ^4.0 || ^5.0",
"symfony/finder": "^3.4.29 || ^4.0 || ^5.0",
"symfony/process": "^3.4.29 || ^4.0 || ^5.0",
"composer/xdebug-handler": "^1.3.3",
"justinrainbow/json-schema": "^5.2",
"nikic/php-parser": "^4.2.2",
"sanmai/pipeline": "^3.1",
"sebastian/diff": "^3.0.2 || ^4.0",
"seld/jsonlint": "^1.7"
}
}
infection/core
Violation inside Infection
Problem
Solution
# composer.json
{
"name": "infection/mutation-badge",
"require": {
"ext-curl": "*"
}
}
# composer.json
{
"name": "infection/codeception-adapter",
"conflict": {
"codeception/codeception": "<3.1.1"
}
}
Instead of real dependencies, only suggestions:
# composer.json
{
"suggest": {
"graylog2/gelf-php": "Allow sending to a GrayLog2 server",
"sentry/sentry": "Allow sending to a Sentry server",
"doctrine/couchdb": "Allow sending to a CouchDB server",
"ruflin/elastica": "Allow sending to an Elastic Search server",
"php-amqplib/php-amqplib": "Allow sending to an AMQP server",
"ext-amqp": "Allow sending to an AMQP server (1.0+ required)",
"ext-mongo": "Allow sending to a MongoDB server",
"mongodb/mongodb": "Allow sending to a MongoDB",
"aws/aws-sdk-php": "Allow sending to AWS services like DynamoDB",
"rollbar/rollbar": "Allow sending to Rollbar",
"php-console/php-console": "Allow sending to Google Chrome"
}
}
monolog/monolog
Conclusion
The classes in a package should be closed together against the same kinds of changes.
GET /api/products?featureId=234
[
{"id": 1, "name": "Product 1"},
{"id": 2, "name": "Product 2"},
{"id": 3, "name": "Product 3"},
{"id": 4, "name": "Product 4"}
]
// 4 elements (all included)
Before (2.4.3)
After (2.4.7)
[]
// 0 elements (all excluded)
SELECT .. FROM .. WHERE p.feature_id=0
SELECT .. FROM ..
GET /api/products?featureId=string
Also, Common Reuse Principle is violated
Conclusion
The dependency structure between packages must be a directed acyclic graph.
No cycles
With cycle
public function runTests(TestFrameworkAdapterInterface $adapter): void
{
// ...
if ($adapter->testsPass()) {
// ...
}
}
class PhpUnitAdapter implements TestFrameworkAdapterInterface {}
class PhpSpecAdapter implements TestFrameworkAdapterInterface {}
class CodeceptionAdapter implements TestFrameworkAdapterInterface {}
Case with release a new major 2.0.0 version
interface TestFrameworkAdapterInterface
{
public function testsPass(): bool;
+ public function getVersion(): string;
}
# project's composer.json
{
"require-dev": {
"infection/core": "^1.0",
"infection/phpunit-adapter": "^1.0"
}
}
# project's composer.json
{
"require-dev": {
"infection/core": "^1.0"
}
}
Stable, Irresponsible
Unstable
Responsible
Instability Value
Instability Value
Instability Value
Instability Value
Instability Value
# phpspec adapter's composer.json
{
"require": {
"infection/core": "^0.13 || ^0.14 ... || 1.0.0",
"symfony/yaml": "^3.4.29 || ^4.0 || ^5.0",
}
}
Instability Value
# phpspec adapter's composer.json
{
"require": {
- "infection/core": "^0.13 || ^0.14 ... || 1.0.0",
+ "infection/abstract-testframework-adapter": "^1.0.0"
}
}
I=0
Instability Value
Abstractness Value
Abstractness Value
Cohesion - what classes belong to a package
Coupling - relations between packages
Before:
After:
# project's composer.json
{
"require-dev": {
"infection/core": "^1.0",
"infection/codeception-adapter": "^1.0"
}
}
# project's composer.json
{
"require-dev": {
"infection/core": "^1.0"
}
}
{
"name": "infection/codeception-adapter",
"type": "infection-extension",
"extra": {
"infection": {
"class": "Infection\\TestFramework\\Codeception\\CodeceptionAdapterFactory"
}
}
}
Infection analyzes composer.lock file and autoregisters all Infection plugins.
Before:
After:
# project's composer.json
{
"require-dev": {
"infection/core": "^1.0",
"infection/phpunit-adapter": "^1.0"
}
}
# project's composer.json
{
"require-dev": {
"infection/core": "^1.0",
"infection/phpspec-adapter": "^1.0"
},
"repositories": [
{
"type": "path",
"url": "../path-to-local/phpspec-adapter"
}
]
}
# studio.json
{
"version": 2,
"paths": [
"../infection-phpspec-adapter"
]
}
Monolith
Multiple Repositories
Monorepo
# infection/core
composer.json
/src
/Mutator
/Differ
...
# infection/codeception-adapter
composer.json
/src
CodeceptionAdapter.php
Version.php
...
# infection/phpunit-adapter
composer.json
/src
PhpunitAdapter.php
Version.php
# infection/infection
composer.json
/src
...
/Differ
/Mutator
/TestFramework
/Codeception
/PhpSpec
/PhpUnit
# infection/infection
/packages
/infection-core
/src
composer.json
/infection-codeception-adapter
/src
composer.json
/infection-phpspec-adapter
/src
composer.json
...
?
# infection/infection
/packages
/infection-core
/src
composer.json
/infection-codeception-adapter
/src
composer.json
/infection-phpspec-adapter
/src
composer.json
...