The document discusses SOLID principles of object-oriented programming and provides an example of refactoring a user registration class to follow these principles. Specifically, it starts with a monolithic user registration class that handles all steps of registration. It then refactors the class to separate concerns by introducing new classes for user validation, persistence, email confirmation, and more. Dependencies are injected through the constructor rather than retrieved internally. This improves modularity and makes the code easier to change and extend.
3. @jessicamauerhan | http://bit.ly/solid-oop
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 | http://bit.ly/solid-oop
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. 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. 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');
}
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. 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. 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);
}
private function emailIsRegistered(string $email): bool { [...] }
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}'));
}
}
14. 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;
}
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}'));
}
}
15. Class UserRegistration
{
public function register(Request $request): Response
{
[...]
}
private function emailIsRegistered(string $email): bool
{
$rows = DB::query(SELECT count(id) FROM users WHERE (email='{$email}'));
return $rows 0;
}
private function saveUser(User $user)
{
DB::query(INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}'));
}
}
16. 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 aclick here/a to confirm your email!;
$headers = From: mysite@email.com;
mail($email, $subject, $message, $headers);
}
17. 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 aclick 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;
}
}
18. 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 aclick 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;
}
}
19. @jessicamauerhan | http://bit.ly/solid-oop
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
20. Class UserRegistration
{
public function register(Request $request): Response { [...] }
private function sendConfirmationEmail(string $email) { [...] }
private function saveUser(User $user)
{
$sql = INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}')
DB::query($sql);
}
private function emailIsRegistered(string $email): bool
{
$rows = DB::query(SELECT count(id) FROM users WHERE (email='{$email}'));
return $rows 0;
}
}
21. Class UserRegistration
{
private $db;
public function __construct(Database $db)
{
$this-db = $db;
}
public function register(Request $request): Response { [...] }
private function sendConfirmationEmail(string $email) { [...] }
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;
}
}
22. 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);
}
private function saveUser(User $user) { [...] }
private function emailIsRegistered(string $email): bool { [...] }
59. Interface Item
{
public function getName(): string;
public function getPrice(): float;
public function getShippingWeight(): float;
public function getShippingCost(): float;
}
60. 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;
}
}
61. 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 Cart { [...] }
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;
}
}
63. 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;
}
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;
}
64. 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 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;
}
}
65. 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 BuyableItem implements Buyable { [...] }
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;
}
}
66. 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
67. @jessicamauerhan | http://bit.ly/solid-oop
SLD: Apply Often
Preemptively
● Single Responsibility
● Liskov Substitution
● Dependency Inversion
OI: Apply As Needed
When Changes Occur
● Open/Closed
● Interface Segregation
68. @jessicamauerhan | http://bit.ly/solid-oop
Thank You!
SOLID in Practice
http://bit.ly/solid-oop
Feedback Questions?
Welcome Encouraged!
@jessicamauerhan
jessicamauerhan@gmail.com
jmauerhan.wordpress.com