SOLID - does Single responsibility principle apply to methods in a class?

1.6k views Asked by At

I am not sure whether this method inside my class is violating Single responsibility principle,

public function save(Note $note)
{
    if (!_id($note->getid())) {

        $note->setid(idGenerate('note'));

        $q = $this->db->insert($this->table)
                      ->field('id', $note->getid(), 'id');

    } else {
        $q = $this->db->update($this->table)
                      ->where('AND', 'id', '=', $note->getid(), 'id');
    }

    $q->field('title', $note->getTitle())
      ->field('content', $note->getContent());

    $this->db->execute($q);

    return $note;
}

Basically it does two jobs in a method - insert or update.

Should I separate it into two methods instead to comply with Single responsibility principle?

But SRP is meant for classes only, isn't it? Does it apply to the methods inside a class?

SRP -

a class should have only a single responsibility (i.e. only one potential change in the software's specification should be able to affect the specification of the class)

EDIT:

Another method for listing notes (including many different type of listings), searching notes, etc...

public function getBy(array $params = array())
{
    $q = $this->db->select($this->table . ' n')
                  ->field('title')
                  ->field('content')
                  ->field('creator', 'creator', 'id')
                  ->field('created_on')
                  ->field('updated_on');

    if (isset($params['id'])) {
        if (!is_array($params['id'])) {
            $params['id'] = array($params['id']);
        }

        $q->where('AND', 'id', 'IN', $params['id'], 'id');
    }

    if (isset($params['user_id'])) {
        if (!is_array($params['user_id'])) {
            $params['user_id'] = array($params['user_id']);
        }

        # Handling of type of list: created / received
        if (isset($params['type']) && $params['type'] == 'received') {
            $q
                ->join(
                    'inner',
                    $this->table_share_link . ' s',
                    's.target_id = n.id AND s.target_type = \'note\''
                )
                ->join(
                    'inner',
                    $this->table_share_link_permission . ' p',
                    'p.share_id = s.share_id'
                )
                # Is it useful to know the permission assigned?
                ->field('p.permission')
                # We don't want get back own created note
                ->where('AND', 'n.creator', 'NOT IN', $params['user_id'], 'uuid');
            ;

            $identity_id = $params['user_id'];

            # Handling of group sharing
            if (isset($params['user_group_id']) /*&& count($params['user_group_id'])*/) {
                if (!is_array($params['user_group_id'])) {
                    $params['user_group_id'] = array($params['user_group_uuid']);
                }

                $identity_id = array_merge($identity_id, $params['user_group_id']);
            }

             $q->where('AND', 'p.identity_id', 'IN', $identity_id, 'id');

        } else {
            $q->where('AND', 'n.creator', 'IN', $params['user_id'], 'id');
        }
    }

    # If string search by title
    if (isset($params['find']) && $params['find']) {
        $q->where('AND', 'n.title', 'LIKE', '%' . $params['find'] . '%');
    }

    # Handling of sorting
    if (isset($params['order'])) {
        if ($params['order'] == 'title') {
            $orderStr = 'n.title';

        } else {
            $orderStr = 'n.updated_on';
        }

        if ($params['order'] == 'title') {
            $orderStr = 'n.title';

        } else {
            $orderStr = 'n.updated_on';
        }

        $q->orderBy($orderStr);

    } else {
        // Default sorting
        $q->orderBy('n.updated_on DESC');
    }

    if (isset($params['limit'])) {
        $q->limit($params['limit'], isset($params['offset']) ? $params['offset'] : 0);
    }

    $res = $this->db->execute($q);

    $notes = array();

    while ($row = $res->fetchRow()) {
        $notes[$row->uuid] = $this->fromRow($row);
    }

    return $notes;
}
2

There are 2 answers

6
deceze On

The method persists the note to the database. If that's what it's supposed to do, then that's a single responsibility and the implementation is fine. You'll need to put the logic of deciding whether to insert or update somewhere, this seems as good a place as any.

Only if you ever needed to explicitly do inserts or updates without the implicit decision logic would it be worthwhile to separate those two out into different methods which can be called separately. But at the moment, keeping them in the same method simplifies the code (since the latter half is shared), so that's likely the best implementation.

Exempli gratia:

public function save(Note $note) {
    if (..) {
        $this->insert($note);
    } else {
        $this->update($note);
    }
}

public function insert(Note $note) {
    ..
}

public function update(Note $note) {
    ..
}

The above would make sense if you sometimes needed to call insert or update explicitly for whatever reason. SRP is not really a reason for this separation though.

3
Yang On

SOLID principles are applied to class-level terminology, they don't explicitly state about methods. A SRP itself states, that classes should have one reason to change, so as long as you can replace a responsibility which is wrapped into one class, you're okay.

Consider this:

$userMapper = new Mapper\MySQL();
// or
$userMapper = new Mapper\Mongo();
// or
$userMapper = new Mapper\ArangoDb();

$userService = new UserService($userMapper);

All those mappers implement one interface and serve one responsibility - they do abstract storage access for users. Therefore mappers have one reason to change since you can swap them easily.

Your case is not about the SRP generally. It's more about best-practice. Well, the best practice regarding methods states that they should do only one thing whenever possible and accept as less arguments as possible. That makes it easier to read and find bugs.

There's one more principle which is called Principle of Least Astonishment. It simply states that method names should explicitly do what their names imply.

Coming down to your code example:

The save() implies that it's all about data saving (updating existing record), not creating. By doing both insert and update there, you break the PoLA.

That's it, when you call explicitly insert() you know and expect that it will add a new record. The same about update() method - you know and expect that it will update a method, it will not create a new one.

Therefore I won't do both things in save(). If I want to update a record, I would call update(). If I want to create a record I would call insert().