SOLID In Practice
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
@jessicamauerhan
Senior Software Engineer
TDD/BDD Expert
jessicamauerhan@gmail.com
jmauerhan.wordpress.com
@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
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
@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;
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
S.O.L.I.D.
● Single Responsibility
● Open/Closed
● Liskov Substitution
● Interface Segregation
● Dependency Inversion
It's All About Change
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
User Registration
● Collect User Input
● Validate User Input
● Persist User Data
● Send Confirmation Email
@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');
}
}
@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;
}
}
@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;
}
}
@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;
}
}
@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;
}
}
@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);
}
}
@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;
}
}
@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;
}
}
@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
@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;
}
@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);
}
Dependency Inversion
Single Responsibility Principle
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
private $factory;
public function __construct(UserFactory $factory)
{
$this->factory = $factory;
}
public function register(Request $request): Response
{
try {
$user = $this->factory->createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception->getErrors());
}
}
}
Class UserRegistration
{
private $factory, $validator;
public function __construct(UserFactory $factory, UserValidator $validator)
{
$this->factory = $factory;
$this->validator = $validator;
}
public function register(Request $request): Response
{
try {
$user = $this->factory->createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception->getErrors());
}
if ($this->validator->isValid($user) === false) {
$invalidProperties = $this->validator
->getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
}
}
Class UserRegistration
{
private $factory, $validator, $repository;
public function __construct(UserFactory $factory, UserValidator $validator,
UserRepository $repository)
{
$this->factory = $factory;
$this->validator = $validator;
$this->repository = $repository;
}
public function register(Request $request): Response
{
try {
$user = $this->factory->createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception->getErrors());
}
if ($this->validator->isValid($user) === false) {
$invalidProperties = $this->validator
->getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
try {
$this->repository->create($user);
return UserCreatedResponse($user);
} catch (UnableToCreateUser $exception) {
return UnableToCreateUserResponse($user);
}
}
}
Class UserRegistration
{
private $factory, $validator, $repository, $emailer;
public function __construct(UserFactory $factory, UserValidator $validator,
UserRepository $repository, ConfirmationEmailer $emailer)
{
$this->factory = $factory;
$this->validator = $validator;
$this->repository = $repository;
$this->emailer = $emailer;
}
public function register(Request $request): Response
{
try {
$user = $this->factory->createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception->getErrors());
}
if ($this->validator->isValid($user) === false) {
$invalidProperties = $this->validator
->getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
try {
$this->repository->create($user);
$this->emailer->sendConfirmationEmail($user);
return UserCreatedResponse($user);
} catch (UnableToCreateUser $exception) {
return UnableToCreateUserResponse($user);
} catch (MailerException $mailerException) {
return UnableToSendConfirmationEmailResponse($user);
}
}
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createRegisteringUser(Request $request): User;
}
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
interface UserRepository
{
/**
* @throws UnableToCreateUser
*/
public function create(User $user): bool;
}
interface ConfirmationEmailer
{
/**
* @throws MailerException
*/
public function send(string $to);
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class UserRegistration
{
private $factory;
public function __construct(UserFactory $factory)
{
$this->factory = $factory;
}
public function register(Request $request): Response
{
try {
$user = $this->factory->createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception->getErrors());
}
}
}
Class UserRegistration
{
private $factory, $validator;
public function __construct(UserFactory $factory, UserValidator $validator)
{
$this->factory = $factory;
$this->validator = $validator;
}
public function register(Request $request): Response
{
try {
$user = $this->factory->createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception->getErrors());
}
if ($this->validator->isValid($user) === false) {
$invalidProperties = $this->validator
->getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
}
}
Class UserRegistration
{
private $factory, $validator, $repository;
public function __construct(UserFactory $factory, UserValidator $validator,
UserRepository $repository)
{
$this->factory = $factory;
$this->validator = $validator;
$this->repository = $repository;
}
public function register(Request $request): Response
{
try {
$user = $this->factory->createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception->getErrors());
}
if ($this->validator->isValid($user) === false) {
$invalidProperties = $this->validator
->getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
try {
$this->repository->create($user);
return UserCreatedResponse($user);
} catch (UnableToCreateUser $exception) {
return UnableToCreateUserResponse($user);
}
}
}
Class UserRegistration
{
private $factory, $validator, $repository, $emailer;
public function __construct(UserFactory $factory, UserValidator $validator,
UserRepository $repository, ConfirmationEmailer $emailer)
{
$this->factory = $factory;
$this->validator = $validator;
$this->repository = $repository;
$this->emailer = $emailer;
}
public function register(Request $request): Response
{
try {
$user = $this->factory->createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception->getErrors());
}
if ($this->validator->isValid($user) === false) {
$invalidProperties = $this->validator
->getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
try {
$this->repository->create($user);
$this->emailer->sendConfirmationEmail($user);
return UserCreatedResponse($user);
} catch (UnableToCreateUser $exception) {
return UnableToCreateUserResponse($user);
} catch (MailerException $mailerException) {
return UnableToSendConfirmationEmailResponse($user);
}
}
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createRegisteringUser(Request $request): User;
}
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
interface UserRepository
{
/**
* @throws UnableToCreateUser
*/
public function create(User $user): bool;
}
interface ConfirmationEmailer
{
/**
* @throws MailerException
*/
public function send(string $to);
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createUser(Request $request): User;
}
class RegisterFormUserFactory implements UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createUser(Request $request): User
{
if(!$request->has('email') || !$request->has('password')){
throw new UnableToParseUser();
}
$email = $request->get('email');
$password = $request->get('password');
return new User($email, $password);
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
class NewUserValidator implements UserValidator
{
/** @var UserRepository */
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function isValid(User $user): bool
{
$emailExists = $this->userRepository->emailExists($user->getEmail());
if ($emailExists === true) {
return false;
}
return true;
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
interface UserRepository
{
/**
* @param User $user
* @return bool
*
* @throws UnableToCreateUser
*/
public function create(User $user): bool;
/**
* @param string $email
* @return bool
*
* @throws DataSourceError
*/
public function emailExists(string $email): bool;
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
class PdoUserRepository implements UserRepository
{
/** @var Pdo */
private $pdo;
public function __construct(Pdo $pdo) {
$this->pdo = $pdo;
}
public function create(User $user): bool {
try {
$statement = $this->pdo->prepare("INSERT INTO users(email, password) VALUES(:email, :password)");
$statement->execute(['email' => $user->getEmail(), 'password' => $user->getPassword()]);
} catch (PDOException $PDOException) {
throw new UnableToCreateUser($user->getEmail());
}
return true;
}
public function emailExists(string $email): bool {
try {
$statement = $this->pdo->prepare("SELECT count(id) AS numRows FROM users WHERE email = :email");
$statement->execute([email => $email]);
$result = $statement->fetchColumn();
return ($result > 0);
} catch (PDOException $PDOException) {
throw new DataSourceError();
}
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
interface ConfirmationEmailer
{
/**
* @throws MailerException
*/
public function send(string $to);
}
class MandrillConfirmationEmailer implements ConfirmationEmailer
{
private $mandrill;
public function __construct(Mandrill $mandrill)
{
$this->mandrill = $mandrill;
}
public function send(User $user)
{
$message = "Please <a>click here</a> to confirm your account.";
$from = "app@example.com";
$subject = "Confirm your account";
$email = [
'to' => [['EmailTemplate' => $user->getEmail()]],
'from_email' => $from,
'subject' => $subject,
'text' => $message
];
$this->mandrill->send($email);
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
interface Mailer
{
public function send(Email $email);
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
interface Mailer
{
public function send(Email $email);
}
class Email
{
private $to, $from, $subject, $message;
public function __construct(string $to, string $from,
string $subject, string $message) {
$this->to = $to;
$this->from = $from;
$this->subject = $subject;
$this->message = $message;
}
public function getTo(): string {
return $this->to;
}
public function getFrom(): string {
return $this->from;
}
public function getSubject(): string {
return $this->subject;
}
public function getMessage(): string {
return $this->message;
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
interface Mailer
{
public function send(Email $email);
}
class Email
{
private $to, $from, $subject, $message;
public function __construct(string $to, string $from,
string $subject, string $message) {
$this->to = $to;
$this->from = $from;
$this->subject = $subject;
$this->message = $message;
}
public function getTo(): string {
return $this->to;
}
public function getFrom(): string {
return $this->from;
}
public function getSubject(): string {
return $this->subject;
}
public function getMessage(): string {
return $this->message;
}
}
class ConfirmationEmailService implements ConfirmationEmailer
{
private $mailer;
private $from = 'mysite@example.com';
private $subject = 'Confirm Email';
private $message = 'Please click here to confirm your email';
public function __construct(Mailer $mailer) {
$this->mailer = $mailer;
}
public function send(string $to) {
$email = new Email($to, $this->from,
$this->subject, $this->message);
$this->mailer->send($email);
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
interface Mailer
{
public function send(Email $email);
}
class Email
{
private $to, $from, $subject, $message;
public function __construct(string $to, string $from,
string $subject, string $message) {
$this->to = $to;
$this->from = $from;
$this->subject = $subject;
$this->message = $message;
}
public function getTo(): string {
return $this->to;
}
public function getFrom(): string {
return $this->from;
}
public function getSubject(): string {
return $this->subject;
}
public function getMessage(): string {
return $this->message;
}
}
class ConfirmationEmailService implements ConfirmationEmailer
{
private $mailer;
private $from = 'mysite@example.com';
private $subject = 'Confirm Email';
private $message = 'Please click here to confirm your email';
public function __construct(Mailer $mailer) {
$this->mailer = $mailer;
}
public function send(string $to) {
$email = new Email($to, $this->from,
$this->subject, $this->message);
$this->mailer->send($email);
}
}
class MandrillMailer implements Mailer
{
private $mandrill;
public function __construct(Mandrill $mandrill) {
$this->mandrill = $mandrill;
}
public function send(Email $email) {
//Convert our email to Mandrill's email
$mandrillMail = ['to' => ['email' => $email->getTo()],
'from_email' => $email->getFrom(),
'subject' => $email->getSubject(),
'text' => $email->getMessage()];
$this->mandrill->send($mandrillEmail);
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
interface Mailer
{
public function send(Email $email);
}
class Email
{
private $to, $from, $subject, $message;
public function __construct(string $to, string $from,
string $subject, string $message) {
$this->to = $to;
$this->from = $from;
$this->subject = $subject;
$this->message = $message;
}
public function getTo(): string {
return $this->to;
}
public function getFrom(): string {
return $this->from;
}
public function getSubject(): string {
return $this->subject;
}
public function getMessage(): string {
return $this->message;
}
}
class ConfirmationEmailService implements ConfirmationEmailer
{
private $mailer;
private $from = 'mysite@example.com';
private $subject = 'Confirm Email';
private $message = 'Please click here to confirm your email';
public function __construct(Mailer $mailer) {
$this->mailer = $mailer;
}
public function send(string $to) {
$email = new Email($to, $this->from,
$this->subject, $this->message);
$this->mailer->send($email);
}
}
class SendGridMailer implements Mailer
{
private $sendgrid;
public function __construct(SendGrid $sendgrid) {
$this->sedngrid = $sendgrid;
}
public function send(Email $email) {
//Convert our email to SendGrid's email
$sgEmail = new SendGridEmail($email->getTo(),
$email->getFrom(),
$email->getSubject(),
$email->getMessage());
$this->sendgrid->queue($sgEmail);
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
class AccountOverdueListener
{
private $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function handle(OverdueEvent $event)
{
$account = $event->getAccount();
$email = new OverdueEmail($account->getEmailAddress(), $account->getAmountDue());
$this->mailer->send($email);
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
class AccountOverdueListener
{
private $mailer;
private $texter;
public function __construct(Mailer $mailer, Texter $texter)
{
$this->mailer = $mailer;
$this->texter = $texter;
}
public function handle(OverdueEvent $event)
{
$account = $event->getAccount();
$notifications = $account->getEnabledNotifications();
foreach ($notifications AS $notificationType) {
if ($notificationType === 'email') {
$email = new OverdueEmail($account->getEmailAddress(), $account->getAmountDue());
$this->mailer->send($email);
} elseif ($notificationType === 'text') {
$text = new OverdueText($account->getPhoneNumber(), $account->getAmountDue());
$this->texter->send($text);
}
}
}
}
Open/Closed Principle
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Interface Notifier
{
public function notify(ContactInfo $contact);
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Interface Notifier
{
public function notify(ContactInfo $contact);
}
Class EmailNotifier implements Notifier
{
private $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function notify(ContactInfo $contact)
{
$email = new OverdueEmail($contact->getEmailAddress());
$this->mailer->send($email);
}
}
Class TextNotifier implements Notifier
{
private $texter;
public function __construct(Texter $texter)
{
$this->texter = $texter;
}
public function notify(ContactInfo $contact)
{
$text = 'Your Account Is Overdue!';
$this->texter->send($text, $contact->getPhoneNumber());
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Interface Notifier
{
public function notify(ContactInfo $contact);
}
Class NotifierFactory
{
private $notifiers;
public function register($name, Notifier $notifier)
{
$this->notifiers[$name] = $notifier;
}
public function getNotifier(string $name): Notifier
{
return $this->notifiers[$name];
}
}
Class EmailNotifier implements Notifier
{
private $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function notify(ContactInfo $contact)
{
$email = new OverdueEmail($contact->getEmailAddress());
$this->mailer->send($email);
}
}
Class TextNotifier implements Notifier
{
private $texter;
public function __construct(Texter $texter)
{
$this->texter = $texter;
}
public function notify(ContactInfo $contact)
{
$text = 'Your Account Is Overdue!';
$this->texter->send($text, $contact->getPhoneNumber());
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
class AccountOverdueListener
{
private $notifierFactory;
public function __construct(NotifierFactory $notifierFactory)
{
$this->notifierFactory = $notifierFactory;
}
public function handle(OverdueEvent $event)
{
$account = $event->getAccount();
$contact = $account->getContactInfo();
$notifications = $account->getEnabledNotifications();
foreach ($notifications AS $notificationType) {
$notifier = $this->notifierFactory->getNotifier($notificationType);
$notifier->send($contact);
}
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
class AccountOverdueEmailListener
{
public function handle(Mailer $mailer, OverdueEvent $event)
{
$account = $event->getAccount();
$email = new OverdueEmail($account->getEmailAddress(), $account->getAmountDue());
$mailer->send($email);
}
}
class AccountOverdueTextListener
{
public function handle(Texter $texter, OverdueEvent $event)
{
$account = $event->getAccount();
$text = new OverdueText($account->getPhoneNumber(), $account->getAmountDue());
$texter->send($text);
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class PercentageDiscount
{
private $discount;
public function __construct(int $discount)
{
$this->discount = $discount;
}
public function calculateAmountOff(Item $item): float
{
return ($this->discount / 100) * $item->getCost();
}
}
@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();
}
return 'Error: Not enough items';
}
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;
}
}
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Class PercentageDiscount
{
private $discount;
public function __construct(int $discount)
{
$this->discount = $discount;
}
public function calculateAmountOff(Item $item): float
{
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): float
{
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;
}
}
Liskov Substitution Principle
@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;
}
}
@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();
}
}
@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 { [...] }
}
@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;
}
}
@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;
}
}
Interface Segregation
@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;
}
}
@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
@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
@jessicamauerhan | https://joind.in/talk/17a66 | @OpenWest
Thank You!
SOLID in Practice
Feedback & Questions?
Welcome & Encouraged!
@jessicamauerhan
jessicamauerhan@gmail.com
jmauerhan.wordpress.com

SOLID in Practice

  • 1.
    SOLID In Practice @jessicamauerhan| https://joind.in/talk/17a66 | @OpenWest
  • 2.
    @jessicamauerhan Senior Software Engineer TDD/BDDExpert jessicamauerhan@gmail.com jmauerhan.wordpress.com
  • 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; } }
  • 7.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest S.O.L.I.D. ● Single Responsibility ● Open/Closed ● Liskov Substitution ● Interface Segregation ● Dependency Inversion
  • 8.
  • 9.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest User Registration ● Collect User Input ● Validate User Input ● Persist User Data ● Send Confirmation Email
  • 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); }
  • 21.
  • 22.
  • 23.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest Class UserRegistration { private $factory; public function __construct(UserFactory $factory) { $this->factory = $factory; } public function register(Request $request): Response { try { $user = $this->factory->createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception->getErrors()); } } } Class UserRegistration { private $factory, $validator; public function __construct(UserFactory $factory, UserValidator $validator) { $this->factory = $factory; $this->validator = $validator; } public function register(Request $request): Response { try { $user = $this->factory->createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception->getErrors()); } if ($this->validator->isValid($user) === false) { $invalidProperties = $this->validator ->getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } } } Class UserRegistration { private $factory, $validator, $repository; public function __construct(UserFactory $factory, UserValidator $validator, UserRepository $repository) { $this->factory = $factory; $this->validator = $validator; $this->repository = $repository; } public function register(Request $request): Response { try { $user = $this->factory->createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception->getErrors()); } if ($this->validator->isValid($user) === false) { $invalidProperties = $this->validator ->getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } try { $this->repository->create($user); return UserCreatedResponse($user); } catch (UnableToCreateUser $exception) { return UnableToCreateUserResponse($user); } } } Class UserRegistration { private $factory, $validator, $repository, $emailer; public function __construct(UserFactory $factory, UserValidator $validator, UserRepository $repository, ConfirmationEmailer $emailer) { $this->factory = $factory; $this->validator = $validator; $this->repository = $repository; $this->emailer = $emailer; } public function register(Request $request): Response { try { $user = $this->factory->createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception->getErrors()); } if ($this->validator->isValid($user) === false) { $invalidProperties = $this->validator ->getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } try { $this->repository->create($user); $this->emailer->sendConfirmationEmail($user); return UserCreatedResponse($user); } catch (UnableToCreateUser $exception) { return UnableToCreateUserResponse($user); } catch (MailerException $mailerException) { return UnableToSendConfirmationEmailResponse($user); } } } interface UserFactory { /** * @throws UnableToParseUser */ public function createRegisteringUser(Request $request): User; } interface UserValidator { public function isValid(User $user): bool; public function getInvalidProperties(User $user):array; } interface UserRepository { /** * @throws UnableToCreateUser */ public function create(User $user): bool; } interface ConfirmationEmailer { /** * @throws MailerException */ public function send(string $to); }
  • 24.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest Class UserRegistration { private $factory; public function __construct(UserFactory $factory) { $this->factory = $factory; } public function register(Request $request): Response { try { $user = $this->factory->createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception->getErrors()); } } } Class UserRegistration { private $factory, $validator; public function __construct(UserFactory $factory, UserValidator $validator) { $this->factory = $factory; $this->validator = $validator; } public function register(Request $request): Response { try { $user = $this->factory->createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception->getErrors()); } if ($this->validator->isValid($user) === false) { $invalidProperties = $this->validator ->getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } } } Class UserRegistration { private $factory, $validator, $repository; public function __construct(UserFactory $factory, UserValidator $validator, UserRepository $repository) { $this->factory = $factory; $this->validator = $validator; $this->repository = $repository; } public function register(Request $request): Response { try { $user = $this->factory->createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception->getErrors()); } if ($this->validator->isValid($user) === false) { $invalidProperties = $this->validator ->getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } try { $this->repository->create($user); return UserCreatedResponse($user); } catch (UnableToCreateUser $exception) { return UnableToCreateUserResponse($user); } } } Class UserRegistration { private $factory, $validator, $repository, $emailer; public function __construct(UserFactory $factory, UserValidator $validator, UserRepository $repository, ConfirmationEmailer $emailer) { $this->factory = $factory; $this->validator = $validator; $this->repository = $repository; $this->emailer = $emailer; } public function register(Request $request): Response { try { $user = $this->factory->createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception->getErrors()); } if ($this->validator->isValid($user) === false) { $invalidProperties = $this->validator ->getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } try { $this->repository->create($user); $this->emailer->sendConfirmationEmail($user); return UserCreatedResponse($user); } catch (UnableToCreateUser $exception) { return UnableToCreateUserResponse($user); } catch (MailerException $mailerException) { return UnableToSendConfirmationEmailResponse($user); } } } interface UserFactory { /** * @throws UnableToParseUser */ public function createRegisteringUser(Request $request): User; } interface UserValidator { public function isValid(User $user): bool; public function getInvalidProperties(User $user):array; } interface UserRepository { /** * @throws UnableToCreateUser */ public function create(User $user): bool; } interface ConfirmationEmailer { /** * @throws MailerException */ public function send(string $to); }
  • 25.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest interface UserFactory { /** * @throws UnableToParseUser */ public function createUser(Request $request): User; } class RegisterFormUserFactory implements UserFactory { /** * @throws UnableToParseUser */ public function createUser(Request $request): User { if(!$request->has('email') || !$request->has('password')){ throw new UnableToParseUser(); } $email = $request->get('email'); $password = $request->get('password'); return new User($email, $password); } }
  • 26.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest interface UserValidator { public function isValid(User $user): bool; public function getInvalidProperties(User $user):array; } class NewUserValidator implements UserValidator { /** @var UserRepository */ private $userRepository; public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; } public function isValid(User $user): bool { $emailExists = $this->userRepository->emailExists($user->getEmail()); if ($emailExists === true) { return false; } return true; } }
  • 27.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest interface UserRepository { /** * @param User $user * @return bool * * @throws UnableToCreateUser */ public function create(User $user): bool; /** * @param string $email * @return bool * * @throws DataSourceError */ public function emailExists(string $email): bool; }
  • 28.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest class PdoUserRepository implements UserRepository { /** @var Pdo */ private $pdo; public function __construct(Pdo $pdo) { $this->pdo = $pdo; } public function create(User $user): bool { try { $statement = $this->pdo->prepare("INSERT INTO users(email, password) VALUES(:email, :password)"); $statement->execute(['email' => $user->getEmail(), 'password' => $user->getPassword()]); } catch (PDOException $PDOException) { throw new UnableToCreateUser($user->getEmail()); } return true; } public function emailExists(string $email): bool { try { $statement = $this->pdo->prepare("SELECT count(id) AS numRows FROM users WHERE email = :email"); $statement->execute([email => $email]); $result = $statement->fetchColumn(); return ($result > 0); } catch (PDOException $PDOException) { throw new DataSourceError(); } } }
  • 29.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest interface ConfirmationEmailer { /** * @throws MailerException */ public function send(string $to); } class MandrillConfirmationEmailer implements ConfirmationEmailer { private $mandrill; public function __construct(Mandrill $mandrill) { $this->mandrill = $mandrill; } public function send(User $user) { $message = "Please <a>click here</a> to confirm your account."; $from = "app@example.com"; $subject = "Confirm your account"; $email = [ 'to' => [['EmailTemplate' => $user->getEmail()]], 'from_email' => $from, 'subject' => $subject, 'text' => $message ]; $this->mandrill->send($email); } }
  • 30.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest interface ConfirmationEmailer { public function send(string $to); /** @throws MailerException */ }
  • 31.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest interface ConfirmationEmailer { public function send(string $to); /** @throws MailerException */ } interface Mailer { public function send(Email $email); }
  • 32.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest interface ConfirmationEmailer { public function send(string $to); /** @throws MailerException */ } interface Mailer { public function send(Email $email); } class Email { private $to, $from, $subject, $message; public function __construct(string $to, string $from, string $subject, string $message) { $this->to = $to; $this->from = $from; $this->subject = $subject; $this->message = $message; } public function getTo(): string { return $this->to; } public function getFrom(): string { return $this->from; } public function getSubject(): string { return $this->subject; } public function getMessage(): string { return $this->message; } }
  • 33.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest interface ConfirmationEmailer { public function send(string $to); /** @throws MailerException */ } interface Mailer { public function send(Email $email); } class Email { private $to, $from, $subject, $message; public function __construct(string $to, string $from, string $subject, string $message) { $this->to = $to; $this->from = $from; $this->subject = $subject; $this->message = $message; } public function getTo(): string { return $this->to; } public function getFrom(): string { return $this->from; } public function getSubject(): string { return $this->subject; } public function getMessage(): string { return $this->message; } } class ConfirmationEmailService implements ConfirmationEmailer { private $mailer; private $from = 'mysite@example.com'; private $subject = 'Confirm Email'; private $message = 'Please click here to confirm your email'; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function send(string $to) { $email = new Email($to, $this->from, $this->subject, $this->message); $this->mailer->send($email); } }
  • 34.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest interface ConfirmationEmailer { public function send(string $to); /** @throws MailerException */ } interface Mailer { public function send(Email $email); } class Email { private $to, $from, $subject, $message; public function __construct(string $to, string $from, string $subject, string $message) { $this->to = $to; $this->from = $from; $this->subject = $subject; $this->message = $message; } public function getTo(): string { return $this->to; } public function getFrom(): string { return $this->from; } public function getSubject(): string { return $this->subject; } public function getMessage(): string { return $this->message; } } class ConfirmationEmailService implements ConfirmationEmailer { private $mailer; private $from = 'mysite@example.com'; private $subject = 'Confirm Email'; private $message = 'Please click here to confirm your email'; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function send(string $to) { $email = new Email($to, $this->from, $this->subject, $this->message); $this->mailer->send($email); } } class MandrillMailer implements Mailer { private $mandrill; public function __construct(Mandrill $mandrill) { $this->mandrill = $mandrill; } public function send(Email $email) { //Convert our email to Mandrill's email $mandrillMail = ['to' => ['email' => $email->getTo()], 'from_email' => $email->getFrom(), 'subject' => $email->getSubject(), 'text' => $email->getMessage()]; $this->mandrill->send($mandrillEmail); } }
  • 35.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest interface ConfirmationEmailer { public function send(string $to); /** @throws MailerException */ } interface Mailer { public function send(Email $email); } class Email { private $to, $from, $subject, $message; public function __construct(string $to, string $from, string $subject, string $message) { $this->to = $to; $this->from = $from; $this->subject = $subject; $this->message = $message; } public function getTo(): string { return $this->to; } public function getFrom(): string { return $this->from; } public function getSubject(): string { return $this->subject; } public function getMessage(): string { return $this->message; } } class ConfirmationEmailService implements ConfirmationEmailer { private $mailer; private $from = 'mysite@example.com'; private $subject = 'Confirm Email'; private $message = 'Please click here to confirm your email'; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function send(string $to) { $email = new Email($to, $this->from, $this->subject, $this->message); $this->mailer->send($email); } } class SendGridMailer implements Mailer { private $sendgrid; public function __construct(SendGrid $sendgrid) { $this->sedngrid = $sendgrid; } public function send(Email $email) { //Convert our email to SendGrid's email $sgEmail = new SendGridEmail($email->getTo(), $email->getFrom(), $email->getSubject(), $email->getMessage()); $this->sendgrid->queue($sgEmail); } }
  • 36.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest class AccountOverdueListener { private $mailer; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function handle(OverdueEvent $event) { $account = $event->getAccount(); $email = new OverdueEmail($account->getEmailAddress(), $account->getAmountDue()); $this->mailer->send($email); } }
  • 37.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest class AccountOverdueListener { private $mailer; private $texter; public function __construct(Mailer $mailer, Texter $texter) { $this->mailer = $mailer; $this->texter = $texter; } public function handle(OverdueEvent $event) { $account = $event->getAccount(); $notifications = $account->getEnabledNotifications(); foreach ($notifications AS $notificationType) { if ($notificationType === 'email') { $email = new OverdueEmail($account->getEmailAddress(), $account->getAmountDue()); $this->mailer->send($email); } elseif ($notificationType === 'text') { $text = new OverdueText($account->getPhoneNumber(), $account->getAmountDue()); $this->texter->send($text); } } } }
  • 38.
  • 39.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest Interface Notifier { public function notify(ContactInfo $contact); }
  • 40.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest Interface Notifier { public function notify(ContactInfo $contact); } Class EmailNotifier implements Notifier { private $mailer; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function notify(ContactInfo $contact) { $email = new OverdueEmail($contact->getEmailAddress()); $this->mailer->send($email); } } Class TextNotifier implements Notifier { private $texter; public function __construct(Texter $texter) { $this->texter = $texter; } public function notify(ContactInfo $contact) { $text = 'Your Account Is Overdue!'; $this->texter->send($text, $contact->getPhoneNumber()); } }
  • 41.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest Interface Notifier { public function notify(ContactInfo $contact); } Class NotifierFactory { private $notifiers; public function register($name, Notifier $notifier) { $this->notifiers[$name] = $notifier; } public function getNotifier(string $name): Notifier { return $this->notifiers[$name]; } } Class EmailNotifier implements Notifier { private $mailer; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function notify(ContactInfo $contact) { $email = new OverdueEmail($contact->getEmailAddress()); $this->mailer->send($email); } } Class TextNotifier implements Notifier { private $texter; public function __construct(Texter $texter) { $this->texter = $texter; } public function notify(ContactInfo $contact) { $text = 'Your Account Is Overdue!'; $this->texter->send($text, $contact->getPhoneNumber()); } }
  • 42.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest class AccountOverdueListener { private $notifierFactory; public function __construct(NotifierFactory $notifierFactory) { $this->notifierFactory = $notifierFactory; } public function handle(OverdueEvent $event) { $account = $event->getAccount(); $contact = $account->getContactInfo(); $notifications = $account->getEnabledNotifications(); foreach ($notifications AS $notificationType) { $notifier = $this->notifierFactory->getNotifier($notificationType); $notifier->send($contact); } } }
  • 43.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest class AccountOverdueEmailListener { public function handle(Mailer $mailer, OverdueEvent $event) { $account = $event->getAccount(); $email = new OverdueEmail($account->getEmailAddress(), $account->getAmountDue()); $mailer->send($email); } } class AccountOverdueTextListener { public function handle(Texter $texter, OverdueEvent $event) { $account = $event->getAccount(); $text = new OverdueText($account->getPhoneNumber(), $account->getAmountDue()); $texter->send($text); } }
  • 44.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest Class PercentageDiscount { private $discount; public function __construct(int $discount) { $this->discount = $discount; } public function calculateAmountOff(Item $item): float { return ($this->discount / 100) * $item->getCost(); } }
  • 45.
    @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(); } return 'Error: Not enough items'; } 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; } }
  • 46.
    @jessicamauerhan | https://joind.in/talk/17a66| @OpenWest Class PercentageDiscount { private $discount; public function __construct(int $discount) { $this->discount = $discount; } public function calculateAmountOff(Item $item): float { 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): float { 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; } }
  • 47.
  • 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; } }
  • 53.
  • 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