why is crud a bad idea - focus on real scenarios

Post on 21-Jan-2018

103 Views

Category:

Internet

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Why Is CRUD a Bad IdeaFocus on Real Scenarios

Petr HeinzMore than 8 years of programming experience.

Loves clean code, regular expressions and clever design.

Dedicated last year to developing the Shopsys Framework, open source e-commerce framework made with passion on Symfony 3.

✉ petr.heinz@shopsys.com

Typical CRUD

Create, Read, Update and DeleteFour basic functions of an persistent storage, often used as an API.

Can be mapped to SQL statements:

INSERT SELECT UPDATE DELETE

Can be mapped to HTTP methods (used in REST APIs):

PUT GET POST DELETE

Example: Articleclass Article {

private $author, $text, $state, $published;

public function setAuthor(Author $author) { $this->author = $author; }

public function getAuthor() { return $this->author; }

public function setText($text) { $this->text = $text; }

// ...

}

class ArticleController {

public function create(...) { // ... }

public function update(...) { // ...

$article->setAuthor($author); $article->setText($text); $article->setState($state); $article->setPublished($published); }

public function delete(...) { // ... }

}

Entities Must Follow Business RulesEntities are often constrained by business rules and the consistency must be kept.

● Company customer must have VAT ID filled in his account.

● Sold out product must be hidden.

● Article in published state must have published date.

This is difficult to achieve in the previous example. That’s because all article

attributes can be changed independently. Developers are not restricted in the way

they interact with the object.

Example: Article without settersclass Article {

const STATE_UNPUBLISHED = 1; const STATE_PUBLISHED = 2; const STATE_DELETED = 3;

private $author, $text, $state, $published;

public function __construct(Author $author, $text, $state, DateTime $published = null) { // ... }

public function update(Author $author, $text, $state, DateTime $published = null) { // ... }

public function delete() { $this->state = self::STATE_DELETED; }

}

Example: Article without settersclass Article {

// ...

public function update(Author $author, $text, $state, DateTime $published = null) { if ($this->state === self::STATE_DELETED) { throw new ArticleIsDeletedException($this); }

$this->author = $author; $this->text = $text; $this->state = $state;

if ($state === self::STATE_PUBLISHED) { $this->published = $published ?: new DateTime(); } elseif ($state === self::STATE_UNPUBLISHED) { $this->published = null; } }

}

What Is an Object Anyway?

Object Oriented ProgrammingObjects have both data (their properties) and behavior (their methods).

Objects model real-world behavior, concepts and relationships.

Encapsulation principle tells us to hide the details about the data and focus solely

on the behavior - the public methods of our objects (“Tell, Don’t Ask”).

In PHP it is easy to combine procedural and object-oriented programming.

Example: Bank Account Objectclass BankAccount {

private $balance;

public function __construct(Money $balance) { $this->balance = $balance; }

public function deposit(Money $amount) { $this->balance = $this->balance->add($amount); }

public function withdraw(Money $amount) { if ($this->balance->isLessThan($amount)) { throw new InsufficientFundsException($balance, $amount); } $this->balance = $this->balance->subtract($amount); }

}

Anemic / Rich Domain Model

Let’s Define Some Terms FirstDomain: Most common definition: “A sphere of knowledge or activity.”

It’s basically the subject area of your application (eg. an online store or news site).

Domain Model: System of abstractions describing part of the domain that can be

used to solve problems. Simplification of the real world.

Domain Object: Object that is part of the domain model (eg. Product, Order, …).

Business Logic: High-level logic reflecting the real-world business rules.

Anemic Domain ModelNo business logic in domain objects

Clearly separates logic and data

Violates object encapsulation

Works well for simple applications

Leads to procedural programming

Called an anti-pattern by M. Fowler

Business logic mainly in domain objects

Domain objects encapsulate inner data,

offer meaningful behavior

Data integrity kept by the encapsulation

Better choice for complex domain

models

Rich Domain Model

Anemic Domain Model Rich Domain Modelclass Worker {

function getVelocity() { return $this->velocity; }

function setVelocity($velocity) { $this->velocity = $velocity; }

}

class WorkerService {

function work(Worker $w, Task $t, $time) { $progress = $t->getProgress(); $progress += $w->getVelocity() * $time; $t->setProgress($progress); }

}

class Worker {

function __construct($velocity) { $this->velocity = $velocity; }

function work(Task $task, $time) { $progress = $this->velocity * $time; $task->makeProgress($progress); }

}

The Problem Lies in Setters

Setters Do Not Exist in the Real WorldSetters have no meaning in the real world:

● A writer does not set a “published” state to an article, he/she publishes it.

● Customers do not set a “paid” status to an order, they pay for it.

● Your happy boss does not set a higher salary to you, he/she raises it.

There is always a better, more expressive, alternative to a setter.

Expressive statements lead to more readable code.

Nobody Expects The Setters To Do StuffSimilarly to adding logic to a CRUD

update, you might feel the need to add

some business logic to your setter.

The problem with this is that nobody

expects setters to do anything beside

setting the property.

An unexpected behavior leads to bugs.

class Order {

// ...

public function setStatus($status) { if (!$this->isValidStatus($status)) { throw new InvalidArgumentException(); }

$this->status = $status;

if ($status === self::STATUS_PAID) { $this->mailService->sendOrderPaidMail( $this->orderNumber, $this->customer ); } }

}

Nobody Expects The Setters To Do StuffSimilarly to adding logic to a CRUD

update, you might feel the need to add

some business logic to your setter.

The problem with this is that nobody

expects setters to do anything beside

setting the property.

An unexpected behavior leads to bugs.

class Order {

// ...

public function pay() { $this->status = self::STATUS_PAID;

$this->mailService->sendOrderPaidMail( $this->orderNumber, $this->customer ); }

public function cancel() { $this->status = self::STATUS_CANCELLED; }

}

An Update in CRUD Is Similar to a SetterGeneric update method in CRUD is similar to a setter:

● It does not have a real-world meaning.

● There are better alternatives based on real scenarios to be implemented.

For example, by “updating” an article we mean “rewriting” it and possibly

“publishing”, “unpublishing” or “deleting” it.

Conclusion and Recommendations

Focus on Real ScenariosBy building your application around you domain objects and their behavior you

can get expressive code that is easier to understand, use and maintain.

Concept of “setting” or “updating” to too generic to be meaningful.

Your API should be focused on real scenarios, real use-cases. This will keep the

API clean and intuitive and it will help you keep the integrity of your data.

Think About the Way You ProgramThere is no best way, no silver bullet. And there probably never will be one.

Keep in mind the techniques of object-oriented programming, encapsulation

principle, focusing on the behavior.

Knowing about the two extremes will help you improve the design of your

application and choose the proper solution for your project.

Need CRUD methods? Add a Layer.If you for some reason want to

allow classical CRUD methods,

you can build it on top of your

internal API.

You can use Adapter pattern for

this task.

class Article {

// ...

public function update(...) { if ($this->state === self::STATE_DELETED) { throw new ArticleIsDeletedEx($this); }

$this->author = $author; $this->text = $text; $this->state = $state;

if ($state === self::STATE_PUBLISHED) { $this->published = $published ?: new DateTime(); } elseif ($state === self::STATE_UNPUBLISHED) { $this->published = null; } }

}

Need CRUD methods? Add a Layer.If you for some reason want to

allow classical CRUD methods,

you can build it on top of your

internal API.

You can use Adapter pattern for

this task.

class ArticleCrudAdapter {

// ...

public function update(...) { if ($this->article->isDeleted()) { throw new ArticleIsDeletedEx($this->article); }

$this->article->rewrite($text, $author); switch ($state) { case Article::STATE_PUBLISHED: $this->article->publish($published); break; case Article::STATE_UNPUBLISHED: $this->article->unpublish(); break; case Article::STATE_DELETED: $this->article->delete(); break; } }

}

It Is About the BalanceEvery way has its tradeoffs and, as always, it is about the balance.

Focusing on real-world use-cases helps to maintain integrity and usability.

Integration with other libraries or components is easier with generic methods.

When developing Shopsys Framework we try to keep that in mind, and take

inspiration from Rich Domain Model.

See for yourself, join closed beta: https://www.shopsys-framework.com

Thanks for listening!

Let’s get down to your questions!

top related