1

I've been reading in basic concepts of OOP,as i'm trying to make this shift from transactional scripts to more oop manner in php, and i often come across this definition :

An object stores its state in fields (variables) and exposes its behavior through methods (functions). Methods operate on an object's internal state and serve as the primary mechanism for object-to-object communication

and a simple example like :

Objects like real life objects have state and behavior. Dogs have state (name, color, breed, hungry) and behavior (barking, fetching, wagging tail).

from definition i see that methods should be either manipulating internal state (setters) or communicating with other objects;

but when i try to apply this to a Blog example. composed of mainly 2 domains User and posts.

what would the User object Behavior ?

I cannt find any !

  1. login, its an auth lib. thing so i should not include it in user.
  2. posting articles is a Post object thing; again user conduct it; but its more of a post object concern to create a post right ?

User may be the main Aggregate object in a blog; yet the user is more like the Creator of Other Objects but he does not have a Behavior i can think of; he is being used -and his state- by other objects in most cases that all !

also regarding object-to-object communication; what is that ?

does this means that an object can invoke another object method and inject him self ($this) as an argument ? is that what is meant by communication ? i though i should do this outside of object -in a service layer or something- what am i missing here ?

in a nutshell : what are allowed type of methods inside an object ?

Zalaboza
  • 8,899
  • 16
  • 77
  • 142

1 Answers1

3

Methods manipulating it's own object's state are cohesive. If an object has method that operate on it's members (state), then we speak of high cohesion. If a method does not operate on it's members, you have low cohesion. In the latter case it's a good indicator that the method is misplaced on the object.

Example:

class Foo 
{
    private $fooBar;

    public function highCohesionMethod()
    {
        // do something with $fooBar
    }

    public function lowCohesionMethod($input)
    {
        return strtolower($input);
    }
}

Cohesion is not limited to setters. Methods can broadly be categorized as Commands or Queries. Commands tell the object to do something. A Command with high cohesion will somehow use the object's internal state for this. Queries ask the object about it's internal state. By doing so, you are not changing the state, but you still have high cohesion. By virtue of the CommandQuerySeparation principle, Commands should not return something, while Queries should.

Example:

class Foo 
{
    private $fooBar;

    // Command        
    public function setFooBar($fooBar)
    {
        $this->fooBar = $fooBar;
    }

    // Query
    public function getFooBar($input)
    {
        return $this->fooBar;
    }
}

The consequence of High Cohesion and CommandQuerySeparation is, that you should try to Tell Don't Ask, e.g. try to tell objects what they should do instead of asking them and then putting the logic into the consumer. If you pull state of collaborators from the outside and then make decisions, you are almost always lowering cohesion:

Example:

class Consumer
{
    public function askingObjects()
    {
        if ($this->collaborator->hasFooBar()) {
            $fooBar = $this->collaborator->getFooBar();
            // do something with $fooBar
            // more code …
        } else {
            throw new RuntimeException('Collaborator has no foo');
        }
    }
}

As you can see, the consumer is not operating on it's own state, but it's pulling in the collaborator's state to operate on that. To do so, the collaborator has to have methods exposing it's internal state and the consumer has to know about this. In most cases, this is not necessary and violates Information Hiding principle and is much closer to procedural programming than to OOP. Compare with

class Consumer
{
    public function tellingObjects()
    {
        try {
            $this->collaborator->doFooBar();
        } catch (CollaboratorException $e) {
            // handle gracefully
        }
    }
}

class Collaborator
{
    public function doFooBar($input)
    {
        if ($this->hasFooBar()) {
            // do something with $this->fooBar
            // more code …
        } else {
            throw new CollaboratorException('This has no foo');
        }
    }
}

With this code, the decision whether collaborator can or cannot do something is up to the collaborator itself. It's appropriate to do so, because the collaborator is an Information Expert. It knows best what it can and cannot do.

Following Tell Don't Ask will usually reduce the number of Getters and Setters drastically, as you focus on what the object responsibilities. This will give you much smaller interfaces for object to object communication, e.g. it will narrow the contracts between collaborators, benefiting changes.

However, following Tell Don't Ask will often lead to object's having more than one responsibility, which violates the SingleResponsibilityPrinciple so in practise you will have a mix of Commands and Queries and Mediators, e.g. objects handling communication between other objects.

This gets us to your blog example. You can do pretty much everything from and through the user:

class User
…
    public function authenticate($authenticator, $password)
    {
         $this->isAuthenticated = $authenticator->isValidUser(
             $this->username, 
             $password
         );
    }

You can also handle all the BlogPost modifications through it:

class User
…
    public function createNewBlogPost()
    {
        $blogPost = new BlogPost;
        $blogPost->setAuthor($this);

        return $blogPost;
    }

    public function readBlogPost($id)
    {
        return $this->blogPostRepository->findById($id);
    }

    public function updateBlogPost(BlogPost $blogPost, $title, $body, $tags)
    {
         $blogPost->setTitle($title);
         $blogPost->setBody($body);
         $blogPost->setTags($tags);
    }

    public function deleteBlogPost(BlogPost $blogPost)
    {
        $blogPost->setDeleted(true);
    }

But then you'll end up with a User object that has very little cohesion. It will likely also know quite a bit about all the other classes in your application. It might grow pretty big which is usually a sign of too many responsibilities. However, in Domain Driven Design it is common to have a single entry point to a larger object graph. This is the called the Aggregate Root. I'm just not so sure whether the User is the best choice for that. Maybe you should introduce a Blog class as the Aggregate Root.

Note that it's not necessarily a BlogPost object concern to create, update or publish itself. The BlogPost pretty much depends on being told what it's state is. In that regard it's just a data container. Think of it as a glorified array. Putting Getters and Setters for collaborators to manipulate it is okay.

Another option would be to split all the CRUD actions into separate Service classes. This is easy to maintain and has the added benefit that you'll know immediately what your app can do by looking at the files in the UseCase folder

Example:

class CreateNewBlogPostService()
…
    public function createNewBlogPost(User $user)
    {
        $blogPost = new BlogPost;
        $blogPost->setAuthor($user);

        $this->blogPostsRepository->add($blogPost);

        return $blogPost;
    }
}

This is easy to read and understand because everything happens inside that single method. There is just a single responsibility here. Obviously there isn't much cohesion but Services orchestrate other collaborators so it's normal for them not to have any state at all.

If you don't like this approach, you could also experiment with Role Objects and DCI, but this is out of scope for this answer. Likewise, given that a Blog is mainly just CRUD of blog posts, it might make more sense to skip the DomainModel and just approach it with a TableDataGateway and TableModule approach instead.

Anyways, hope that sheds some light.

Community
  • 1
  • 1
Gordon
  • 312,688
  • 75
  • 539
  • 559