Baixe o app para aproveitar ainda mais
Prévia do material em texto
Table of Contents 1. Introduction 2. Basics of Abstraction 3. Single Purpose Principle 4. Stepwise Refinement 5. Dependency Inversion Principle 6. Basic Three Rules of Design 7. The Art of Uniform Interface 8. Localized Change vs Additive Change 9. Coupling Basics : Dependency Direction 10. Concrete Class vs Abstract Messages 11. Flexible Design 12. Open Closed Principle Essential Object Oriented Design This book covers the basic Object Oriented Design concepts using Ruby programming language. The goal is to provide a solid foundation to build upon. This book distills my Object Oriented Design knowledge into a concise and easy-to-read format. Repetition is key to learning Ruby. We will visit the concepts from different angles. You will get the most benefit out of the book if you work through every example as you read through the book. This book uses Ruby 2.3.0. About the Author Bala Paranj has a Master's degree in Electrical Engineering from Wichita State University. He began working in the IT industry in 1996. He started his career as a Technical Support Engineer and then became a Web Developer using Perl, Java and Ruby. He is available for freelance work. Please contact him at support@zepho.com or via Ruby Plus. He is also working on screencasts based on this book. If you want notification about the release, please contact him. https://www.rubyplus.com/contact Basics of Abstraction In this chapter, you will learn the basics of abstraction using examples. Definition The dictionary says abstraction means to draw away , to remove characteristics from something in order to reduce it to a set of essential characteristics. It is a tool for simplification. We find the essence by ignoring irrelevant details. In his paper Is Abstraction the Key to Computing?, Jeff Kramer says abstraction also means: The process of formulating general concepts by abstracting common properties of instances, and; A general concept formed by extracting common features from specific examples. Example 1 Chemistry is an abstraction of physics. VS Biology is an abstraction of chemistry. VS Genetics is an abstraction of biology. VS Example 2 The London Underground map that overlays the underground system onto a conventional geographical map. In this map, you can see: River Thames Relative distances between stations. Harry Beck's simplified map. This fits the purpose of navigating around the London Underground. It is misleading for other purposes. Let's consider two extremes: Too abstract The map would not provide sufficient information for the purpose. Too detailed The map becomes confusing and less comprehensible. The level, benefit and value of a particular abstraction depend on its purpose. Abstraction in Software To quote Booch in Object-Oriented Analysis and Design with Applications: An abstraction denotes the essential characteristics of an object that distinguish it from all other kinds of objects and thus provide crisply defined conceptual boundaries, relative to the perspective of the viewer." The term perspective of the viewer needs an explanation. Let us consider a House object, when a banker sees this house, he thinks in terms of the value of the property, opportunity for appreciation, etc whereas when a decorator views it, he thinks in terms of what color the house should be painted, total area to be painted, etc. The same object House can be viewed from different perspectives and can lead to entirely different abstractions by different people. Booch, Fairsmith, Henderson-Sellers define abstraction as: Any model that includes the most important, essential, or distinguishing aspects of something while suppressing or ignoring less important, immaterial, or diversionary details. Coad, Fairsmith, Henderson-Sellers, Rumbaugh define abstraction as: The cognitive tool for rationalizing the world by considering only those details necessary for the current purpose. So, abstraction is about what details we choose to emphasize and what details we choose to ignore. What we choose to emphasize is dictated by the application. It simplifies the things that we look at in the real world. For example, a chair can be made up of different kinds of material, height adjusting knobs, reclining adjustment knobs etc. If every time we looked at the chair, if we had to deal with what material it is made up of, how the height adjustment knobs are designed and other irrelevant details related to our purpose using a chair to sit, our brains will be exhausted. So, the abstraction process simplifies things and allows us to manage complexity during problem solving process. Computer science is a science of abstraction — creating the right model for thinking about a problem and devising the appropriate mechanizable techniques to solve it. Every other science deals with the universe as it is. The physicist’s job, for example, is to understand how the world works, not to invent a world in which physical laws would be simpler or more pleasant to follow. Computer scientists, on the other hand, must create abstractions of real-world problems that can be understood by computer users and, at the same time, that can be represented and manipulated inside a computer. Abstraction in the sense we use it implies simplification, the replacement of a complex and detailed real-world situation by an understandable model within which we can solve a problem. That is, we “abstract away” the details whose effect on the solution to a problem is minimal or nonexistent, thereby creating a model that lets us deal with the essence of the problem. -- From the book 'Foundations of Computer Science' by Alfred V Aho and Jeffrey D Ullman Why Abstraction? Abstraction is crucial to produce clear, elegant designs and programs. It is useful to manage complexity. We can diagnose components at the interfaces rather than by exhaustively tracing functions of all components. Advantages in Treating Systems by Levels of Abstraction Each level has its own definition and specification. So development can proceed concurrently at each level. We can allocate work according to strength. A system can evolve by evolving components separately. It is not necessary to re-implement the entire system when one component changes. This avoids ==Second System Syndrome==. Abstraction in Daily Life You use abstraction in everyday things in your life. For instance you say : I am going to a Rock Concert this weekend. You don't say: "I am going to a musical performance characterized by electric guitar, electric bass guitar and drums this weekend". How to Abstract In order to learn the process of abstraction you need to learn how to find the essence of something. Oxford dictionary defines essence as : The intrinsic nature or indispensable quality of something that determines its character. Let's now think about the following question to illustrate finding the essence. What is the essence of a chair? A chair is a thing and it has a form and function. If you assume sitting as the function of a chair, then you have the attributes such as number of legs, material of the chair, whether it has a support for the back and so on as the variables that can be varied in the chair definition. The question now is what is the least amount of these attributes we need but still retain the concept of chair? Can a chair have no support for the back? Yes. So we can consider this as irrelevant to the chair concept. We can continue this process for other attributes to come up with the essence of a chair. Leaky Abstraction Abstraction that is leaky will force us to look at implementation to learn about the usage of the API. This is like looking under the hood of your car and understanding the working of the internals of the car engine in order to learn how to drive the car. Conclusion In this article we discussed the what and the why of abstraction. Abstraction is one of the most important concepts thatis taught in Computer Science. But developers still find it difficult to apply it in software development. Exercises What is the essence of a pen? What is the essence of a car? References [Abstraction in Computer Science & Software Engineering: A Pedagogical Perspective] (https://edu.technion.ac.il/Faculty/OritH/HomePage/FrontierColumns/OritHazzan_SystemDesigFrontier_Column5.pdf 'Abstraction in Computer Science') [Is Abstraction the Key to Computing?] (https://www.ics.uci.edu/~andre/informatics223s2007/kramer.pdf 'Is Abstraction the Key to Computing') Computational Thinking https://edu.technion.ac.il/Faculty/OritH/HomePage/FrontierColumns/OritHazzan_SystemDesigFrontier_Column5.pdf https://www.ics.uci.edu/~andre/informatics223s2007/kramer.pdf https://www.cs.cmu.edu/~15110-s13/Wing06-ct.pdf Single Purpose Principle In this chapter you will about Single Purpose Principle. A class should capture one and only one key abstraction Object Oriented Design Heuristics by Arthur Riel Where Did this Confusion Begin? Robert C. Martin came up with the Single Responsibility Principle in 1995. The problem is CRC card already had used the term Responsibility to mean something different. CRC cards came way back in 1989. Also Robert Martin redefined the term responsibility to mean a reason to change. He basically took the concept of cohesion and added his own insights to come up with Single Responsibility Principle. The main point of this principle is that there should be only one reason to change. I don't want to call it Single Responsibility Principle because of the confusion it creates with the existing terminology. Since this principle says that the set of responsibilities (as used by CRC cards) should be focused on one purpose, I think it is appropriate to call it Single Purpose Principle. Single Purpose Unix is a perfect example for following the Single Purpose Principle. Each command line utility does one thing really well. It proves that good design is timeless. How to Recognize Violation of Single Purpose Principle? Let's discuss the examples that are used in Robert C. Martins's SRP: The Single Responsibility Principle. Example 1 : Bowling Game - Keep track of frames Scorer - Calculate the score Each of the above is an axis of change. What is an axis of change? How do you recognize different axes of change? He does not explain in his paper. It is confusing. If you ask yourself the questions: 1. Does the operations operate on the data most of time? 2. Is the data and operations on it related together? 3. Is this class highly cohesive? then it will be easy to recognize that we need to separate the Game and Scorer objects. In this example the abstractions are consistent and they are at the application level. Example 2 : Rectangle Application 1 : Computational geometry Application 2 : Graphical in nature These are different abstractions. One is at the UI level and the other is at the domain level. The abstractions must be consistent, you cannot mix different levels of abstraction into one class. He says : In the context of the Single Responsibility Principle (SRP) we define a responsibility to be a reason for change . If you can think of more than one motive for changing a class, then that class has more than one responsibility. This is sometimes hard to see. Yes, it is hard to see because, you can always say that there is only one reason to change when in reality there is more than one reason. This happens when you mix different levels of abstraction. For instance, I worked on a project where I had to upload files to Amazon S3. The requirements demanded that I configure the number of threads that can upload files at once to S3. I wrote threading library that did not have any dependency on the S3 file uploader. The S3 file uploader was not aware that it was used by multiple threads. The plumbing code was separated from the actual task in the application. Later I can replace the threading library with a library that uses Celluloid to abstract away the plumbing code. This change will not impact the S3 file uploader. Example 3 : Modem This example also mixes different levels of abstraction. Connection : Plumbing level abstraction (dial and hangup) Data Channel : Application level abstraction (send and receive) The plumbing and application level abstractions are not separated. Example 4 : Employee Employee class with calculate_pay , save , report_hours methods violates the SRP. In this case, he argues that the people who request changes for the business rules are different from those who request changes to the saving functionality related to the database. It is very clear in this example also that the Employee class mixes different levels of abstraction. We need to separate persistence level abstraction from application level abstraction. By applying Separation of Concerns we can easily recognize that the class has more than one purpose. His Conclusion The SRP is one of the simplest of the principle, and one of the hardest to get right. Conjoining responsibilities is something that we do naturally. Finding and separating those responsibilities from one another is much of what software design is really about. My Thoughts Another flaw in his reasoning can be illustrated by using the Amazon S3 uploader I mentioned earlier. In this case, the stake holders who would demand changes due to threading is not different from file uploading aspect. So since the stake holder is the same you would think it follows Single Purpose Principle. But that is wrong. Because we know that we are mixing different levels of abstraction and it can change due to two different reasons: plumbing related code and the file uploading related code. Focus on keeping abstractions consistent. Do not mix different levels of abstraction. It will be easier to make better design decisions. You can read Code Complete 2 by Steve McConnell for more detailed explanation. Summary In this chapter you learned about Single Purpose principle. If we apply Single Purpose Principle throughout the system we will obey the Separation of Concerns principle and the system will be organized into different layers. Each layer will be focused on fulfilling one purpose such as Object Relational Mapping layer, data conversion for external system and so on. For a good discussion on this topic, read The Art of Separation of Concerns. http://aspiringcraftsman.com/2008/01/03/art-of-separation-of-concerns/ Stepwise Refinement In this chapter, you will learn about Stepwise Refinement. Problem I am the laziest professor ever. I've given a multiple choice test (A-D are valid answers) but I lost my test key. I'll assume that whatever answer was most popular for a question is the correct answer. I've included a file that contains the students quiz answers. Each row represents one student. Write code that figures out the most popular answer to each question and then prints out the key. Example Given: ABCCADCB DDDCAACB ABDDABCB AADCAACC BBDDAACB ABDCCABB ABDDCACB Output: ABDCAACB Analysis Question # : 1 2 3 4 5 6 7 8 Answers : ? There are seven students. There are eight questions. Steps Step 1 1. Figure out the most popular answer to each question 2. Print out the key Step 2 1. Figure out the most popular answer to each question for each of the 8 questions Find the most popular answer Save the answer end 1. Print out the key Print the answer for each question in the saved answer key Step 3 1. Figure out the most popular answer to each question for each of the 8 questions Find the most popular answer Possible Answers are : A, B, C, D Initialize the number of answers to 0 for all the possible answers For each of the 7 students Check the answer by the student Increment the count for the answer end Save the answer end 1. Print out the key Print the answer for each question in the saved answerkey This is the blueprint for our program. This blueprint can be used to code the solution in any language. Step 4 What is the structure of the input? Let's assume an array of answers arranged by students: ['ABCCADCB','DDDCAACB', 'ABDDABCB', 'AADCAACC', 'BBDDAACB', 'ABDCCABB', 'ABDDCACB'] Step 5 @submissions = ['ABCCADCB','DDDCAACB', 'ABDDABCB', 'AADCAACC', 'BBDDAACB', 'ABDCCABB' answer_key = [] def find_most_popular_answer_for(question) key = Hash.new(0) for submission in @submissions key[submission[question]] += 1 end key.max_by{|k,v| v} end for question in (0..7).to_a answer_key << find_most_popular_answer_for(question) end puts answer_key After I wrote the program, I had to add max_by to retain only the highest scoring answer for the given question. You can always go back and refine the step when required. Summary Stepwise Refinement is a useful technique for solving problems. We can start with the 'What' and gradually move towards the 'How' and finally code the solution for a given problem. Resources Lazy Professor Program Development by Stepwise Refinement by Niklaus Wirth https://github.com/SeaRbSg/braincandy/blob/master/lazy-professor/README.md Dependency Inversion Principle In this chapter, we will explore Dependency Inversion Principle by examples. Steps Step 1 Let's write a program to : 1. Read from keyboard. 2. Write to a terminal. Here is the program: def copy message = gets puts message end copy We can run this: $ ruby copy.rb Hello Hello Step 2 We now want to accommodate changes to our program that can: 1. Read from a tape reader 2. Write to a terminal Here is the modified program: def read_tape p 'Input from tape reader' end def copy(tape_reader = false) if tape_reader message = read_tape else message = gets end puts message end copy(true) We can run it: $ ruby copy.rb Input from tape reader Input from tape reader Step 3 We now need to accommodate changes to our program that can: 1. Read from tape reader as before 2. Write to paper tape punch Here is the modified program: def read_tape p 'Input from tape reader' end def write_to_paper_tape_punch(output) p 'Output to paper tape punch' end def copy(tape_reader = false, paper_tape_punch = false) if tape_reader message = read_tape else message = gets end if paper_tape_punch write_to_paper_tape_punch(message) else puts message end end copy(true, true) The solution is becoming messy with lot of conditionals. When we add more types of input and output coding down this path will lead to very difficult to maintain codebase. Applying Good Design Principles We will apply the following three basic principles: - Separate things that change from things that stays the same. Encapsulate what varies behind a well- defined interface. - Program to interfaces, not implementations. This exploits polymorphism. - Depend on abstractions. Do not depend on concrete classes. The input device and output device can change. We can encapsulate them behind the interfaces named read and write. The code must use the newly defined read and write interfaces. This results in the code depending on abstractions. class Copier def initialize(reader, writer) @reader, @writer = reader, writer end def copy message = @reader.read @writer.write(message) end end class KeyboardReader def read gets end end class PrinterWriter def write(output) p "Writing #{output} to printer" end end Copier class now depends on stable interface read and write. The implementation of KeyboardReader and PrinterWriter is now hidden behind the well defined read and write interface. We can use this new design to copy from any input source to any output source without modifying the Copier class. reader = KeyboardReader.new writer = PrinterWriter.new copier = Copier.new(reader, writer) copier.copy We can run this program. $ ruby copy.rb This is a test "Writing This is a test\n to printer" Dependency Inversion Principle It states: - Details should depend on abstractions. - Abstractions should not depend on details. - High level modules should not depend upon low level modules. Both should depend upon abstractions. Compare the dependency of: Copy Read Keyboard WriterPrinter vs Copy Reader : KeyboardReader Writer : PrinterWriter The interface in KeyboardReader and PrinterWriter is implicit in Ruby. We conform to the read and write interface instead of explicitly saying we implement those interfaces in the code. The copier class is reusable with different input and output devices. Code Breaker Game Example Let's look at the code example for Codebreaker game from The Rspec Book. The solution in the book does not apply the DIP. Before module Codebreaker class Game def initialize(output) @output = output end def start(secret) @secret = secret @output.puts 'Welcome to Codebreaker!' @output.puts 'Enter guess:' end end end g = Codebreaker::Game.new($stdout) g.start('sekret') This is a good example that shows using BDD or TDD does not make the design emerge magically. You must apply good design principles consciously and deliberately. Let's look at the solution after applying the DIP. After module Codebreaker class Game def initialize(writer) @writer = writer end def start(secret) @secret = secret @writer.write 'Welcome to Codebreaker!' @writer.write 'Enter guess:' end end end class StandardConsole def write(message) $stdout.puts(message) end end writer = StandardConsole.new g = Codebreaker::Game.new(writer) g.start('sekret') It's surprising to find coding solution from a published book that has gone through technical review come up short in design. Summary In this chapter, we applied the three basic principles of good design: 1. Separate things that is likely to vary 2. Hide them behind a well defined interface 3. Write your program to use the stable interfaces instead of depending on concrete details. to come up with a flexible design that is easy to maintain and extend. The Three Basic Rules for a Good Design In this chapter, you will learn how to apply basic object oriented design principles. Problem Write a program that: Loads a set of employee records from a flat file. Sends a greetings email to all employees whose birthday is today. The flat file is a sequence of records, separated by newlines; these are the first few lines: last_name, first_name, date_of_birth, email Doe, John, 1982/10/08, john.doe@foobar.com Ann, Mary, 1975/09/11, mary.ann@foobar.com The greetings email contains the following text: Subject: Happy birthday! Happy birthday, dear <first_name>! where first_name is the place holder for first name. Object Oriented Design Basic Principles We will apply the following three basic principles: Separate things that change from things that stays the same. Encapsulate what varies behind a well-defined interface. Program to interfaces, not implementations. This exploits polymorphism. Depend on abstractions. Do not depend on concrete classes. High Level Steps 1. Read data.txt file. 2. Check if date of birth is today. 3. Send greetings email if today is the person's birthday. Step 1 Let's read a CSV file that contains data. file_name = "data.txt" text = open(file_name) print text.read Refine the Step 1 1. Read data.txt CSV file. Skip the header Here is our simple program that we can start playing with: require 'csv' file_name = "#{Dir.pwd}/data.txt" data = CSV.read(file_name, {headers: true}) data.each do |x| p x end Refine the Step 2 1. Read data.txt CSV file Skip the header 2. Check if date of birth is today Retrieve the third column Remove the spaces at the ends Check if month and date is the sameas today's month and date If yes, return the person's first name 3. Send greetings email if today is birthday Refine the Step 3 1. Read data.txt CSV file Skip the header 2. Check if date of birth is today Retrieve the third column Remove the spaces at the ends Check if month and date is the same as today's month and date If yes, return the person's first name 3. Send greetings email Use Gmail, Sendgrid, Pony etc. Before The program written that does not apply the 3 design principles looks like this: require 'csv' file_name = "#{Dir.pwd}/data.txt" data = CSV.read(file_name, {headers: true}) data.each do |x| birth_date = x[2].strip! month = birth_date[5..6] day = birth_date[8..9] if (month.to_i == Date.today.month) and (day.to_i == Date.today.day) p x[1].strip email = <<EMAIL_TEXT Subject: Happy birthday! Happy birthday, dear #{x[1].strip}! EMAIL_TEXT p email end end Analysis Responsibilities This program has the following responsibilities: 1. Parsing CSV file 2. Checking if today is the birthday of a person 3. Sending email Things that can Change Let's make a list of things that can change. 1. Input data source 2. How greetings is sent Things that Stays the Same Let's make a list of things that stays the same. 1. Logic to find if someone's birthday is today. Redesign Step 1 The PersonFileStore class will have records method that will return a list of Person objects. The code that applies what we did in analysis now looks like this: require 'csv' # This is a domain object # This object has no dependency on other objects. It is agnostic to storage mechanism class Person attr_reader :first_name def initialize(first_name, last_name, date_of_birth, email) @first_name = first_name @last_name = last_name @date_of_birth = date_of_birth @email = email end def birth_day @date_of_birth[8..9] end def birth_month @date_of_birth[5..6] end end # This class knows how to parse the CSV file to create Person objects # The direction of dependency is from PersonFileStore to the domain object class PersonFileStore def initialize(file) @file = file end def records result = [] data = CSV.read(@file, {headers: true}) data.each do |x| person = Person.new(x[1], x[0], x[2].strip!, x[3]) result << person end result end end # This section of the code is not yet cleaned up. pfs = PersonFileStore.new("#{Dir.pwd}/data.txt") records = pfs.records records.each do |person| month = person.birth_month day = person.birth_day if (month.to_i == Date.today.month) and (day.to_i == Date.today.day) email = <<EMAIL_TEXT Subject: Happy birthday! Happy birthday, dear #{person.first_name}! EMAIL_TEXT p email end end Step 2 Let's extract the logic to find if anyone has a birthday today. # Person and PersonFileStore classes is same as before. # This class encapsulates the logic to find out if the birthday is today or not. # It has no dependency on other objects class BirthDay def initialize(month, day) @month = month @day = day end def today? (@month.to_i == Date.today.month) and (@day.to_i == Date.today.day) end end pfs = PersonFileStore.new("#{Dir.pwd}/data.txt") records = pfs.records records.each do |person| month = person.birth_month day = person.birth_day birth_day = BirthDay.new(month, day) if birth_day.today? email = <<EMAIL_TEXT Subject: Happy birthday! Happy Birthday, Dear #{person.first_name}! EMAIL_TEXT p email end end Step 3 Let's extract sending greeting. # Person, PersonFileStore and Birthday classes is same as before. # Sending email to the console output is encapsulated within the send interface class GreetingConsole def initialize(message, email) @message = message @email = email end def send p "Sending email to : #{email}" p @message end end # The following code is the client code pfs = PersonFileStore.new("#{Dir.pwd}/data.txt") records = pfs.records records.each do |person| month = person.birth_month day = person.birth_day birth_day = BirthDay.new(month, day) if birth_day.today? message = <<EMAIL_TEXT Subject: Happy Birthday! Happy Birthday, Dear #{person.first_name}! EMAIL_TEXT # Client is tied to a specific implementation of sending an email message # This needs to change to GreetingEmail.new(message), greeting.send to send email greeting greeting = GreetingConsole.new(message) greeting.send end end Step 4 Let's add an in-memory data source and make it work. class PersonMemoryStore def records result = [] person = Person.new('Bugs', 'Bunny', '1982/10/06', 'bbunny@rubyplus.com') result << person person = Person.new('Daffy', 'Duck', '1975/09/11', 'dduck@rubyplus.com') result << person result end end # The following code is the client code pfs = PersonMemoryStore.new records = pfs.records records.each do |person| month = person.birth_month day = person.birth_day birth_day = BirthDay.new(month, day) if birth_day.today? message = <<EMAIL_TEXT Subject: Happy Birthday! Happy Birthday, Dear #{person.first_name}! EMAIL_TEXT # Client is tied to a specific implementation of sending an email message # This needs to change to GreetingEmail.new(message), greeting.send to send email greeting greeting = GreetingConsole.new(message) greeting.send end end Notice that the PersonMemoryStore has the same interface as the PersonFileStore class. In a real project, we could use SQLite in-memory database. Step 5 Let's add a different way to send email by using Pony gem. require 'pony' # Sending a real email using Pony gem class GreetingPony def initialize(message, email) @message = message @email = email end def send Pony.mail(:to => @email, :from => 'admin@rubyplus.com', :subject => 'Happy Birthday!' end end After Redesign The channel folder has greeting_console.rb and greeting_pony.rb classes. The GreetingConsole class looks like this: # Sending email to the console output is encapsulated within the send interface class GreetingConsole def initialize(message, email) @message = message @email = email end def send p "Sending email to : #{@email}" p "Subject : Happy Birthday!" p @message end end Here is the GreetingPony class: require 'pony' # Sending a real email using Pony gem class GreetingPony def initialize(message, email) @message = message @email = email end def send Pony.mail(:to => @email, :from => 'admin@rubyplus.com', :subject => 'Happy Birthday!' end end The domain folder contains the BirthDay and Person classes. Here is the BirthDay class: require 'date' # This class encapsulates the logic to find out if the birthday is today or not. # It has no dependency on other objects class BirthDay def initialize(month, day) @month = month @day = day end def today? (@month.to_i == Date.today.month) and (@day.to_i == Date.today.day) end end Here is the Person class: # This is a domain object # This object has no dependency on other objects. It is agnostic to storage mechanism class Person attr_reader :first_name, :email def initialize(first_name, last_name, date_of_birth, email) @first_name = first_name @last_name = last_name @date_of_birth = date_of_birth @email = email end def birth_day @date_of_birth[8..9] end def birth_month @date_of_birth[5..6] end end The source folder contains person_file_store.rb and person_memory_store.rb . require 'csv' require_relative '../domain/person' # This class knows how to parse the CSV file to create Person objects # The direction of dependency is from PersonFileStoreto the domain object class PersonFileStore def initialize(file) @file = file end def records result = [] data = CSV.read(@file, {headers: true}) data.each do |x| person = Person.new(x[1], x[0], x[2].strip!, x[3]) result << person end result end end You can see that we need the require_relative statement, since it has dependency on the Person domain object. The PersonMemoryStore class looks like this: require_relative '../domain/person' # This class provides in-memory implementation of the data source interface # Useful in writing tests class PersonMemoryStore def records result = [] person = Person.new('Bugs', 'Bunny', '1982/10/08', 'bbunny@rubyplus.com') result << person person = Person.new('Daffy', 'Duck', '1975/09/11', 'dduck@rubyplus.com') result << person result end end Visual Representation You can download the final refactored code that has a better design here: [Ruby Greeter] (https://bitbucket.org/bparanj/greeter 'Ruby Greeter') https://bitbucket.org/bparanj/greeter Summary We separated the input data source that can change into it's own source folder. We encapsulated it behind a well-defined interface. We did the same for different ways to send birthday greetings by moving all the relevant classes to the channel folder. We depend on the send method for greeting delivery and records method for data source, so we program to the interface. The glue code in main.rb that uses the classes in the channel, domain and source folders depends on concrete classes. You can use dependency injection and vary the input source and the channel to make them depend on abstractions instead of concrete classes. Reference The birthday greetings kata http://matteo.vaccari.name/blog/archives/154 The Art of Uniform Interface Context After reading how the first, second, third and so on methods were added in the Rails doctrine by DHH, I started to look for ways on how I would actually go about doing something better. Obviously the drawback of defining methods like that is that they are not scalable. But, I see his point of not indexing into an array to get data for some common use cases. Challenge How can we come up with a uniform interface that does not result in explosion of method and at the same time achieves ignorance of indexing into an array? Ruby, has values_at method for an array. > a = [1,2] => [1, 2] > a.values_at(1) => [2] > a.values_at(0) => [1] This exposes the knowledge of the indexing of an array. It also knows that is an array. Ruby also has values_at method for hash. h = { "cat" => "feline", "dog" => "canine", "cow" => "bovine" } h.values_at("cow", "cat") #=> ["bovine", "feline"] This exposes the knowledge that is an hash. You need to know the key in the hash to get the corresponding value. Well Defined Interface I don't care about the type of data structure, just give me the element in the given position. class Array def element(position) self[position - 1] end end p [4,5,6].element(3) You can just provide the position as the parameter to element method instead of going through the mental mapping of position and index of an array. Similarly, we can define a element method for Hash. class Hash def element(position) a = self.to_a[position-1] {a[0] => a[1]} end end h = {a: 1, b: 2, c: 3, d: 4} p h.element(2) This returns : {b: 2} Both implementation are data structure agnostic. It does not force the developers to map the position of an element and the index in array or knowing the key of a hash. Principle of Least Surprise I was surprised when I tried to call element method on Array and Hash and I got errors. Ruby has failed in this case. It only has first and last method defined. Summary In this chapter, we saw how we can define a uniform interface that hides the data structure and knowledge required to pass in parameters to a method. This solution eliminates parametric coupling. Localized Change vs Additive Change To learn how to eliminate conditionals and come up with a good object oriented design. Transformer Let's write a program to transform a json string or a binary format string. Steps Step 1 The transformer that uses conditional: require 'json' class Transformer def initialize(string) @string = string end def transformed_string(type) if type == :json JSON.parse(@string) elsif type == :binary @string.unpack('B*').first end end end t = Transformer.new('Hello') x = t.transformed_string(:binary) p x This prints: 0100100001100101011011000110110001101111 Step 2 Let's eliminate the conditional in transformed_string method: require 'json' class Transformer def initialize(string) @string = string end def transformed_string(transformation) transformation.transform(@string) end end class JSONTransformation def self.transform(string) JSON.parse(string) end end y = Transformer.new('{"foo": "bar"}').transformed_string(JSONTransformation) p x This prints : { "foo" => "bar" } Step 3 Let's now implement the binary transformation: require 'json' class Transformer same as before end class BinaryTransformation def self.transform(string) string.unpack('B*').first end end x = Transformer.new('Hello').transformed_string(BinaryTransformation) p x This prints: 0100100001100101011011000110110001101111 Step 4 Let's change the setter dependency injection to constructor dependency injection and cleanup the code a bit: require 'json' class Transformer def initialize(type) @type = type end def transform(string) @type.transform(string) end end class BinaryTransformation def transform(string) string.unpack('B*').first end end transformer = Transformer.new(BinaryTransformation.new) x = transformer.transform('Hello') p x This prints: 0100100001100101011011000110110001101111 Step 5 We can implement the JSON tranformation: class JSONTransformation def transform(string) JSON.parse(string) end end t = Transformer.new(JSONTransformation.new) y = t.transform('{"foo": "bar"}') p y Step 6 Let's implement MD5 for fun: require 'digest' class MD5Transformation def transform(string) Digest::MD5.hexdigest string end end s = Transformer.new(MD5Transformation.new) z = s.transform('Hello') p z we eliminated conditionals and replaced it with well-defined abstraction. We went from isolated changes to additive changes to implement new functionality. In this case trasform method. This is the main theme of Design Patterns: Elements of Reusable Object-Oriented Software book. Step 7 We can do even better by replacing these anemic classes with blocks. require 'json' class Transformer def transform(string) yield(string) end end binary_transformer = ->(x) { x.unpack('B*').first } transformer = Transformer.new a = transformer.transform('Hello', &binary_transformer) p a json_transformer = ->(y) { JSON.parse(y) } b = transformer.transform('{"foo": "bar"}', &json_transformer) p b require 'digest' md5transformer = ->(z) { Digest::MD5.hexdigest(z) } c = transformer.transform('Hello', &md5transformer) p c Summary In this chapter, we used dependency injection to vary the implementation for transforming a given string. We improved the design by going from Localized Change to Additive Change. The examples used in this article is based on SOLID Review: Dependency Inversion Principle. http://www.runtime-era.com/2015/04/solid-review-dependency-inversion.html Coupling Basics : Dependency Direction Inversion of Control Principle The Hollywood Principle is another name for Inversion of Control Principle. Hollywood Principle: Don't call us, we'll call you. Martin Fowler says: IoC is about who initiates the call. If your code initiates a call, it is not IoC, if the container/system/library calls back into codethat you provided it, it is IoC. Example #1 for Hollywood Principle: class Car def initialize(name) @name = name end def to_s "My name is #{@name}" end end c = Car.new('Tesla') print c This prints: My name is Tesla We did not explicitly call to_s in our code. The to_s method is called by print. Example #2 for Hollywood Principle: In a Rails app: def new @user = User.new end You implement the action in the controller, the framework calls this method. You as a programmer never call the new action in your code. Why Apply Inversion of Control? Inversion of Control principle helps us to achieve loose coupling thereby allowing us to achieve re-use in our projects. It is one of the ways to achieve Context Independence. Summary In this article you learned about the Inversion of Control Principle and examples illustrating their use. The most important take away is that Inversion of Control is about DIRECTION of messages and it can be used to achieve Context Independence. Concrete Class vs Abstract Messages In this chapter, you will learn about depending on abstract messages instead of concrete classes to write re-usable code. Steps Step 1 Create copy.rb : while line = gets puts line end Step 2 Run it. $ruby copy.rb Here is a sample run: Hi Hi Hello Hello Press Ctrl+D to quit the program. This reads from keyboard and writes to the console. Step 3 Create a readme.txt file: This is the first line This is the second line Now run the program as follows: $ruby copy.rb readme.txt This reads the readme.txt from the file and writes it to the console. Step 4 Create file_copy.rb : File.open('./readme.txt', 'r') do |file| while line = file.gets puts line end end Run it. $ruby file_copy.rb This reads the readme.txt and writes it to the console. Step 5 Create abstract.rb with: require 'stringio' ip = StringIO.new('This is a test') op = StringIO.new('', 'w') ip.each_line do |line| op.puts line end print op.string This program uses StringIO a fake file system to read and write. This is good for testing. The dependency is on the message: each_line and puts and not on any concrete class. If there is a name of the class in the code, it creates tight coupling in the code and makes it difficult to reuse. As long as the ip and op variables can respond to the each_line and puts messages. This program will work. Depend on messages that capture abstractions which can be varied by having different implementations. Summary In this chapter, we saw examples for: Standard In --> Standard Out Keyboard --> Console File --> Console Fake File --> Fake Console You learned that depending on a concrete class creates tight coupling and depending on messages that can be implemented by different implementations in different class can lead to re-usable code. Flexible Design Software does not exist in a vacuum. It interacts with environment and the environment interacts with it. The environment is market forces, users, external systems, operating systems, competing software, changes in law etc. It evolves, either it improves or decays over time. The only thing that is constant is change demanded by the environment. The Law of Change: The longer your program exists, the more probable it is that any piece of it will have to change. Max Kanat- Alexander in Code Simplicity book You need to work on a existing code base in order to : 1. Improve Performance 2. Improve the Design 3. Fix Bugs 4. Enhance existing features 5. Upgrade to newer software it depends on 6. Add features 7. Remove features How do we evaluate a design that will make the software easy to do all of the above? If we order from the most to least desirable ways to achieve quality, they are: 1. Data driven or meta-programming 2. Additive change 3. Localized modification. Just because meta-programming is on the top of the list, I am not advocating that is the first choice for every design problem. As long as meta- programming is used to achieve a good design that obeys good design principles, it is ok. Remember the two golden rules from Code Simplicity book: It is more important to reduce the effort of maintenance than it is to reduce the effort of implementation. The effort of maintenance is proportional to the complexity of the system. Localized Modification According to the dictionary, localized means restrict something to a particular area. We change only one specific location in our existing code base to implement a new feature. The changed code must be deployed. Before you can run the example code, you need to install highline and clipboard gems. $gem install highline $gem install clipboard Here is the localized modification version of the password recall script: #!/usr/local/bin/ruby require 'digest/sha1' require 'highline/import' require 'clipboard' def unlock_password(account, domain) salt = ask("Enter your secret key : ") do |q| q.echo = false q.verify_match = true q.gather = {"Enter your secret key" => '', "" => ''} end password = Digest::SHA1.hexdigest(domain + account + salt) Clipboard.copy(password) end choose do |menu| domain = ask("Enter the website : ") menu.prompt = "Please make a selection : " menu.choice :yahoo do unlock_password('email', domain) say("Yahoo password copied.") end menu.choice :google do unlock_password('email', domain) say("Gmail password copied.") end menu.choice :microsoft do unlock_password('email', domain) say("Live password copied.") end end Here we modify just one file and we add a new site to menu.choice call. Additive We add new code to the existing system without modifying the existing code to implement a new feature. Risk of introducing bugs to existing code is very low. New code must be deployed. This will use polymorphism so that the new object introduced will have the same interface that the existing code uses. Here is an example from Rails Antipatterns by Chad Pytel and Tammer Saleh book that I have improved the design by moving it from localized modification to additive change. Before (Solution in the Book) class OrderConverter def initialize(order) @order = order end def to_xml end def to_json end def to_csv end def to_pdf end end oc = OrderConverter.new(order) oc.to_xml This solution needs localized changes to add a new conversion format for the order. After (My Improved Solution) Here is my solution that allows additive changes: class Order attr_reader :amount, :number def initialize(amount, number) @amount = amount @number = number end end class OrderXmlConverter def initialize(order) @order = order end def convert "<order><amount>#{@order.amount}</amount><number>#{@order.number}</number> </order>" end end Instead of hard-coding class name, you can use const_get to dynamically instantiate a class: order = Order.new(19, 2) format = 'Xml' class_name = Object.const_get("Order#{format}Converter") converter = class_name.new(order) puts converter.convert In rails, you can use constantize method: class_name = "Order#{format}Converter".constantize By following a convention in naming the converter class, we eliminate dependency on a specific class name. In order to add a new format, for instance json, we add a new class OrderJsonConverter which has the same interface convert that returns JSON representation. Uniform interface allows additive change. We end up with small classes that is focused on doing one thing really well, they all have the same interface, convert in our example. Data Driven New data is added to make the system implement a new feature. This is the most flexible design. Probably this design will demand the highest effort of implementation. No code deployment necessary. The example below requires reading the value of the array items from an external configurationfile. #!/usr/local/bin/ruby require 'digest/sha1' require 'highline/import' require 'clipboard' def unlock_password(account, domain) salt = ask("Enter your secret key : ") do |q| q.echo = false q.verify_match = true q.gather = {"Enter your secret key" => '', "" => ''} end password = Digest::SHA1.hexdigest(domain + account.to_s + salt) Clipboard.copy(password) end choose do |menu| domain = ask("Enter the website : ") menu.prompt = "Please make a selection : " # This is hard- coded. You must read the values of the list from an external configuration file to make it data driven that does not require source code changes to add a new site. items = [:yahoo, :google, :microsoft] items.each do |item| menu.choice item do unlock_password(item, domain) say("#{item.to_s} password copied to clipboard.") end end end Design Techniques What are the design techniques to achieve these three kinds of design? 1. Localized changes are better than changes that ripple across your code base. 2. Additive changes use polymorphism, meta-programming etc. It obeys Open Closed Principle if properly designed. New code is added with no modification to existing code. 3. Data driven technique obeys Open Closed Principle. No change is made in existing code. No new code is added. Summary In this chapter we saw three different kinds of design that gives different levels of flexibility. Sometimes you have to make a trade off between complexity and flexibility. You can recognize these different types of flexibility in your code and make decisions based on your current requirements. Reference Rails Antipatterns by Chad Pytel and Tammer Saleh Open Closed Principle In this chapter, you will learn about Open Closed principle. Let's consider the FizzBuzz problem to learn how to apply the Open Closed Principle. FizzBuzz requirements: - For multiples of 3, print Fizz - For multiples of 5, print Buzz - For multiples of 3 and 5, print FizzBuzz Steps Step 1 Define classes to implement the above requirements: class Fizz def value(n) if n % 3 == 0 'Fizz' end end end class Buzz def value(n) if n % 5 == 0 'Buzz' end end end class FizzBuzz def value(n) if n % 15 == 0 'FizzBuzz' end end end Step 2 One of the requirement is implicit, because numbers that is not multiple of 3, 5 or 15 should not be transformed. So we need a NoOp class: class NoFizzBuzz def value(n) n end end So far, we have the concrete classes that implement the FizzBuzz logic. Notice that we have a uniform interface value(n) that allows clients to program to an interface and not to an implementation. You will see this in action in upcoming steps. Step 3 Define FizzBuzzGenerator class that will delegate the FizzBuzz generation to the concrete classes. class FizzBuzzGenerator def initialize(objects, list) @list = list @objects = objects end def generate result = [] @list.each do |num| @objects.each do |l| v = l.value(num) unless v.nil? result << v break end end end result end end Notice that the dependency is on the message value(num). There is no dependency on the name of a class. So we don't have any references to Fizz, Buzz, FizzBuzz or NoFizzBuzz classes. This class is open for extension and closed for modification. This means we can add more concrete classes such as Fazz that returns multiples of 7 as Fazz, if such a new requirement arises without modifying this class and extend the functionality. Step 4 Finally, here is the test run: objects = [FizzBuzz.new, Fizz.new, Buzz.new, NoFizzBuzz.new] g = FizzBuzzGenerator.new(objects, (1..20).to_a) r = g.generate puts r Discussion The list of concrete classes (objects), needs to change only when new concrete classes are added. Deploying new feature requires additive changes. This means we add new concrete classes and an instance of that object to the objects array. The generator class does not require any modification to the existing code. This results in a flexible and easy to maintain code base. In our solution, notice that we don't have any if-else-elsif statements. If your solution used if-else-elsif then it would require Localized Changes and it would not be Additive Change. There is a subtle dependency between the FizzBuzzGenerator class and the order of the objects in the test run code. The correct generation of the FizzBuzz sequence depends on the order of objects. This is a quick-and-dirty implementation of Chain of Responsibility pattern. However this example was chosen to illustrate the Open Closed Principle. If the concrete classes have business logic that can be implemented by passing through a chain of handlers independent of the order in which they are executed, this solution would shine. Because, in that case, there would be no dependency on the order of the handlers in the objects array. Exercise In order to understand the concepts explained in this article, implement the feature where you must print Fuzz for multiples of 7. What are the changes required to satisfy the requirement? Summary In this chapter, you learned about the Open Closed Principle and how to apply it by working through FizzBuzz example. Table of Contents Introduction Basics of Abstraction Single Purpose Principle Stepwise Refinement Dependency Inversion Principle Basic Three Rules of Design The Art of Uniform Interface Localized Change vs Additive Change Coupling Basics : Dependency Direction Concrete Class vs Abstract Messages Flexible Design Open Closed Principle Introduction Basics of Abstraction Single Purpose Principle Stepwise Refinement Dependency Inversion Principle Basic Three Rules of Design The Art of Uniform Interface Localized Change vs Additive Change Coupling Basics : Dependency Direction Concrete Class vs Abstract Messages Flexible Design Open Closed Principle
Compartilhar