vinhnglx / pood

Basic notes about Object Oriented Design in Ruby
0 stars 0 forks source link

Designing Classes with a Single Responsibility #1

Open vinhnglx opened 8 years ago

vinhnglx commented 8 years ago

The foundation of an object-oriented system is the message, but the most visible organizational structure is the class.

Anyone can arrange code to make it work right now. Creating an easy-to-change application, however, is a different matter. Your application needs to work right now just once; it must be easy to change forever.

Deciding what belongs in a Class

The common issue that every developer always make is organization; you know how to write the code but not where to put it.

Grouping Methods into Classes Grouping methods into classes, at this early stage of the project, you can't possibly get it right. If your application succeeds, many of the decisions you make today will need to be changed later.

Organizing Code to Allow for Easy Changes Define easy to changes as

The code should be

vinhnglx commented 8 years ago

Creating Classes that have a Single Responsibility

A class should do the smallest possible useful thing; that is, it should have a single responsibility.

Applications that are easy to change consist of classes that are easy to reuse.

A class that has more than one responsibility is difficult to reuse - for example if you want to reuse some (but not all) of its behavior, it is impossible to get at only the parts you need. You are faced with two options:

Determining If a Class has a Single Responsibility

A good way to know what a class is actually doing is to attempt to describe it in one sentence. Remember that a class should do the smallest possible useful thing. That thing ought to be simple to describe.

If the simplest description you can devise uses the word "and" then the class likely has more than one responsibility.

If the word "or" then the class has more than one responsibility and they aren't even related.

OO designers use the word cohesion to describe this concept. When everything in a class is related to its central purpose, the class is said to be highly cohesive or to have Single Responsibility.

vinhnglx commented 8 years ago

Writing code that embraces change

We have a few well-known techniques that you can use to create code that embraces change

Depend on Behavior, not Data

Behavior is captured in methods and invoked by sending messages. When you create classes that have a single responsibility, every tiny bit of behavior lives in one and only one place.

The phrase "Don't Repeat Yourself" (DRY) is a shortcut for this idea.

In addition to behavior, objects often contain data. Data is held in an instance variable and can be anything from simple string to a complex hash. Data can be accessed in one of two ways:

With the first way, we need to careful when using it, if your class have instance variable and it is referred to ten times and it suddenly needs to be adjusted, then the code will need many changes.

But if the instance variable is wrapped in an accessor method and we call this method when we need. This approach changed from data (which is referenced all over) to behavior (which is defined one).

Hide Instance Variables

Take a look at the example below:

Ruby provides attr_readers as an easy way to create the encapsulating methods.

class Math
  attr_reader :a, :b
  def initialize(a, b)
    @a = a
    @b = b
  end

  def calculate
    a + b
  end
end

Using attr_reader caused Ruby to create simple wrapper methods for the variables. Here is a virtual representation for attr_reader :a

def a
  @a
end

However, you can change what a mean by implementing your own version of the method.

class Math
  attr_reader :b
  def initialize(a, b)
    @a = a
    @b = b
  end

  def calculate
    a + b
  end

  def a
    @a * 5 + 10
  end
end
vinhnglx commented 8 years ago

Dealing with data as if it's an object that understands messages introduces two new issues.

The first issue involves visibility. Wrapping the @a instance variable (the example above) in a public a method expose this variable to the other objects in your application; any other object can now send a. It would have been just as easy to create a private wrapping method (will talk about this later)

The second issue is more abstract. Because it's possible to wrap every instance variable in a method and to therefore treat any variable as if it's just another object, the distinction between data and a regular object begins to disappear. Regardless of how far your thoughts move in this direction, you should hide data from yourself. Doing so protects the code from being affected by unexpected changes.

Hide Data Structure

If being attached to an instance variable is bad, depending on a complicated data structure is worse.

If an instance variable contains a complicated data structure (for example two-dimensional array or array of hashes, etc), just hiding the instance variable is not enough. A change to its structure would cascade throughout your code; each change represents an opportunity to create a bug.

Direct references into complicated structures are confusing, because they obscure what data really is, and they are a maintenance nightmare.

In Ruby, it's easy to separate structure from meaning. Just as you can use a method to wrap an instance variable, you can use the Ruby Struct class to wrap a structure.

For example

From

class Person
  attr_reader :name, :day, :month, :year

  def initialize(opts={})
    @name = opts[:name]
    @day = opts[:day]
    @month = opts[:month]
    @year = opts[:year]
  end

  def birthday
    # do st to get birthday from day, month, year.
  end
end

We can change

class Person
  attr_reader :name, :birthday

  Birthday = Struct.new(:day, :month, :year) do
     def birth_day
       # Do st 
     end
  end

  def initialize(opts={})
    @name = name
    @birthday = Birthday.new(opts[:day], opts[:month], opts[:year])
  end
end

This style of code allows you to protect against changes in externally owned data structures and to make your code more readable and intention revealing.

Yo! I wanna to talk a little bit about Struct.

Why should I use a Struct?

You don't have to use a Struct, but it is there for certain situation where it can make your life easier.

vinhnglx commented 8 years ago

Enforce Single Responsibility Everywhere

Creating classes with a single responsibility has important implications for design, but the idea of single responsibility can be usefully employed in many other parts of your code.

Extract extra responsibilities from Methods Methods, like classes, should have a single responsibility, makes them easy to change and easy to re-use.

The impact of a single refactoring like this is small, but the cumulative effect of this coding is huge. Methods that have a single responsibility confer the following benefits:

Isolate Extra Responsibilities in Classes

Once every method has a single responsibility, the scope of your class will be more apparent. But, if circumstances allow you to create a separate class, perhaps you should.

Ruby allows you to move the responsibility to a new class.

class Person
  attr_reader :name, :birthday

  def initialize(opts={})
    @name = opts[:name]
    @birthday = Birthday.new(opts[:day], opts[:month], opts[:year])
  end
end

class Birthday
  def initialize(day, month, year)
    # do st
  end
end

If you have a muddled class with too many responsibilities, separate those responsibilities into different classes. Concentrate on the primary class. If you identify extra responsibilities that you can't yet remove, isolate them. Don't allow extraneous responsibilities to leak into your class.

Summary

The path to changeable and maintainable object-oriented software begins with classes that have single responsibility. Classes that do one thing isolate that thing from the rest of your application. The isolation allows change without consequence and reuse without duplication.