♻️ Refactoring
Random refactoring approaches. Most are about web development but other than that, these should work with most languages and frameworks.
Related notes:
Utilize form objects for complex HTML posts.
// POSTing a form to /items creates an item.
Route->post('items', function () {
$item = new ItemForm(request());
$item->save();
});
Utilize use cases for complex and common operations. Don't use this everywhere though; only if it reduces complexity or duplication. It's easier to refactor the resulting code e.g. to be multithreaded.
class PurchasePodcast extends UseCase
{
private function handle() {
$this->preparePurchase()
->sendEmail();
}
// preparePurchase which returns $this...
// sendEmail which returns $this...
}
abstract class UseCase
{
public static function perform() { return (new static)->handle(); }
abstract public function handle();
}
// And usage:
PurchasePodcast::perform(); // You could pass parameters here.
Split up god objects. Too much responsibility, even the slightest change can break something unrelated.
- Find common knowledge domain and split it behind a function call. E.g. move all counts and other number return functions to
WhateverStatsclass and expose it through member functionstats(). - Create a trait/mixin if you can encapsulate functionality. E.g. create expendable
Completableclass that you use on the main class. - Extract a value object if a lot of behavior depends on computing on it. E.g. in stead of
revenue()andrevenueInDollars()etc., userevenue()->inDollars().
Use policy objects. Authorization checks can become a pain if you have multiple access levels.
// Checks like these are common in web applications.
public function save()
{
if (auth()->guest()) {
abort(403, 'You are not signed in.')
}
if ($this->owner_id != auth()->user()->id) {
abort(403, 'You are not the owner')
}
}
// Policy object
class SaveItemPolicy
{
public function allowed()
{
// The checks
}
}
// And usage
(new SaveItemPolicy())->allowed();
Split long chunks of code. The same reason as with god objects.
class Thing
{
public function handle()
{
$tasks = [
DoThis::class,
DoThat::class,
RunSomething::class,
EraseSomething::class
];
foreach ($tasks as $task) {
(new $task)->handle();
}
}
}
class DoThis { public function handle() {} }
class DoThat { public function handle() {} }
class RunSomething { public function handle() {} }
class EraseSomething { public function handle() {} }
Hide complex conditionals behind a strategy object. Especially good if the conditional clauses don't have a lot common.
class SubscriptionsController
{
public function store(Request $request)
{
$strategy = $this->getRegistrationStrategy($request);
$strategy->handle();
}
protected function getRegistrationStrategy(Request $request)
{
if ($request->type == 'vip') {
return new RegistersLifetimeMember;
}
if ($request->plan == 'yearly') {
return new RegistersYearlyMember;
}
if ($request->plan == 'monthly') {
return new RegistersMonthlyMember;
}
}
}
Strategy works for too many arguments too.
function compress(CompressionStrategy $strategy)
{
$strategy->handle();
}
compress(new JavaScriptDriver($files, $destination));
Use normalizing or normalizing class for initializing input data.
class SubscriptionsController
{
public function update(Request $request)
{
$plan = $request->plan;
$code = $request->coupon;
// These checks can easily get out of hand so normalizing
// is frequently good.
if ( ! Coupon::isValid($code, $plan) ) {
$code = null;
}
$this->user
->subscription()
->usingCoupon($code)
->swap($plan);
}
}
// Normalized...
$this->user
->subscription()
->usingCoupon(Coupon::normalize($code, $plan))
->swap($plan);
Use presenters for representation calculations and generation.
class User
{
public function present()
{
return new UserPresenter($this);
}
}
class UserPresenter extends Presenter
{
public function fullName()
{
return $this->presentee->firstName . ' ' . $this->presentee->lastName;
}
}
abstract class Presenter
{
protected $presentee;
public function __construct($presentee)
{
$this->presentee = $presentee;
}
public function __get($property)
{
if (method_exists($this, $property)) {
return call_user_func([$this, $property]);
}
$message = '%s does not respond to "%s" property or method';
throw new Exception(sprintf($message, static::class, $property));
}
}
// Usage:
$user->present()->fullName;
Decorators are also nice replacements for conditionals. Check if your language supports then without basic wrapping.
$tutorial = new TweetsTutorial(new EmailsTutorial(new Tutorial()))
$tutorial->publish();
class TweetsTutorial
{
protected $tutorial;
public function __construct($tutorial)
{
$this->tutorial = $tutorial;
}
public function publish()
{
$this->tutorial->publish();
// Tweet it here
}
}