3. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
What is SOLID?
● Five principles for Object
Oriented Programming
● Guidelines which can help
ensure system is easy to
maintain
● Primarily focused on
communication between
dependencies or
collaborators
4. Let ϕ( ) be a property provable
about objects of type T.
Then ϕ( ) should be true for
objects of type S where S is a
subtype of T
5. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class Rectangle
{
protected $length;
protected $height;
public function area()
{
return $this->length * $this->area();
}
public function setLength($length)
{
$this->length = $length;
}
public function setHeight($height)
{
$this->height = $height;
}
}
Class Square extends Rectangle
{
public function setLength($length)
{
$this->length = $length;
$this->height = $length;
}
public function setHeight($height)
{
$this->length = $height;
$this->height = $height;
}
}
10. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
public function register(Request $request): Response
{
//Collect user input
if (!$request->has('email') || !$request->has('password')) {
return new Response('register', ['error' => 'Please provide an email and a password']);
}
$user = new User();
$user->email = $request->get('email');
$user->password = $request->get('password');
}
}
11. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
public function register(Request $request): Response
{
[...]
}
private function emailIsRegistered(string $email): bool
{
$dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER'];
$db = pg_connect($dsn);
$result = pg_query($db, "SELECT id FROM users WHERE (email='{$email}')");
$rows = pg_num_rows($result);
return $rows > 0;
}
}
12. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
public function register(Request $request): Response
{
//Collect user input
if (!$request->has('email') || !$request->has('password')) {
return new Response('register', ['error' => 'Please provide an email and a password']);
}
$user = new User();
$user->email = $request->get('email');
$user->password = $request->get('password');
//Validate user input
if ($this->emailIsRegistered($user->getEmail())) {
return new Response('register', ['error' => 'Your email address is already registered']);
}
}
private function emailIsRegistered(string $email): bool
{
$dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER'];
$db = pg_connect($dsn);
$result = pg_query($db, "SELECT id FROM users WHERE (email='{$email}')");
$rows = pg_num_rows($result);
return $rows > 0;
}
}
13. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
public function register(Request $request): Response
{
[...]
}
private function saveUser(User $user)
{
$dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER'];
$db = pg_connect($dsn);
pg_query($db, "INSERT INTO users(email, password) VALUES('{$user->email}', '{$user->password}')");
}
private function emailIsRegistered(string $email): bool
{
$dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER'];
$db = pg_connect($dsn);
$result = pg_query($db, "SELECT id FROM users WHERE (email='{$email}')");
$rows = pg_num_rows($result);
return $rows > 0;
}
}
14. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
public function register(Request $request): Response
{
[...]
}
private function saveUser(User $user)
{
DB::query("INSERT INTO users(email, password) VALUES('{$user->email}', '{$user->password}')");
}
private function emailIsRegistered(string $email): bool
{
$rows = DB::query("SELECT count(id) FROM users WHERE (email='{$email}')");
return $rows > 0;
}
}
15. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
public function register(Request $request): Response
{
//Collect user input
if (!$request->has('email') || !$request->has('password')) {
return new Response('register', ['error' => 'Please provide an email and a password']);
}
$user = new User();
$user->email = $request->get('email');
$user->password = $request->get('password');
//Validate user input
if ($this->emailIsRegistered($user->getEmail())) {
return new Response('register', ['error' => 'Your email address is already registered']);
}
//Persist User
$this->saveUser($user);
//Send Confirmation Email
$this->sendConfirmationEmail($user->getEmail());
return new Response('register', ['success' => 'You are registered! Please check your email!']);
}
private function sendConfirmationEmail(string $email)
{
$subject = "Confirm Email";
$message = "Please <a>click here</a> to confirm your email!";
$headers = "From: mysite@email.com";
mail($email, $subject, $message, $headers);
}
}
16. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
public function register(Request $request): Response {
//Collect user input
if (!$request->has('email') || !$request->has('password')) {
return new Response('register', ['error' => 'Please provide an email and a password']);
}
$user = new User();
$user->email = $request->get('email');
$user->password = $request->get('password');
//Validate user input
if ($this->emailIsRegistered($user->getEmail())) {
return new Response('register', ['error' => 'Your email address is already registered']);
}
//Persist User
$this->saveUser($user);
//Send Confirmation Email
$this->sendConfirmationEmail($user->getEmail());
return new Response('register', ['success' => 'You are registered! Please check your email!']);
}
private function sendConfirmationEmail(string $email) {
$subject = "Confirm Email";
$message = "Please <a>click here</a> to confirm your email!";
$headers = "From: mysite@email.com";
mail($email, $subject, $message, $headers);
}
private function saveUser(User $user) {
DB::query("INSERT INTO users(email, password) VALUES('{$user->email}', '{$user->password}')");
}
private function emailIsRegistered(string $email): bool {
$rows = DB::query("SELECT count(id) FROM users WHERE (email='{$email}')");
return $rows > 0;
}
}
17. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
public function register(Request $request): Response {
//Collect user input
if (!$request->has('email') || !$request->has('password')) {
return new Response('register', ['error' => 'Please provide an email and a password']);
}
$user = new User();
$user->email = $request->get('email');
$user->password = $request->get('password');
//Validate user input
if ($this->emailIsRegistered($user->getEmail())) {
return new Response('register', ['error' => 'Your email address is already registered']);
}
//Persist User
$this->saveUser($user);
//Send Confirmation Email
$this->sendConfirmationEmail($user->getEmail());
return new Response('register', ['success' => 'You are registered! Please check your email!']);
}
private function sendConfirmationEmail(string $email) {
$subject = "Confirm Email";
$message = "Please <a>click here</a> to confirm your email!";
$headers = "From: mysite@email.com";
mail($email, $subject, $message, $headers);
}
private function saveUser(User $user) {
DB::query("INSERT INTO users(email, password) VALUES('{$user->email}', '{$user->password}')");
}
private function emailIsRegistered(string $email): bool {
$numRows = DB::query("SELECT count(id) FROM users WHERE (email='{$email}')");
return $rows > 0;
}
}
18. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Dependency Injection
● Collaborators are supplied to
class from outside - aka
"injected" into it
Service Location
● Collaborators are retrieved or
instantiated from inside the
class using them
19. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
private $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function register(Request $request): Response
{
[...]
}
private function saveUser(User $user)
{
$sql = "INSERT INTO users(email, password) VALUES('{$user->email}', '{$user->password}')"
$this->db->query($sql);
}
private function emailIsRegistered(string $email): bool
{
$rows = $this->db->query("SELECT count(id) FROM users WHERE (email='{$email}')");
return $rows > 0;
}
20. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
private $db;
private $mailer;
public function __construct(Database $db, Mailer $mailer)
{
$this->db = $db;
$this->mailer = $mailer;
}
public function register(Request $request): Response
{
[...]
}
private function sendConfirmationEmail(string $email)
{
//Convert our email to Mandrill's email
$mandrillEmail = [
'to' => ['email' => $email->getTo()],
'from_email' => $email->getFrom(),
'subject' => $email->getSubject(),
'text' => $email->getMessage()
];
$this->mandrill->send($mandrillEmail);
}
48. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class PercentageDiscount
{
private $discount;
public function __construct(int $discount)
{
$this->discount = $discount;
}
public function calculateAmountOff(Item $item)
{
return ($this->discount / 100) * $item->getCost();
}
}
Class BuyItemsToGetDiscount extends PercentageDiscount
{
private $type;
private $countItemsRequired = 2;
public function setEligibleType(string $type)
{
$this->type = $type;
}
public function calculateAmountOff(Item $item)
{
if ($this->hasEnoughItems() && $item->isType($this->type)) {
return ($this->discount / 100) * $item->getCost();
}
throw new NotEnoughItems();
}
public function hasEnoughItems(array $items): bool
{
$found = 0;
foreach ($items AS $item) {
if ($item->isType($this->type)) {
$found++;
}
if ($found == $this->countItemsRequired) {
return true;
}
}
return false;
}
}
49. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class BuyItemsToGetDiscount extends PercentageDiscount
{
private $type;
private $countItemsRequired = 2;
public function __construct($discount, $type)
{
$this->type = $type;
parent::__construct($discount);
}
public function calculateAmountOff(Item $item)
{
if ($this->hasEnoughItems() && $item->isType($this->type)) {
return ($this->discount / 100) * $item->getCost();
}
return 0;
}
public function hasEnoughItems(array $items): bool
{
$found = 0;
foreach ($items AS $item) {
if ($item->isType($this->type)) {
$found++;
}
if ($found == $this->countItemsRequired) {
return true;
}
}
return false;
}
}
Class PercentageDiscount
{
private $discount;
public function __construct(int $discount)
{
$this->discount = $discount;
}
public function calculateAmountOff(Item $item)
{
return ($this->discount / 100) * $item->getCost();
}
}
50. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Interface Discount
{
/** @throws DiscountException */
public function calculateAmountOff(Item $item): float;
}
Class PercentageDiscount implements Discount
{
private $discount;
public function __construct(int $discount)
{
$this->discount = $discount;
}
public function calculateAmountOff(Item $item): float
{
return ($this->discount / 100) * $item->getCost();
}
}
Class BuyEligibleItemsToGetDiscount extends PercentageDiscount
{
public function calculateAmountOff(Item $item): float { [...] }
}
51. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Interface Item
{
public function getName(): string;
public function getPrice(): float;
public function getShippingWeight(): float;
public function getShippingCost(): float;
}
class Cart
{
private $items;
private $total;
public function addItem(Item $item)
{
$this->items[] = $item;
}
public function calculateTotal()
{
$subtotal = 0;
$shippingTotal = 0;
foreach ($this->items AS $item) {
$subtotal += $item->getPrice();
$shippingTotal += $item->getShippingCost();
}
return $subtotal + $shippingTotal;
}
}
52. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Interface Item
{
public function getName(): string;
public function getPrice(): float;
public function getShippingWeight(): float;
public function getShippingCost(): float;
public function getWorkerAssigned(): Worker;
public function assignWorker(Worker $worker);
public function getServiceScheduledDate(): DateTime;
public function getServiceCompletedDate(): DateTime;
public function isCompleted(): bool;
}
Class ServiceManager
{
private $services;
public function addService(Item $item)
{
$this->services[] = $item;
}
public function getCompletedServices()
{
$completed = [];
foreach ($this->services AS $service) {
if ($service->isCompleted()) {
$completed[] = $service;
}
}
return $completed;
}
}
54. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Interface Buyable
{
public function getName(): string;
public function getPrice(): float;
}
Interface Product
{
public function getShippingWeight(): float;
public function getShippingCost(): float;
}
Interface Service
{
public function getWorkerAssigned(): Worker;
public function assignWorker(Worker $worker);
public function getServiceScheduledDate(): DateTime;
public function getServiceCompletedDate(): DateTime;
public function isCompleted(): bool;
}
class Cart
{
private $products;
public function addItem(Product $product){
$this->products[] = $product;
}
public function calculateTotal(){
$total = 0;
foreach ($this->products AS $p) {
$total += $p->getPrice() + $p->getShippingCost();
}
return $total;
}
}
Class ServiceManager
{
private $services;
public function addService(Service $service){
$this->services[] = $service;
}
public function getCompletedServices(){
$completed = [];
foreach ($this->services AS $service){
if ($service->isCompleted()) {
$completed[] = $service;
}
}
return $completed;
}
}
Class BuyableItem implements Buyable
{
protected $name, $price;
public function __construct(string $name, float $price) {
$this->name = $name;
$this->price = $price;
}
public function getName(): string {
return $this->name;
}
public function getPrice(): float {
return $this->price;
}
}
Class BuyableProduct extends BuyableItem implements Product
{
protected $shippingCost, $shippingWeight;
public function __construct(string $name, float $price,
float $shippingCost, float $shippingWeight) {
$this->shippingCost = $shippingCost;
$this->shippingWeight = $shippingWeight;
parent::__construct($name, $price);
}
public function getShippingCost(): float {
return $this->getShippingCost();
}
public function getShippingWeight(): float {
return $this->shippingWeight;
}
}
55. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
S.O.L.I.D.
● Single Responsibility - Registration Class delegating to Collaborators
● Open/Closed - Adding new Notifiers without always editing Listener
● Liskov Substitution - Ensured Discount subtypes could be used as Discount
● Interface Segregation - Items use Buyable, Product and Service interfaces
● Dependency Inversion - Registration Controller, Emailer, Shopping Cart,
Notification Factory - all use interfaces, not implementations
56. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
SLD: Apply Often
& Preemptively
● Single Responsibility
● Liskov Substitution
● Dependency Inversion
OI: Apply As Needed
When Changes Occur
● Open/Closed
● Interface Segregation
57. @jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Thank You!
SOLID in Practice
Feedback & Questions?
Welcome & Encouraged!
@jessicamauerhan
jessicamauerhan@gmail.com
jmauerhan.wordpress.com