SOLID
5 characters for better code
Jan-Oliver Pantel
@JanPantel
- CouchCommerce
- Hochschule Hannover

SOLID
- First introduced in the early 2000's
- By Robert 'Uncle Bob' Martin
- Good foundation for application design
- Collection of 5 software design principles
- All are connected
- Breaking one often means breaking multiple
- Understanding SOLID results in higher understanding of the most design patterns
S
ingle Responsibility
class ProductController extends MvcController
{
fn save()
{
if ( ! this.request.has(['price', 'name']))
{
return new BadRequestResponse('Missing input data!');
}
if (this.database.table('products').query().whereEquals('name', this.request.body.name).count() > 0)
{
return new BadRequestResponse('The name is already taken!');
}
if (this.request.body.price <= 0)
{
return new BadRequestResponse('The price must be greater than 0!');
}
var product = new Product();
product.price = this.request.body.price;
product.name = this.request.body.name;
try
{
product.save();
}
catch (DatabaseError e)
{
return new InternalServerErrorResponse('Error saving the product!');
}
return new Response(product);
}
}Looks good?
Nope....
- What are the responsibilities of this class?
- Handle the "save product"-endpoint
- What tasks do this class tackle in real?
- Validation of the request
- Validation against domain rules
- Database interaction
- HTTP logic
The Single Responsibility Principle
[...] the single responsibility principle states that every class should have responsibility over a single part of the functionality [...] and that responsibility should be entirely encapsulated by the class.
- wikipedia.com
Problems?
- We always have to alter the ProductController if
- the domain logic changes
- we use another data store
- But the only reason it should change if we use another protocol
- Responsibility of the controller: Transport (in this case HTTP)
Solution
class ProductRepository
{
fn construct(Database database);
fn exists(name)
{
return this.database
.table('products')
.query()
.whereEquals('name', name)
.count() > 0;
}
fn create(data)
{
var product = new Product();
product.price = data.price;
product.name = data.name;
try
{
product.save();
}
catch (DatabaseError e)
{
return null;
//Or throw any domain exception
//for a more explicit error reporting
}
return product;
}
}class SaveProductRequestValidator
{
fn construct(ProductRepository productRepo);
fn validate(data)
{
var errors = [];
if ( ! data.contains(['price', 'name']))
{
errors.add('Missing input data!'));
}
if (productRepo.exists(data.name))
{
errors.add('Name already taken!');
}
if (data.price <= 0)
{
errors.add('The price must be greater than 0!');
}
return errors;
}
}The Controller
class ProductController extends MvcController
{
fn construct(ProductRepository productRepo, SaveProductRequestValidator validator);
fn save()
{
var validationResult = this.validator.validate(this.request.body);
if ( ! validationResult.empty())
{
return new BadRequestResponse(validationResult);
}
product = this.productRepo.create(this.request.body);
return product != null
? new Response(product)
: new InternalServerErrorResponse('Error saving product!');
}
}O
pen Closed Principle
class SaveProductRequestValidator
{
fn construct(ProductRepository productRepo);
fn validate(data)
{
var errors = [];
if ( ! data.contains(['price', 'name']))
{
errors.add('Missing input data!'));
}
if (productRepo.exists(data.name))
{
errors.add('Name already taken!');
}
if (data.price <= 0)
{
errors.add('The price must be greater than 0!');
}
return errors;
}
}Drawbacks
- Class needs to be modified if
- business rules change
- more validations needed
- Possibility of breaking the application
- We need a VCS to rollback changes
The Open Closed Principles
[...]the open/closed principle states "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"; that is, such an entity can allow its behaviour to be extended without modifying its source code.
- wikipedia.com
Solution
interface SaveProductRequestValidatorInterface
{
fn validate(data);
}class SaveProductRequestValidator
implements SaveProductRequestValidatorInterface
{
fn construct(ProductRepository productRepo);
fn validate(data)
{
// ...
}
}class ProductController extends MvcController
{
fn construct(
ProductRepository productRepo,
SaveProductRequestValidatorInterface validator
);
fn save()
{
// ...
}
}Solution
class ExtendedSaveProductRequestValidator
extends SaveProductRequestValidator
{
fn construct(ProductRepository productRepo);
fn validate(data)
{
errors = parent.validate(data);
if (data.price > 999)
{
errors.add('Price needs to be lower than 1000!');
}
}
}- Now we are able to inject the new validator to the controller
- We didn't modified old code
- We can easily rollback to the old logic
- We didn't modified the controller
class PresenceSaveProductRequestValidator
extends SaveProductRequestValidatorInterface
{
fn construct(ProductRepository productRepo);
fn validate(data)
{
if ( ! data.has(['name', 'price']))
{
return ['Missing input attributes!'];
}
return [];
}
}L
iskov Substitution Principle
interface ProductRepositoryInterface
{
fn exists(name);
fn create(data);
}class XmlProductRepository implements ProductRepositoryInterface
{
//let's assume the document is already parsed in this object
fn construct(XmlDocument xml);
fn exists(name)
{
return this.xml.xpath('//product[@name=\'' + name + '\']').count() > 0;
}
fn create(data)
{
// ...
}
}Switching implementations
class DatabaseProductRepository implements ProductRepositoryInterface
{
//let's assume our db connection does not connect automatically
fn construct(Database database);
fn connect()
{
if ( ! this.database.isConnected())
{
this.database.connect();
}
}
fn exists(name)
{
return this.database
.table('products')
.query()
.whereEquals('name', name)
.count() > 0;
}
fn save(data)
{
// ...
}
}The controller
The controller now has to be aware of this implementation detail
class ProductController extends MvcController
{
fn save()
{
if (this.productRepo instanceof DatabaseProductRepository)
{
this.productRepo.connect();
}
product = this.productRepo.save(this.request.body);
// ...
}
}Drawbacks
- The controller has to be aware of implementation details
- The controller is coupled to the database repository class
- You can't drop in the database repository without altering the controller
- By default there is no need for the controller to change
Liskov substitution principle
[...] states that [...] if S is a subtype of T, then objects of type T may be replaced with objects of type S [...] without altering any of the desirable properties of that program (correctness, task performed, etc.)
- wikipedia.com
Solution
class DatabaseProductRepository implements ProductRepositoryInterface
{
//let's assume our db connection does not connect automatically
fn construct(Database database);
fn connect()
{
if ( ! this.database.isConnected())
{
this.database.connect();
}
}
fn exists(name)
{
this.connect();
return this.database
.table('products')
.query()
.whereEquals('name', name)
.count() > 0;
}
fn save(data)
{
this.connect();
// ...
}
}I
nterface segragation principle
class Car
{
fn drive(metersPerSecond, seconds = 1)
{
this.position += metersPerSecond * seconds;
}
fn activateCruiseControl()
{
this.cruiseControl.control(this);
}
}Anti-Pattern described with classes
Now let's implement a bycycle... hey cheap we can extend Car because we need the same implementation of drive()
class Bycycle extends Car
{
fn activateCruiseControl()
{
throw new Error('bikes do not have any cruise control!');
}
}JACKPOT!
IT WORX WITH NEARLY NO EFFORT!!!11
Drawbacks :(
- The class Bycicle now has the method activateCruiseControl() from the Car
- The method does not make any sense at all
- By default the method throws an exception
Interface segregation principle
The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use. ISP splits interfaces which are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.
- wikipedia.com
Solution
abstract class DriveableVehicle
{
fn drive(metersPerSecond, seconds = 1)
{
this.position += metersPerSecond * seconds;
}
}class Car extends DriveableVehicle
{
fn activateCruiseControl()
{
this.cruiseControl.control(this);
}
}class Bycycle extends DriveableVehicle
{
}Side node:
You can also use traits in some languages
Back to interfaces
interface CollectionInterface
{
fn add(entry);
fn has(entry);
fn get(entry);
}class ReadonlyCollection implements CollectionInterface
{
fn add(entry)
{
throw new Error('This collection is read-only!');
}
// ...
}Solution
interface CollectionInterface
{
fn has(entry);
fn get(key);
}interface MutableCollectionInterface extends CollectionInterface
{
fn add(entry);
}class ReadOnlyCollection implements CollectionInterface
{
fn has(entry)
{
return this.items.has(entry);
}
fn get(key)
{
return this.items[key];
}
}Dependency Inversion Principle
High-Level-Code
interface ValidatorInterface
{
fn validate(data);
}Mid-Level-Code
interface SaveProductRequestValidatorInterface
extends ValidatorInterface
{
}class SaveProductRequestValidator
implements SaveProductRequestValidatorInterface
{
fn construct(Database database);
fn validate(data)
{
// ...
}
}Low-Level-Code
Anti Pattern I
High-/Mid-Level code couples to Low-Level code
class Product extends OrmModel
{
fn getName()
{
return this.attributes.name;
}
fn setName(name)
{
this.attributes.name = name;
}
fn category()
{
return this.hasOne('category');
}
fn getCategory()
{
return this.category().getRelatedModel();
}
}interface ProductRepositoryInterface
{
fn getById(id) : Product;
}var product = this.productRepo.getById(1337);
var category = product.category().getRelatedModel();Problem of this code
- The high-level-code (repository) depends on low level code
- The Product class is tightly coupled to the ORM (so other code will become coupled)
The Dependency Inversion Principle
This principle states that high-level code should not depend on low-level code, and that abstractions should not depend upon details.
- Taylor Otwell - From Apprentice to Artisan
Solution
interface ProductInterface
{
fn getName();
fn setName();
fn getCategory() : CategoryInterface;
}interface ProductRepositoryInterface
{
fn getById(id) : ProductInterface;
}class Product extends OrmModel implements ProductInterface
{
fn getName()
{
return this.attributes.name;
}
fn setName(name)
{
return this.attributes.name = name;
}
fn getCategory() : CategoryInterface
{
return this.hasOne('category').getRelatedModel();
}
}Anti-Pattern II
Implementation is coupled to other implementations
class DatabaseProductRepository implements ProductRepositoryInterface
{
fn construct(MysqlDatabase database);
fn getById(id)
{
return this.database.find(id);
}
// ...
}Solution
class DatabaseProductRepository implements ProductRepositoryInterface
{
fn construct(DatabaseInterface database);
fn getById(id)
{
return this.database.find(id);
}
// ...
}OK cool...
... but I always forget about these principles
More SOLID code for everyone
- Always write unit tests (before writing implementations)
- Often you will recognize violations of SOLID right before
- Always take a minute after you've written a class to think about SOLID
Questions for good code
- Does my class have more than one reason to change?
- Calculations + model interaction = 2 reasons
- Do I need to alter my code to embrace changes of the environment?
- Do I have to alter dependent classes if I reimplement this?
- Is the interface I've used very explicit for the technology I've used?
- Do I really have an interface?
THANKS!
:)
SOLID
By Jan Pantel
SOLID
- 649