CSC/ECE 517 Fall 2012/ch2a 2w25 nr: Difference between revisions
No edit summary |
No edit summary |
||
Line 224: | Line 224: | ||
This change is simple enough. However, as the list of methods that require delegation grows your class can become littered with delegation code. | This change is simple enough. However, as the list of methods that require delegation grows your class can become littered with delegation code <ref name="Understanding The Law of Demeter in Ruby"> [http://ablogaboutcode.com/2012/02/27/understanding-the-law-of-demeter] </ref>. | ||
Fortunately Forwardable is included in the standard library of Ruby. Forwardable allows you delegate method calls to an object, on a method by method basis. | Fortunately Forwardable is included in the standard library of Ruby. Forwardable allows you delegate method calls to an object, on a method by method basis. |
Revision as of 17:00, 30 October 2012
Law of Demeter
Introduction
Law of demeter(LoD) states "Only talk to your friends", which means do not talk to friend's friend. By doing this, we don't get to know the internals of our friend. In software terms, a class A should only talk to or invoke methods on closely related classes, class B in this case and should not try to poke around the internals of B to reach a new class C. We have two options in order to achieve this, either class A can directly invoke methods on class C or the interface of class B should be changed to accomodate this call from A, then A will invoke methods on B and B will take care of invoking methods on C internally that would satisfy A's request.
In a way, law of demeter can be thought of a more specialized version of low coupling principle. Object oriented design recommends low coupling for better design and maintainability of software systems. The more loosely the classes are coupled, they become more easily testable. Classes need to directly interact with its collaborators and be shielded from understanding their internal structure. Using the internal knowledge of classes introduces risk of breaking functionality, instead they should talk to well known public interfaces. By doing all this we try to create a more shallow relationship than deeper relationship, which reduces the number of classes which may be affected by changes within the system.
Who can class A interact with?
If M is a method belonging to class A, then M may invoke other methods belonging to the following<ref name="">Who can class interact with</ref>:
1. Class A
2. Members of A
3. Parameters of A
4. Objects created by M
5. Objects created by other methods of class A called by M
6. Globally accessible objects
When and How to identify Law of Demeter
From the above sections we have a good understanding of the law and its benefits, but we haven't yet discussed on how to identify places in existing code where we can apply it and just as important, where not to apply it. The following are the places where we can apply the Law of Demeter
Chained 'get' statements
The below statement clearly denotes we are violating the law of demeter since we our access crosses the boundary.
value = object.getName().getCountry().getZip()
More number of dots is also an indicator of the violation of law.
Lots of 'temporary' objects
name = object.getName(); country = name.getCountry(); zip = country.getZip();
This is equivalent to chained get statement, but broken down into multiple statement, this is little more harder to detect that previous case, but this also clearly violates the law
Importing or including many classes
In Java, there is a rule which states that we only import classes that are actually used. These practises are never followed and we always see something like below in our source code.
import java.lang.* import java.io.*; import java.util.*;
With this rule in place, it is not uncommon to see a dozen or so import statements all coming from the same package. If this is happening in our code, it could be a good place to look for obscured examples of violations. If we need to import it, we are coupled to it. If it changes, we may have to change as well. By explicitly importing the classes, we will begin to experience how coupled the classes really are.
Advantages
The advantages of following Law of Demeter are<ref name="Advantages">Advantages</ref>:
Reusable code
Better design
Less risk of breaking functionality
Modular code
Disadvantages
In order not to expose the internals of the calling class, many layers of wrapper needs to be added. This introduces overhead in maintaining the wrapper and might introduce significant delay since control has to follow all the wrappers<ref name="Disadvantages">Disadvantages</ref>.
Example
Java
Law of Demeter is a technique for reducing the 'coupling' between objects in the code. Here is an example based on Java that illustrates the Law of Demeter<ref name="The Paperboy, The Wallet,and The Law Of Demeter">[1]</ref>.
A ‘Paperboy’ has to get a payment from a ‘Customer’. There has to be a mechanism used by the paperboy for receiving payments from that customer. Lets assume that the paperboy will run a snippet in order to achieve the above. The definition of each Java class for this example is as follows.
Customer
The Customer class can have a first name, a last name, an account number, a shipping address, some method of payment, etc. In this example lets define the customer with just enough functionality to make our illustration of Law of Demeter to work.
public class Customer { private String firstName; private String lastName; private Wallet myWallet; public String getFirstName(){ return firstName; } public String getLastName(){ return lastName; } public Wallet getWallet(){ return myWallet; } }
Wallet
Each customer is associated with a wallet by which the payment is made. A wallet can have total money, credits cards, drivers license, different types of currency, etc. For this example we design a pretty simplistic wallet.
public class Wallet { private float value; public float getTotalMoney() { return value; } public void setTotalMoney(float newValue) { value = newValue; } public void addMoney(float deposit) { value += deposit; } public void subtractMoney(float debit) { value -= debit; } }
Now that we have all required information about the customer and his wallet. Lets turn our focus on to the paperboy.
Paperboy
In this paperboy class we focus more on the method by which payment is made. In order to get the payment from the customer the paperboy executes a code snippet. The code snippet is some method in the Paperboy class (eg : getPayment() ) which achieves this objective. The typical getPayment method for this illustration is as follows
Paying the Paperboy - getPayment()
void getPayment(Customer myCustomer) { payment = 2.00; // Payment amount Wallet theWallet = myCustomer.getWallet(); // Wallet of the corresponding customer who has to pay if (theWallet.getTotalMoney() > payment) { theWallet.subtractMoney(payment); } else { // Other code } }
This code snippet gets the wallet from the customer, checks to make sure if the customer has the money and transfers it from the customers wallet into the paperboy's wallet.
Lets analyse what is bad about this code. We can actually translate what the code is actually doing into real-language. Apparently, when the paperboy stops by and demands payment, the customer is just going to turn around, let the paperboy take the wallet out of his back pocket, and take out two bucks. This is not the typical how some will handle their wallet.
There are a number of 'real-world' problems with this, not to mention we are trusting the paperboy to be honest and just take out what he's owed. If our future Wallet object holds credit cards, the paperboy has access to those too. The basic problem is that “the paperboy is being exposed to more information than he needs to be”.
The most important concept is the 'Paperboy' class now 'knows' that the customer has a wallet and can manipulate it. When we compile the Paperboy class, it will need the Customer class and the Wallet class. These three classes (Customer, Wallet and Paperboy) are now 'tightly coupled'. If we change the Wallet class, we may have to make changes to both of the other classes.
There is another classic problem that this can create. The customer wallet can be stolen or lost. In this case we can set the Wallet to null like
victim.setWallet(null);
This seems a reasonable assumption. This code enforces any mandatory value for wallet and there is a certain 'elegance' about using null for this condition but this never handles the paperboy. The code assumes there will be a wallet so the paperboy will get a runtime exception for calling a method on a null pointer. We could fix this by checking for 'null' on the wallet before we call any methods on it, but this starts to clutter the paperboy's class. The real-language description is becoming even worse as more and more complex arises. This makes the design and code more convoluted.
Code improvement , better design using Law of Demeter
The example illustrated can be made much better by incorporating proper design and improving the code structures. The proper way to fix the issue to move along the 'real world' scenario. When the paperboy needs payment, he would approach the customer but he is not going to handle the customer wallet. In fact he won't even know whether the customer has a wallet or not even if he does, he can’t be sure that the customer is willing to pay him.
The New Customer
The New customer has design changes such that he no longer has a 'getWallet()' method but it does have a getPayment method. The new customer class is as shown below
public class Customer { private String firstName; private String lastName; private Wallet myWallet; public String getFirstName(){ return firstName; } public String getLastName(){ return lastName; } public float getPayment(float bill) { if (myWallet != null) { if (myWallet.getTotalMoney() > bill) { theWallet.subtractMoney(payment); return payment; } } } }
The New Wallet
There won’t be any changes in design or the code of the Wallet as it performs the required function without any hassle in this example.
The New Paperboy
In this new paperboy class we have a method for getting the payment but this done by customer and not the right of the paperboy as per the previous design. In order to get the payment from the customer the paperboy executes a code snippet. The code snippet is some method in the Paperboy class (eg : requestPayment() ) which achieves this objective. The typical requestPayment method for this illustration is as follows
void RequestPayment(Customer myCustomer) { payment = 2.00; // Payment amount paidAmount = myCustomer.getPayment(payment); if (paidAmount == payment) { // Give customer a receipt } else { // Other code }
This had made the code and design much better.
Reasons that led to better design and code
The first reason that this is better is because it better models the real world scenario. The Paperboy code is now 'asking' the customer for a payment. The paperboy does not have direct access to the wallet.
The second reason that this is better is because the Wallet class can now change and the paperboy is completely isolated from that change. If the interface to Wallet were to change, the Customer would have to be updated, but that's it. As long as the interface to Customer stays the same, none of the client's of Customer will care that he got a new Wallet. Code will be more maintainable, because changes will not 'ripple' through a large project.
The third and probably most 'object-oriented' answer is that it is now free to change the implementation of 'getPayment()'. In the old example, the assumption was that the Customer would have a wallet. This led to the null pointer exception. In the real world though, when the paper boy comes, the customer may actually get the two bucks from a jar of change, search between the cushions of his couch or borrow it from his roommate, whatever. All of this is 'Business Logic',and is of no concern to the paper boy. All this could be implemented inside of the getPayment() method, and could change in the future, without any modification to the paper boy code.
Those these above discussion have led to lots of advantages we need to accept the fact that it has made the customer a more complex object.
Ruby
While developing software, Law of Demeter are violated often especially in Ruby on Rails which allows to easily navigate between objects based on table relationships<ref name="Law of Demeter in Ruby">[2]</ref>.
For example, if you are working on a partial that displays a charge from a credit card statement it wouldn't be surprising to see:
charge.statement.customer.name
The above code could be used to display the customer's name in the partial. Unfortunately, it assumes statement will have a customer and customer will have a name. This is easily fixed by changing charge to only talk to it's friends.
charge.customer_name
The simple fix to support this is to define customer_name in charge, and customer_name in statement
class Charge def customer_name statement.customer_name end end class Statement def customer_name customer.name end end
This change is simple enough. However, as the list of methods that require delegation grows your class can become littered with delegation code <ref name="Understanding The Law of Demeter in Ruby"> [3] </ref>.
Fortunately Forwardable is included in the standard library of Ruby. Forwardable allows you delegate method calls to an object, on a method by method basis.
Using Forwardable the above code becomes:
class Charge extend Forwardable def_delegators :statement, :customer_name end class Statement extend Forwardable def_delegator :customer, :name, :customer_name end
Forwardable becomes even more valuable when you need to delegate several methods def_delegators :amount_info, :units, :fractions, :currency.
References
<references/>