Week 4 - Object-Oriented Design (Encapsulation & Cohesion, Inheritance & Module)

Week 4 - Object-Oriented Design (Encapsulation & Cohesion, Inheritance & Module)

·

8 min read

Encapsulation

We break our programs up into multiple objects to control complexity. So how do we decide which methods and variables should live in which objects?

Example 1

Bank
  get_balance
  credit_account(date, amount)
  debit_account(date, amount)
  print_statement

Can be turned into two groups:

 get_balance
 print_statement
#
  credit_account(date, amount)
  debit_account(date, amount)

Example 2

Reorganising secret diary into classes:

SecretDiary

  • lock
  • unlock
  • add_entry
  • get_entries
require 'secretDiary'

class Diary 

  def initialize
    @secretDiary = SecretDiary.new
  end

  def lock
    @secretDiary.lock
  end

  def unlock
    @secretDiary.unlock
  end

  def add_entry
    if @secretDiary.locked
      raise 'Diary is locked!'
    else
      'Add in entry'
    end
  end

  def get_entries
    if @secretDiary.locked
      raise 'Diary is locked!'
    else
      'Show entries'
    end
  end

end
class SecretDiary 

  attr_accessor :locked

  def initialize
    @locked = true
  end

  def unlock

    @locked = false
  end

  def lock
    @locked = true
  end
end

Low Coupling

How much do your different modules depend on each other?

Modules should be as independent as possible from other modules, so that changes to module don’t heavily impact other modules.

High coupling would mean that your module knows the way too much about the inner workings of other modules. Modules that know too much about other modules make changes hard to coordinate and make modules brittle. If Module A knows too much about Module B, changes to the internals of Module B may break functionality in Module A.

By aiming for low coupling, you can easily make changes to the internals of modules without worrying about their impact on other modules in the system. Low coupling also makes it easier to design, write, and test code since our modules are not interdependent on each other. We also get the benefit of easy to reuse and compose-able modules. Problems are also isolated to small, self-contained units of code.

High Cohesion

We want to design components that are self-contained: independent, and with a single, well-defined purpose—

Cohesion often refers to how the elements of a module belong together. Related code should be close to each other to make it highly cohesive.

Easy to maintain code usually has high cohesion. The elements within the module are directly related to the functionality that module is meant to provide. By keeping high cohesion within our code, we end up trying DRY code and reduce duplication of knowledge in our modules. We can easily design, write, and test our code since the code for a module is all located together and works together.

Low cohesion would mean that the code that makes up some functionality is spread out all over your code-base. Not only is it hard to discover what code is related to your module, it is difficult to jump between different modules and keep track of all the code in your head.

Cohesion

Each class should have one purpose or job, sometimes referred to as its responsibility.

A class has high cohesion when everything inside of it relates to that purpose, without anything extraneous. Perfection is achieved when there is nothing left to take away.

Maintainable Code

Writing maintainable code helps increase productivity for developers. Having highly maintainable code makes it easier to design new features and write code. Modular, component-based, and layered code increases productivity and reduces risk when making changes.

By keeping code loosely coupled, we can write code within one module without impacting other modules. And by keeping code cohesive, we make it easier to write DRY code that is easy to work with.

A good way to determine how cohesive and coupled your code is, is illistrated by this quote from The Pragmatic Programmer:

When you come across a problem, assess how localized the fix is. Do you change just one module, or are the changes scattered throughout the entire system? When you make a change, does it fix everything, or do other problems mysteriously arise?

While you are writing and working with your code base, ask yourself:

  1. How many modules am I touching to fix this or create this functionality?
  2. How many different places does this change need to take place?
  3. How hard is it to test my code?
  4. Can we improve this by making code more loosely coupled? Can this be improved by making our code more cohesive?

Inheritance

Classes can inherit behaviour from another class.

All fruits have a color, a weight, and a name.

Some fruits may have special characteristics that aren't shared with other fruits, so you create a new class that inherits the characteristics of ALL fruits (color, weight, etc.) & then you add the special characteristic.

class Pet

attr_accessor :name, :age

def initialize name, age
      @name = name
      @age = age
end

class Dog < Pet  ##we use the '<' operator to show inheritance

attr_reader :dog_breed

def initialize dog_breed
      @dog_breed = dog_breed
end

Pet is initialized with a name and age, as they are attributes all pets will share. The attr_accessor is there to read and write the values of each attribute for us. By using “<” we are essentially saying that our class “Dog” is inheriting from our “Pet” class. This is what establishes the parent-child relationship. Pet is passing all of its methods and attributes to Dog and Cat, but our child classes have their own properties such as breed.

Module

Modules are a way of grouping together methods, classes, and constants. Modules give you two major benefits:

  1. Modules provide a namespace and prevent name clashes.
  2. Modules implement the mixin facility.

Ruby modules allow you to create groups of methods that you can then include or mix into any number of classes. Modules only hold behaviour, unlike classes, which hold both behaviour and state.

In order to include a module into a class, we use the method include which takes one parameter - the name of a Module.

module WarmUp
  def push_ups
    "Phew, I need a break!"
  end
end

class Gym
  include WarmUp

  def preacher_curls
    "I'm building my biceps."
  end
end

class Dojo
  include WarmUp

  def tai_kyo_kyu
    "Look at my stance!"
  end
end

puts Gym.new.push_ups
puts Dojo.new.push_ups