FROM STUPID TO SOLID CODE
IN MODERN EXAMPLES
PRESENTED BY: TOMASZ BANASIAK
WHO AM I?
WHY DO WE NEED SOFTWARE ARCHITECTURE?
https://abstracthdwallpapers.wordpress.com/2012/07/
SOFTWARE ARCHITECTURE
-
LARGE PROJECTS
-
MANY COOPERATING TEAMS
-
NEW TEAMMATES
-
CODE RE-USE
WHAT MAKES CODE STUPID?
STUPID CODE
-
Singleton
-
Tight Coupling
-
Untestability
-
Premature Optimization
-
Indescriptive Naming
-
Duplication
STUPID ...REFACTORING
public function register() {
$user = DBORM::factory('user');
$user->exchangeArray($this->input->post('user', Input::TYPE_PREFIX));
$user->save();
$userId = $user->id;
switch($this->input->post('type')) {
case self::ACCOUNT_TYPE_SELLER:
$shop = new Shop($userId, $userEmail);
$user->addShop($shop);
break;
case self::ACCOUNT_TYPE_CUSTOMER:
case self::ACCOUNT_TYPE_CLIENT:
$dbAccounts = Module_Account_Manager::getInstance();
$dbAccounts->create($user);
break;
default:
throw new \Exception('Unknown user type!');
break;
}
// ...
}
STUPID ...REFACTORING
public function register() {
$user = DBORM::factory('user');
$user->exchangeArray($this->input->post('user', Input::TYPE_PREFIX));
$user->save();
$userId = $user->id;
$this->registerUserType($this->input->post('type'))
}
public function registerUserType($type) {
switch($type) {
case self::ACCOUNT_TYPE_SELLER:
$shop = new Shop($userId, $userEmail);
$user->addShop($shop);
break;
case self::ACCOUNT_TYPE_CUSTOMER:
case self::ACCOUNT_TYPE_CLIENT:
$dbAccounts = Module_Account_Manager::getInstance();
$dbAccounts->create($user);
break;
// ...
}
// ...
}
SOLID CODE
http://www.lifeofpix.com/
SINGLE RESPONSIBILITY PRINCIPLE
SOLID
ONE REASON TO CHANGE
OBJECT/CLASS SHOULD HAVE ONE AND
ONLY ONE REASON TO CHANGE
https://github.com/PrestaShop/PrestaShop-1.5/blob/master/controllers/front/OrderController.php
public function processOrder()
{
if (!Tools::getConfig('shop-shopping-available'))
$this->context->cart->disableCart();
if (!Customer::customerHasAddress($this->context->customer->id, (int)Tools::getValue('id_address_delivery'))
|| (!$same && Tools::getValue('id_address_delivery') != Tools::getValue('id_address_invoice')
&& !Customer::customerHasAddress($this->context->customer->id, (int)Tools::getValue('id_address_invoice'))))
$this->errors[] = Tools::displayError('Invalid address', !Tools::getValue('ajax'));
else
{
$this->context->cart->id_address_delivery = (int)Tools::getValue('id_address_delivery');
$this->context->cart->id_address_invoice = (int)Tools::getValue('id_address_invoice');
CartRule::lockCartProducts($this->context, strtotime('+3hours'));
CartRule::autoAddToCart($this->context);
if (!$this->context->cart->isMultiAddressDelivery())
$this->context->cart->setNoMultishipping();
// Add checking for all addresses
$address_without_carriers = $this->context->cart->getDeliveryAddressesWithoutCarriers();
if (count($address_without_carriers) > 1)
$this->errors[] = sprintf(Tools::displayError('No delivery to some addresses you selected.'));
else
$this->errors[] = sprintf(Tools::displayError('No delivery to the address you selected.'));
}
foreach($this->context->cart->items as $item)
$cartSum += $item['price'];
// ...
class ProductsManager {
public function lock(ProductsCollection $collection);
public function unlock(ProductsCollection $collection);
}
class ProductsCollection extends Collection {
// Collection of simple product entities (eg just ID's)
public function sum() {};
}
class UserAddressCollection extends Collection {
// Collection of UserAddresses
}
class UserAdress extends Address {
public function isOwner($userId) {};
}
class Cart {
protected $products; // ProductsCollection
public function preCheckout() {}
public function postCheckout() {}
}
public function processOrder(Cart $cart, ProductsManager $productsManager)
{
$order = new Order();
try {
$deliveryAddress = new UserAdress((int) Tools::getValue('id_address_delivery'));
$invoiceAddress = new UserAddress((int) Tools::getValue('id_address_invoice'));
if ($deliveryAddress->isOwner($this->context->customer->id)
&& $invoiceAddress->isOwner($this->context->customer->id) {
$order->setAddresses($deliveryAddress, $invoiceAddress);
}
$cart->preCheckout(); // Throw an event or execute list of actions
$productsManager->lock($cart->getProducts());
$order->addProducts($cart->getProducts());
$order->save();
$cart->postCheckout(); // Clean up cart etc
} catch (\Exception $e) {
$productsManager->unlock($cart->getProducts());
$this->errors[] = 'Cannot save order: ' . $e->getMessage();
}
}
SINGLE RESPONSIBILITY - CLUES
-
MORE THAN ONE REASON TO CHANGE
-
NAMING PROBLEMS
-
MANY LOC PER METHOD/CLASS
-
HARD TO READ, HARD TO UNDERSTAND
-
NEED TO BE MODIFIED BEFORE TESTING
OPEN/CLOSED PRINCIPLE
SOLID
BE AWARE OF MY BEHAVIOUR, BE UNCONSICIOUS OF MY GEARS
OBJECT/CLASS SHOULD BE EXTENDABLE
WITHOUT INTERNAL MODIFICATIONS
class PaymentHandler extends DefaultHandler {
public function __construct(array $config) {
$this->config = $config;
}
public function handlePayment(Order $order) {
switch($order->getPayment()->getType()) {
case self::PAYMENT_PAYPAL:
$this->executePaypal($order);
break;
// ...
}
}
public function executePaypal(Order $order) {
$payment = new PaypalPayment($this->config['payment']['paypal']);
$payment->amount($order->getProducts()->getSum());
$payment->currency($order->getPayment()->getCurrency());
$payment->execute();
}
// ..
}
class PaymentHandler extends DefaultHandler {
private $paymentHandlers;
public function __construct(PaymentHandlersCollection $paymentHandlers) {
$this->paymentHandlers = $paymentHandlers;
}
public function handlePayment(Order $order) {
$handler = $this->paymentHandlers->get($order->getPayment()->getType());
$handler->execute($order);
}
}
class PaymentHandlersCollection extends Collection {
public function get($type) {
if (isset($this->collection[$type])) {
return $this->collection[$type];
} else throw new \Exception('PaymentHandler: unknown handler ' . $type);
}
public function add(PaymentHandlerInterface $paymentHandler) {
$this->collection[] = $paymentHandler;
}
}
class PaymentHandlerFactory extends DefaultFactory {
public function createService(ServiceLocatorInterface $serviceLocator)
{
$config = $serviceLocator->get('config');
$paymentServicePaypal = $serviceLocator->get(PaypalHandler::class);
$paymentServicePaypal->configure($config['payment']['paypal']);
$handlers = (new PaymentHandlersCollection())
->add($paymentServicePaypal);
return new PaymentHandler($handlers);
}
}
class PaymentInterface {
public function configure(array $config) {}
public function execute(Order $order) {}
}
LISKOV'S SUBSTITUTION PRINCIPLE
SOLID
TRUST ME, IM NOT WHAT IT LOOKS!
EXTENDED CLASSES SHOULD BE REPLACEABLE
BY TheIR BASE CLASSES
interface UserAwareInterface {
/**
* @return User
* @throws UserNotFoundException
*/
public function getUser();
}
class CurrentUser implements UserAwareInterface {
public function getUser() {
if ($this->user) {
$user = $this->repository->getUserById($identity->userId);
if (!$user) {
throw new AuthException('User is not logged in or does not exist');
}
$this->user = $user;
}
return $this->user;
}
}
class User implements UserAwareInterface {
public function __construct($userId) {
$this->userId = $userId;
}
public function getUser() {
$user = $this->repository->getUserById($this->userId);
return $user ? $user : null;
}
}
class CurrentUser implements UserAwareInterface {
public function getUser() {
if ($this->user) {
$user = $this->repository->getUserById($identity->userId);
if (!$user) {
throw new UserNotFoundException('User is not logged in or does not exist');
}
$this->user = $user;
}
return $this->user;
}
}
class User implements UserAwareInterface {
public function __construct($userId) {
$this->userId = $userId;
}
public function getUser() {
$user = $this->repository->getUserById($this->userId);
if (is_null($user)) {
throw new UserNotFoundException('Cannot find user with ID ' . $this->userId);
}
return $user;
}
}
INTERFACE SEGREGATION PRINCIPLE
SOLID
JACK OF ALL TRADES, MASTER OF NONE
IT"S BETTER TO HAVE MANY USEFUL INTERFACES INSTEAD OF COMPLICATED ONE
class DatabaseManager {
private $databases = array();
public function getStorage($type) {}
}
class DatabaseInterface {
public function findById($id) {}
public function execute($stmt) {}
public function join(\DatabaseTable $table) {}
}
class MysqlDatabase implements DatabaseInterface {
public function findById($id) {}
public function execute($stmt) {}
public function join(\DatabaseTable $table) {}
}
class MongoDatabase {
public function findById($id) {} // works
public function execute($stmt) {
throw new \InvalidMethodException('Method execute does not exist in MongoDB!');
}
// Requires a collection instead of table
public function join(\DatabaseTable $table) {}
}
DEPENDENCY INVERSION PRINCIPLE
SOLID
ASK FOR INTERFACE, NOT AN IMPLEMENTATION
DEPENDENCY SHOULD RELY ON INTERFACE
NOT AN IMPLEMENTATION
class Notification {
public function __construct(MandrillProvider $mailClient, $config) {
$this->mailClient = $mailClient
->setLogin($config['login'])
->setSecret($config['secret'])
->setMessageType(MandrillClient::CONTENT_TYPE_HTML);
}
public function send($message, $title, $recipient) {
$this->mailClient->send($message, $title, $recipient);
}
}
interface Message {
private $subject;
private $message;
private $type;
}
interface MailProvider {
public function __construct($config) {}
public function send(Message $message, Recipients $recipients) {}
}
class MandrillMail extends MailProvider {
public function __construct($config) {
$this->client = $mailClient
->setLogin($config['login'])
->setSecret($config['secret'])
->setMessageType(MandrillClient::CONTENT_TYPE_HTML);
}
public function send(Message $message, Recipients $recipients) {
$this->client->send(
$message->getMessage(),
$message->getTitle(),
$recipients->getArrayCopy()
);
}
}
SUMMARY
SOLID
ENTRANCE FOR WORLD OF SOFTWARE ARCHITECTURE
QUESTIONS?
SOLID
DON'T BE AFRAID TO ASK :)
INSPIRATIONS & MORE
-
Daniel Nahrebecki's Internal Presentation
-
Robert C. "Uncle Bob" Martin's BLOG
-
CLEAN CODE - BOOK
-
Domain Driven Design
THANK YOU!
TOMASZ BANASIAK
TOMASZ.BANASIAK@RST.COM.PL
HTTP://BANASIAK.PRO
RST.COM.PL/KARIERA
From STUPID to SOLID code in modern examples
By Tomasz Banasiak
From STUPID to SOLID code in modern examples
- 2,365