ruk·si

♻️ Refactoring

Updated at 2016-08-02 02:57

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.

  1. Find common knowledge domain and split it behind a function call. E.g. move all counts and other number return functions to WhateverStats class and expose it through member function stats().
  2. Create a trait/mixin if you can encapsulate functionality. E.g. create expendable Completable class that you use on the main class.
  3. Extract a value object if a lot of behavior depends on computing on it. E.g. in stead of revenue() and revenueInDollars() etc., use revenue()->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
    }
}

Sources