Skip to content

Instantly share code, notes, and snippets.

@jonnyjava
Last active November 27, 2022 17:27
Show Gist options
  • Save jonnyjava/31886e4b99dd4ababb9ced4ba7323d07 to your computer and use it in GitHub Desktop.
Save jonnyjava/31886e4b99dd4ababb9ced4ba7323d07 to your computer and use it in GitHub Desktop.
Practical Object-Oriented Design in Ruby

Practical Object-Oriented Design in Ruby

1 Introduction

  • OO is about managing dependencies. Objects interact between them exchanging messages
  • Code must follow S.O.L.I.D. principles
    • Single responsibility
    • Open/Closed
    • Liskow substitution
    • Interface segregation
    • Dependency inversion
  • Is very important to collect and work on quality metrics
  • Be DRY
  • Follow the law of Demeter
  • Never try to anticipate future! Implement only the things you know. Specially when designing.

The most important rule is REDUCE COST

2 The single responsibility

Make it simple. Code easy to change tomorrow is better than code working today.

Easy to change means:

  • It should not have unexpected side effects
  • Small requirement changes should require small code changes

Code should be easy to reuse. Code must be T.R.U.E.

  • Transparent
  • Reasonable
  • Usable
  • Exemplary

When wrinting code, interrogate the class to know if it has only one responsibility. Try to describe that class in an unique sentence.

  • Cohesion means having all inside a class related to the same purpose.
  • Is better postpone decision when the future is unknown
  • Is a good practice wrap instace variables into accessor methods
  • Is a good practice hide data structures using STRUCTS
  • Methods must have single responsibility too. So is a good practice separate interaction from action.
  • Good practices reveal design

Isolation is the first step of painless change and easiness of reuse

3 Avoid dependenciness

  • Initialize an object with an instance of another to inject dependencies
  • Isolate external calls into methods
  • Remove order argument using hashes to pass them when possible
  • Define defaults explicitly; is a good practice to do it in a specific method
  • Wrap third parts methods calls
  • The object which purpose is only to create antoher object is a Factory
  • Depend on things that change less often than you do
  • Depending on abstractions is safer

4 Creating flexibles interfaces

  • Object oriented design is made of classes but is defined by messages.
  • The objects methods used by others are its public interface
  • Public interfaces reveal an object responsibility, are more stables because they are an abstraction. So they are sefe to depend on, and they must be thoroughly tested.
  • The interface is the implementation of the contract establishing the object responsibility
  • Classes made by datas and behaviour are called domain objects
  • Should this receiver be responsible for respondin to this message?
  • The system has objects because it needs to send and respond to messages
  • I know what you want and I trust you will do your part
  • Respect the law of Demeter: use delegation

5 Duck typing

  • Is not about what an object is but about what it does
  • Find the duck! Use sequence diagrams
  • Polymorphism refers to the ability of different objects to respond to the same message
  • It smells like a duck when there are:
  • Case statements
  • responds_to
  • is_a
  • Any other sentence checking the type of the object
  • A good duck reduces unstables dependencies

6 Inheritance

  • Inheritance means automatic message delegation. When an object inherits from another, and receives something it does not understand, it will forward that message to its ancestors.
  • Subclasses are specializations of their superclasses
  • A subclass is all what a superclass is, plus something more
  • Superclasses should exists only to be inherited. Superclasses should be abstracts.

The template method pattern

Any class that uses the template method pattern must supply an implementation for every message it sends. Is good practice stating explicitly all the mandatory implementations with error messages (which are good documentation too!).

Is a best practice decouple subclasses providing **hook messages. An hook provides to subclasses a place to extend some implementation without knowing its algorithm.

Inheritance allows to isolate shared code, forces subclasses to implement specialization through the template method pattern and hides implementation with hooks messages.

7 Modules

Classical inheritance is always optional. Every problem it solves, can be solved in another way.

Sometimes, unrelated objects share a role. A role is a group of methods indipendents of any objects so they can be added to anyone. In ruby these mix-ins are called modules

A module provides automatic delegation too. Objects should manage themselves, they should contain their behaviour.

Modules must follow the same good practices rules of inheritance, for example if a module sends a message it must provide an implementation even if it is a simple error raise.

The difference between modules and classical inheritance stay in the kind of realtionship between entities: classical inheritance represent an is-a relationship specialization, modules represents a behaves-like-a relationship.

The lookup chain

  • Class methods overrides module ones
  • include is for instance methods, extend for class ones

Writing inheritable code

  • Use classical inheritance to refactor code checking object type category to decide which message send to self
  • Use a duck type or a module to refactor code checking to who send the message
  • Superclasses should not contain code that applies to only some and not all subclasses. The same is valid for modules because if a subclass (or a role player in case of module) does not implement that method probably it means it is-not-a or not-behaves-like-a
  • To check if Liskov is respected, subclasses must honor the contract established by the interface. A subclass should be able to be exchanged with its superclass.
  • Use the Template method pattern
  • Use hook messages
  • Shallow narrow hierarchies are easier to understand
  • An object should act like what it claims to be

The whole point of dependency injection is that it allows to substitute different concrete clsses without changing the code.

8 Composition

  • Factories are objects that create other objects
  • Do not forget to use Struct and OpenStruct

A composed object is made of parts interacting between them via established interfaces.

Composition is a has-a / has-many realationship. A component can't live outside the composed object

An aggregated object is a composed object where components can live by their own

Inheritance VS composition

  • Use inheritance when is not possible to use composition, for example in a is-a relationship
  • Use duck types in behaves-like-a relationships
  • Use composition for has-a relationship

Inheritance is specialization, composition is a whole that is more than its sum of parts

Inheritance

  • Small changes in the code can produce big changes in the behaviour
  • Classes in inheritance chain are open to exptension and closed for modification
  • Is easy to create subclasses to specialize behaviours
  • Subclasses have always a dependency to their superclasses

Composition

  • Smaller objects, which have more obvious behaviour
  • There is not an automatic message delegation. the composed object must know to who to send which message

9 Design cost effective tests

Master OOP to write code easy to change, because the easyness to change is the best metric to judge a well designed code.

Refactor means improve the design of the existing code without altering its behaviour

Write test Tests let to reduce costs because they:

  • Find and fix bugs
  • Document the code
  • Help to defer design decisions
  • Support abstractions: each individual abstraction is easy to understand, but there is not a single place in the code able to explain the abstraction of the whole. This level of abstraction is impossible to change without tests
  • Expose design flaws. If a test is hard to write, the code is hard to reuse (and probably to understand)

Test messages and not internals. Messages can be:

  • queries: outgoing messages with no side effects. In this case the test goes on the receiver
  • commands: outgoing messages changing something. In this case the test goes on the sender
  • states: incoming message returning a state. In this case the test goes on the receiver

Poorly designed code without test is just legacy code.

Use tests to document roles

Carefully choose between mocks, doubles and real objects when testing to avoid false positives.

Mocks are useful to test behaviour because they define an expectation that a message will be sent

Doubles return a known response.

The best way to test Liskov is write shared tests and include them in every subclasses test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment