5 characters for better code
@JanPantel
- CouchCommerce
- Hochschule Hannover
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);
}
}Nope....
[...] 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
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;
}
}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!');
}
}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 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
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()
{
// ...
}
}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!');
}
}
}class PresenceSaveProductRequestValidator
extends SaveProductRequestValidatorInterface
{
fn construct(ProductRepository productRepo);
fn validate(data)
{
if ( ! data.has(['name', 'price']))
{
return ['Missing input attributes!'];
}
return [];
}
}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)
{
// ...
}
}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 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);
// ...
}
}[...] 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
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();
// ...
}
}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!');
}
}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
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
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!');
}
// ...
}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];
}
}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
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();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
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();
}
}Implementation is coupled to other implementations
class DatabaseProductRepository implements ProductRepositoryInterface
{
fn construct(MysqlDatabase database);
fn getById(id)
{
return this.database.find(id);
}
// ...
}class DatabaseProductRepository implements ProductRepositoryInterface
{
fn construct(DatabaseInterface database);
fn getById(id)
{
return this.database.find(id);
}
// ...
}:)