3

I have a module that generates a phone number in the format I need.

module PhoneNumber def self.prefix '+' end def self.country rand(1..9).to_s end def self.code rand(100..999).to_s end def self.number rand(1000000..9999999).to_s end end 

I use it as follows. Or as a formatted string "#{}#{}".

phone_number = PhoneNumber.prefix + PhoneNumber.country + PhoneNumber.code + PhoneNumber.number 

I want to rewrite the body of the module in this way, so that I can use it in a dotted format.

PhoneNumber.prefix.code.number 
1
  • 1
    You could do this with some effort, but chained methods don’t semantically mean what you’re trying to do. Why not take the parts as arguments? Commented Aug 24, 2018 at 18:29

3 Answers 3

6

I agree with Todd that you shouldn't do this; it will be awkward to implement and confusing for people who read the code (including yourself in the future) as it's not the typical way to write the sort of operations you're talking about.

That said, if you really want to do so, you'll need to provide a method called prefix which returns another object which has a method code and so on, storing state along the chain and combining the strings at the end. It would get even more complicated if you want to swap around the order.

Strongly recommend a method that looks like this instead:

PhoneNumber.build(:prefix, :code, :number) 
Sign up to request clarification or add additional context in comments.

5 Comments

How is it confusing? It’s a well-known builder pattern.
@mudasobwa Methods aren't members, and "method train wrecks" aren't idiomatic Ruby.
@ToddA.Jacobs how about Rails’ relations/scopes that are being built exactly that way? How about Rails’ 10.seconds.ago? I dislike this way of building objects, and I hate Rails, but I wouldn’t call it non-idiomatic.
@ToddA.Jacobs also, unlike your solution, mine supports putting two country codes as well as putting a country code after the number (exactly as Rails’ scopes do.) If the OP’s original example was contrived, it might matter to have an ability to mix the order.
@mudasobwa Your solutions address the OP's question, but I feel like it's an X/Y problem. I chose to address X rather than Y, but I cited your solutions (and especially your class solution) and elliotcm's as viable alternatives.
3

To chain methods one should basically constantly return self from all the methods you are going to chain:

module PhoneNumber @number = '' def self.prefix @number << '+' self end def self.country @number << rand(1..9).to_s self end def self.code @number << rand(100..999).to_s self end def self.number @number << rand(1000000..9999999).to_s self end def self.to_s @number end end puts PhoneNumber.prefix.code.number #⇒ +9065560843 

Note the explicit #to_s implementation for the last step, since you probably want a string as an outcome, not the class itself.

This implementation has a glitch: it is hardly reusable, since there is a single shared @number, hence you’d better make all methods as instance methods and do:

class PhoneNumber def initialize @number = '' end def prefix @number << '+' self end def country @number << rand(1..9).to_s self end def code @number << rand(100..999).to_s self end def number @number << rand(1000000..9999999).to_s self end def to_s @number end end puts PhoneNumber.new.prefix.code.number #⇒ +6117160297 

Now it works for subsequent calls.

1 Comment

The code works, but I think it remains semantically dubious. A typical reader might reasonably expect #number to act on the return value of #code, and code to act on the return value of #prefix. It does, under the hood, but you aren't really "numbering the code" or "coding the prefix." This could be fixed by renaming the methods, like #append_country_prefix or #append_area_code, but the original code is speaking to a different mental map. I'm glad you helped the OP, but I genuinely think the solution is perpetuating a design problem.
1

Wrap Your String Construction Into a Class Method

"Dotted format" is really a set of chained methods. While you could conceivably do something similar to what you're trying to do by appending each class method's output to a String in your method chain, I would consider this a brittle and unsavory OOP design.

On a semantic level, each method in a chain means "do x to the return value of the previous method." When you want to describe the members of an object, or a different kind of initialization, there are more idiomatic ways to do it.

Without making significant changes to your existing code, you can simply add a PhoneNumber#create class method to do the heavy lifting for you. For example:

module PhoneNumber def self.create [self.prefix, self.country, self.code, self.number].join end def self.prefix '+' end def self.country rand(1..9).to_s end def self.code rand(100..999).to_s end def self.number rand(1000000..9999999).to_s end end if __FILE__ == $0 puts PhoneNumber.create end 

Joining the array of String objects returned by your other class methods is reasonably idiomatic, semantically clear, and sidesteps the need to alter the existing class methods, which other objects in your programs may currently rely on. This localizes change, which is often a good thing in OOP design.

Parameterizing a method or converting your module to a class, as described in other answers to your question, are also reasonable alternatives. Naturally, your mileage and stylistic tastes may vary.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.