1

I coded up a solution to the narcissistic numbers kata on codewars.

After writing a single function, I extracted two helper functions in order to keep my line count at a maximum of 5 lines (Sandi Metz' Rules For Developers).

This resulted in 3 functions:

def digits(number)
  number
    .to_s
    .chars
    .map(&:to_i)
end

def checksum(digits, exp)
  digits
    .map { |d| d**exp }
    .reduce(:+)
end

def narcissistic?(number)
  digits = digits(number)
  exp = digits.length
  checksum = checksum(digits, exp)
  checksum == number
end

Now, I would like to pretend that this code should be added to a larger real-world project. My question is how this should be idiomatically done in Ruby.

Generally speaking, I have two requirements:

  1. The code should be somehow namespaced (considering a real-world project).
  2. It should be clear that narcissistic? is the public API function - being on a higher level, while the other two functions digits and checksum are on a lower level of abstraction.

My reasoning so far is: This code does not really need OOP. But in Ruby the only way to get something into a namespace is by creating a Class or a Module.

Probably, a Module would be a better choice? Still, I am not sure whether I should prefer:

module MathUtils::NarcissisticNumbers
  def self.narcissistic?(number)
    ...
  end

  private
  ...
end

vs

module MathUtils::NarcissisticNumbers
  def narcissistic?(number)
    ...
  end

  private
  ...
end

How would you bring in this code into a Ruby project? Please, if you know a best-practices solution, let me know! :)

Any other pointers would be highly appreciated as well.

Peter Slotko
  • 327
  • 2
  • 10

2 Answers2

0

in my opinion, it's depend on the purpose of your method, consider 2 names of narcissistic method:

  1. narcissistic?(number): this make me think there's an outside class that take responsible for checking whether the input number is narcissistic or not.

  2. narcissistic?: this make me think about the class itself be able to check that whether it's narcissistic or not.

So in case 1, let assume that you have a class Code that include the module MathUtils::NarcissisticNumbers, if that module does not support class method then only instances of the class Code can_do check narcissistic, then the method name should be fall into case 2 above.

On the other hand, if the module support class method, then the method name should fall into case 1, however, suppose you have a class Money that need to check narcissistic it's value, if you use Code.narcissistic?(money.value) that'll make other confuse (at least they need to know what's Code), but it's total make sense if you use MathUtils::NarcissisticNumbers.narcissistic?(money.value), other will understand immediately that is a checking a kind of number method.

I suggest that you let MathUtils::NarcissisticNumbers is a module_function and create another module for narcissistic?

module MathUtils::NarcissisticNumbers
  module_function
  def is_narcissistic?(number)
  end
end

module Narcissistic
 def narcissistic?
   MathUtils::NarcissisticNumbers.is_narcissistic?(self.value)
 end
end

class Code
 include Narcissistic
end

class Money
 include Narcissistic
end

code = Code.new(...)
code.narcissistic?

# for those classes that only check narcissistic? internally
# then you can include MathUtils::NarcissisticNumbers
# since is_narcissistic?(number) become a private method

class FormatNumber
 include MathUtils::NarcissisticNumbers
 def format(number)
   if is_narcissistic?(number)
    # ...
   else
    # ...
   end
 end
end

# you can use MathUtils::NarcissisticNumbers wherever you want (as helper)
# on other classes that not include Narcissistic, including views , ...
<% if MathUtils::NarcissisticNumbers.is_narcissistic?(input) %>
Lam Phan
  • 3,405
  • 2
  • 9
  • 20
0

I agree with most things what Lam already wrote. However, I would extract a class first which you use in your module. Classes make it a much more easy to work with data (and to follow the advice your methods should be max 5LOC).

class MathUtils::NarcissisticNumber
  def initialize(number)
    @number = number
  end

  def valid?
    checksum == number
  end

  private
  
  attr_reader :number

  def checksum
    digits.map { |d| d**exponent }.reduce(:+)
  end

  def digits
    @digits ||= number.to_s.chars.map(&:to_i)
  end

  def exponent
    @exponent ||= digits.length
  end
end

By using a class we were able to remove all method parameters and temp variables. We can now use this class in a helper module suggested by Liam.

module MathUtils::NarcissisticNumbers
  def narcistic?(number)
    NarcissisticNumber.new(number).valid?
  end
end