Luka Peharda
Insult
Excuse
The art of restructuring existing computer code without changing its external behavior
It is a code makeover, but without the need to hire Mirjana Mikulec
"Refactoring is a series of small behavior preserving transformations. Each transformation does little, but a sequence of these transformations can produce a significant restructuring."
Martin Fowler
Enhance readability
Enhance maintainability
Improve extensibility
Increase understanding of unfamiliar code
Increase performance
Reduce technical debt
1 + 1 = 3
It's a warm summer evening in ancient Greece…
Course / Lesson / Video & Comments
class Course
{
protected array $lessons;
public function statement(): string
{
$count = count($this->lessons);
$duration = 0;
$commentsCount = 0;
// Loop through lessons and calculate duration and comments count
foreach ($this->lessons as $lesson) {
if ($lesson->video !== null) {
$duration += floor($lesson->video->duration / 60);
} else {
$duration += floor(
str_word_count(
strip_tags($lesson->text)
) / 200 * 60
);
}
$commentsCount += count($lesson->comments);
}
return sprintf(
'This course has %d lessons with %d comments and will take %d minutes to complete.',
$count,
$commentsCount,
$duration
);
}
}
public function statement(): string
{
$count = count($this->lessons);
$duration = 0;
$commentsCount = 0;
// Loop through lessons and calculate duration and comments count
foreach ($this->lessons as $lesson) {
$duration += $this->getLessonDuration($lesson);
$commentsCount += count($lesson->comments);
}
return sprintf(
'This course has %d lessons with %d comments and will take %d minutes to complete.',
$count,
$commentsCount,
$duration
);
}
protected function getLessonDuration(Lesson $lesson): int
{
if ($lesson->video !== null) {
return floor($lesson->video->duration / 60);
} else {
return floor(
str_word_count(
strip_tags($lesson->text)
) / 200 * 60
);
}
}
class Lesson
{
public ?Video $video = null;
public ?string $text = null;
public array $comments;
public function getDuration(): int
{
if ($this->video !== null) {
return floor($this->video->duration / 60);
} else {
return floor(
str_word_count(
strip_tags($this->text)
) / 200 * 60
);
}
}
public function getCommentsCount(): int
{
return count($this->comments);
}
}
public function statement(): string
{
$count = count($this->lessons);
$duration = $commentsCount = 0;
// Loop through lessons and calculate duration and comments count
foreach ($this->lessons as $lesson) {
$duration += $lesson->getDuration();
$commentsCount += $lesson->getCommentsCount();
}
return sprintf(
'This course has %d lessons with %d comments and will take %d minutes to complete.',
$count,
$commentsCount,
$duration
);
}
class Course
{
protected array $lessons;
protected function calculateDuration(array $lessons): int
{
return array_reduce(
$lessons,
fn($acc, $lesson) => $acc + $lesson->getDuration()
, 0);
}
protected function commentsCount(array $lessons): int
{
return array_reduce(
$lessons,
fn($acc, $lesson) => $acc + $lesson->getCommentsCount()
, 0);
}
public function statement(): string
{
return sprintf(
'This course has %d lessons with %d comments and will take %d minutes to complete.',
count($this->lessons),
$this->calculateDuration($this->lessons),
$this->commentsCount($this->lessons)
);
}
}
Rule of three
When you add a new feature
When you fix a bug
When implementing new feature using a new technique
During code review
When procrastinating
or needing a small win to get you started
If it stinks, change it
Long Method
Long Class
Comments
Comments
// $commentable is a lesson so it should only be made searchable if lesson is not published
if ($commentable::class === Lesson::class && ! $commentable->isPublished()) {
// ...
}
class Lesson
{
public function shouldBePublished(): bool
{
return $this->isPublished();
}
}
if ($commentable->shouldBePublished()) {
// ...
}
Comments
Primitive Obsession
Primitive Obsession
class Order
{
public function pay(float $amount, string $currency = 'USD'): void
{
// ...
}
}
Primitive Obsession
class Price
{
public function __construct(
protected float $amount,
protected string $currency = 'USD'
) {
// ...
}
}
class Order
{
public function pay(Price $price): void
{
// ...
}
}
Primitive Obsession
$item = [
'name' => 'Beer',
'price' => 2.0,
'currency' => 'USD'
];
$item = new OrderItem(
name: 'Beer',
price: 2.0,
currency: 'USD'
);
Long Parameter List
Long Parameter List
class MemberService
{
public function createMember(
Website $website,
string $email,
string $firstName,
string $lastName,
bool $sendNotification = false
): Member {
$member = Member::create([
'website_id' => $website->id,
'email' => $request->email,
'first_name' => $request->firstName,
'last_name' => $request->lastName
]);
if ($sendNotification) {
dispatch(SendNotification($website, $member));
}
return $member;
}
}
Long Parameter List
class MemberService
{
public function createMember(
Website $website,
CreateMemberRequest $request,
bool $sendNotification = false
): Member {
$member = Member::create([
'website_id' => $website->id,
'email' => $request->email,
'first_name' => $request->firstName,
'last_name' => $request->lastName
]);
if ($sendNotification) {
dispatch(SendNotification($website, $member));
}
return $member;
}
}
Long Parameter List
class MemberService
{
public function createMember(
Website $website,
CreateMemberRequest $request
): Member {
return Member::create([
'website_id' => $website->id,
'email' => $request->email,
'first_name' => $request->firstName,
'last_name' => $request->lastName
]);
}
public function createMemberAndSendNotification(
Website $website,
CreateMemberRequest $request
): Member {
$member = $this->createMember($website, $request);
dispatch(SendNotification($website, $member));
return $member;
}
}
Shotgun Surgery
Shotgun Surgery
class MakeCommentSearchable
{
public function shouldBeSearchable(): bool
{
if ($comment->created_at < now()->subHours(3)) {
return false;
}
return true;
}
}
class ShowComments
{
public function shouldBeDisplayed(): bool
{
if ($comment->created_at < now()->subHours(3)) {
return false;
}
return true;
}
}
Shotgun Surgery
class Comment {
public function wasCreatedRecently(): bool
{
if ($this->created_at < now()->subHours(3)) {
return false;
}
return true;
}
}
public function shouldBeSearchable(): bool
{
return $comment->wasCreatedRecently();
}
public function shouldBeDisplayed(): bool
{
return $comment->wasCreatedRecently();
}
Inappropriate Intimacy
Inappropriate Intimacy
class Member
{
public function getFullAddress(): string
{
$output = $this->address->street . ' ' . $this->houseNumber;
$output .= ', ' . $this->address->city . ' ' . $this->address->zipCode;
if ($this->address->state) {
$output .= ', ' . $this->address->state;
}
$output .= ', ' . $this->address->country;
return $output;
}
}
Inappropriate Intimacy
class Address
{
public function getFullAddressAsString(): string
{
$output = $this->street . ' ' . $this->houseNumber;
$output .= ', ' . $this->city . ' ' . $this->zipCode;
if ($this->state) {
$output .= ', ' . $this->state;
}
$output .= ', ' . $this->country;
return $output;
}
}
public function getFullAddress(): string
{
return $this->address->getFullAddress();
}
Feature Envy
Dead Code
Duplicate Code
Extract Method / Class
Move Method
Replace Temp with Query
Replace Data Value with Object
Introduce Parameter Object
Replace Array with Object
Introduce Explaining Variable
Introduce Explaining Variable
if (($platform->toUpperCase()->indexOf("MAC") > -1)
&& ($browser->toUpperCase()->indexOf("IE") > -1)
&& $this->wasInitialized()) {
// ...
}
$isMacOs = $platform->toUpperCase()->indexOf("MAC") > -1;
$isIE = $browser->toUpperCase()->indexOf("IE") > -1;
if ($isMacOs && $isIE && $this->wasInitialized()) {
// ...
}
Hide Delegate
Hide Delegate
$lesson->getVideo()->getDuration();
$lesson->getVideoDuration();
Remove Middle Man
Remove Middle Man
$lesson->getVideoType();
$lesson->getVideo()->getType();
Introduce Foreign Method
Introduce Foreign Method
class Report {
public function sendReport()
{
$previousReportDate = clone $this->previousReportDate;
$paymentDate = $previousReportDate->modify("+7 days");
// ...
}
}
Introduce Foreign Method
class Report {
public function sendReport()
{
$paymentDate = $this->nextWeek($this->previousReportDate);
// ...
}
/**
* Foreign method. Should be in Date.
*/
private function nextWeek(DateTime $date)
{
$previousReportDate = clone $date;
return $previousReportDate->modify("+7 days");
}
}
Introduce Local Extension
Replace Magic Number with Symbolic Constant
Replace Magic Number with Symbolic Constant
class CommentService
{
public function getPublishedComments()
{
return Comment::where('status', 2)->get();
}
public function getPendingComments()
{
return Comment::where('status', 1)->get();
}
}
public function getPublishedComments()
{
return Comment::where('status', 'published')->get();
}
Replace Magic Number with Symbolic Constant
class Comment
{
const STATUS_PUBLISHED = 2;
const STATUS_PENDING = 1;
}
public function getPublishedComments()
{
return Comment::where('status', Comment::STATUS_PENDING)->get();
}
Decompose Conditional
Decompose Conditional
class Order
{
public function calculateTotal()
{
if ($this->taxSettings->includeVat === true && $this->taxSettings->chargeVat === true && $this->taxSettings->vatPercentage > 0 && $this->customer->vatValid === false) {
$price = $this->price * (1 + $this->taxSettings->vatPercentage / 100);
$total = $price * $quantity;
} else {
$price = $this->price;
$total = $price * $quantity;
}
return $total;
}
}
Decompose Conditional
class Order
{
public function calculateTotal()
{
if (
$this->taxSettings->includeVat === true
&& $this->taxSettings->chargeVat === true
&& $this->taxSettings->vatPercentage > 0
&& $this->customer->vatValid === false
) {
$price = $this->price * (1 + $this->taxSettings->vatPercentage / 100);
$total = $price * $this->quantity;
} else {
$price = $this->price;
$total = $price * $this->quantity;
}
return $total;
}
}
Decompose Conditional
protected function shouldChargeVat(TaxSettings $taxSettings, Customer $customer)
{
return $taxSettings->includeVat === true && $taxSettings->chargeVat === true
&& $taxSettings->vatPercentage > 0 && $customer->vatValid === false;
}
protected function calculateTotalVatIncluded()
{
return $this->price
* (1 + $this->taxSettings->vatPercentage / 100)
* $this->quantity;
}
protected function calculateTotalWithoutVat()
{
return $this->price * $this->quantity;
}
public function calculateTotal()
{
if ($this->shouldChargeVat($this->taxSettings, $this->customer)) {
return $this->calculateTotalVatIncluded();
}
return $this->calculateTotalWithoutVat();
}
Hide delegate VS Remove middle man
Extract method VS Inline method
Add parameter VS Remove parameter
Parametrize method VS Replace parameter with explicit methods
Replace error code with exception VS Replace exception with test
Refactoring by Michael Fowler
https://refactoring.com
https://refactoring.guru
https://kata-log.rocks/refactoring
Mikado Method
Leave the code in better shape than you found it
Class is an extensible program-code-template for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions or methods)
Object is called an instance of the class, and the member variables specific to the object are called instance variables
Facade is a structural design pattern that provides a simple interface to a complex subsystem which contains lots of moving parts